Code Monkey home page Code Monkey logo

epistasislab / tpot2 Goto Github PK

View Code? Open in Web Editor NEW
146.0 8.0 22.0 5.53 MB

A Python Automated Machine Learning tool that optimizes machine learning pipelines using genetic programming.

Home Page: https://epistasislab.github.io/tpot2/

License: GNU Lesser General Public License v3.0

Jupyter Notebook 69.18% Python 30.82%
adsp aiml alzheimer alzheimers automated-machine-learning automation automl data-science feature-engineering gradient-boosting

tpot2's Introduction

TPOT2 ALPHA

Tests PyPI Downloads Conda Downloads

TPOT stands for Tree-based Pipeline Optimization Tool. TPOT2 is a Python Automated Machine Learning tool that optimizes machine learning pipelines using genetic programming. Consider TPOT2 your Data Science Assistant.

TPOT2 is a rewrite of TPOT with some additional functionality. Notably, we added support for graph-based pipelines and additional parameters to better specify the desired search space. TPOT2 is currently in Alpha. This means that there will likely be some backwards incompatible changes to the API as we develop. Some implemented features may be buggy. There is a list of known issues written at the bottom of this README. Some features have placeholder names or are listed as "Experimental" in the doc string. These are features that may not be fully implemented and may or may not work with all other features.

If you are interested in using the current stable release of TPOT, you can do that here: https://github.com/EpistasisLab/tpot/.

License

Please see the repository license for the licensing and usage information for TPOT2. Generally, we have licensed TPOT2 to make it as widely usable as possible.

Documentation

The documentation webpage can be found here.

We also recommend looking at the Tutorials folder for jupyter notebooks with examples and guides.

Installation

TPOT2 requires a working installation of Python.

Creating a conda environment (optional)

We recommend using conda environments for installing TPOT2, though it would work equally well if manually installed without it.

More information on making anaconda environments found here.

conda create --name tpot2env python=3.10
conda activate tpot2env

Note for M1 Mac or other Arm-based CPU users

You need to install the lightgbm package directly from conda using the following command before installing TPOT2.

This is to ensure that you get the version that is compatible with your system.

conda install --yes -c conda-forge 'lightgbm>=3.3.3'

Installing Extra Features with pip

If you want to utilize the additional features provided by TPOT2 along with scikit-learn extensions, you can install them using pip. The command to install TPOT2 with these extra features is as follows:

pip install tpot2[sklearnex]

Please note that while these extensions can speed up scikit-learn packages, there are some important considerations:

These extensions may not be fully developed and tested on Arm-based CPUs, such as M1 Macs. You might encounter compatibility issues or reduced performance on such systems.

We recommend using Python 3.9 when installing these extra features, as it provides better compatibility and stability.

Developer/Latest Branch Installation

pip install -e /path/to/tpot2repo

If you downloaded with git pull, then the repository folder will be named TPOT2. (Note: this folder is the one that includes setup.py inside of it and not the folder of the same name inside it). If you downloaded as a zip, the folder may be called tpot2-main.

Usage

See the Tutorials Folder for more instructions and examples.

Best Practices

1

TPOT2 uses dask for parallel processing. When Python is parallelized, each module is imported within each processes. Therefore it is important to protect all code within a if __name__ == "__main__" when running TPOT2 from a script. This is not required when running TPOT2 from a notebook.

For example:

#my_analysis.py

import tpot2
if __name__ == "__main__":
    X, y = load_my_data()
    est = tpot2.TPOTClassifier()
    est.fit(X,y)
    #rest of analysis

2

When designing custom objective functions, avoid the use of global variables.

Don't Do:

global_X = [[1,2],[4,5]]
global_y = [0,1]
def foo(est):
    return my_scorer(est, X=global_X, y=global_y)

Instead use a partial

from functools import partial

def foo_scorer(est, X, y):
    return my_scorer(est, X, y)

if __name__=='__main__':
    X = [[1,2],[4,5]]
    y = [0,1]
    final_scorer = partial(foo_scorer, X=X, y=y)

Similarly when using lambda functions.

Dont Do:

def new_objective(est, a, b)
    #definition

a = 100
b = 20
bad_function = lambda est :  new_objective(est=est, a=a, b=b)

Do:

def new_objective(est, a, b)
    #definition

a = 100
b = 20
good_function = lambda est, a=a, b=b : new_objective(est=est, a=a, b=b)

Tips

TPOT2 will not check if your data is correctly formatted. It will assume that you have passed in operators that can handle the type of data that was passed in. For instance, if you pass in a pandas dataframe with categorical features and missing data, then you should also include in your configuration operators that can handle those feautures of the data. Alternatively, if you pass in preprocessing = True, TPOT2 will impute missing values, one hot encode categorical features, then standardize the data. (Note that this is currently fitted and transformed on the entire training set before splitting for CV. Later there will be an option to apply per fold, and have the parameters be learnable.)

Setting verbose to 5 can be helpful during debugging as it will print out the error generated by failing pipelines.

Contributing to TPOT2

We welcome you to check the existing issues for bugs or enhancements to work on. If you have an idea for an extension to TPOT2, please file a new issue so we can discuss it.

Known issues

  • TPOT2 uses the func_timeout package to terminate long running pipelines. The early termination signal may fail on particular estimators and cause TPOT2 to run for longer than intended. If you are using your own custom configuration dictionaries, and are noticing that TPOT2 is running for longer than intended, this may be the issue. We are currently looking into it. Sometimes restarting TPOT2 resolves the issue.
  • Periodic checkpoint folder may not correctly resume if using budget and/or initial_population size.
  • Population class is slow to add new individuals. The Population class needs to be updated to use a dictionary for storage rather than a pandas dataframe.
  • Crossover may sometimes go over the size restrictions.
  • Memory caching with GraphPipeline may miss some nodes where the ordering on inputs happens to be different between two nodes.

Support for TPOT2

TPOT2 was developed in the Artificial Intelligence Innovation (A2I) Lab at Cedars-Sinai with funding from the NIH under grants U01 AG066833 and R01 LM010098. We are incredibly grateful for the support of the NIH and the Cedars-Sinai during the development of this project.

The TPOT logo was designed by Todd Newmuis, who generously donated his time to the project.

tpot2's People

Contributors

ethanglaser avatar jay-m-dev avatar jgh9094 avatar nickmatsumoto avatar nickotto avatar perib avatar theaksaini avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tpot2's Issues

Avoid duplicate work for steady state and base evolvers. Have one evolver that can do both generational EA, but also allow for extra individuals to be generated on the fly to make use of idle cores.

In base evolver, it is common to have idle cores while the last few individuals in a population are being evaluated. In the steady state evolver, a new individual is generated and sent to dask when a core is idle. This eliminates idle cores. However, in some cases the generational approach performs better, hence the need to keep both possible.

Currently, we have fixed population size EA in Baseevolver, and pure steady state EA in the steadystateevolver. (These also have slightly different uses for the same parameter names, which is confusing). We also now have to estimators, one for each evolver. Maintaining both revolvers can lead to duplicate work.

We could have one evolver that supports both the classic generations approach, steady state, and generational with extra individuals for idle cores. (Note, this does not mean having separate implementations under one evolver. The same implementation can be used for both)

The parameters of this evolver could be:

generations # stopping condition based on the number of generations
initial_population_size # number of individuals in the initial population
parent_size # number of parents that are available to be used for generating new offspring
offspring_size #number of individuals generated each generation.
opportunistic_offsprint (bool) # This is whether we allow extra cores to be used for extra individuals within a generation (ignored if offspring_size=None)

In this API, if offspring_size=None, then we update the parent population after every individual as they complete the evaluation. if offspring_size is an int, then the algorithm is equivalent to the generational approach. if opportunistic_offspring is True, then we also generate extra individuals during the generational approach if cores are idle.

I believe this can be implemented with the existing steady state evolver by adding a conditional as to when the parent population is updated.

if offspringsize>0 : Only update the parent population if we completed evaluating all offspring this generation (not including extras) OR

else: update parent population after every individual.

API : improve selector module organization

We may need to differentiate between selection methods with possible replacement (for matching parents) and those without replacement (such as select best, used for cutting the population down)

Should we split selection functions into two modules: parent selection and survival selection?

optimization of preprocessing pipeline

This would be done automatically as a preprocessing step or as part of the evolutionary algorithm. For the latter, a configuration dictionary could be defined such that the first segment would be a linear sequence of preprocessors, followed by the user-specified search space. This should be possible with the existing support for recursive graphindividuals, though the interface may be confusing?

Licensing text in source files

TPOT1 had some files with licensing text from other packages (for example: https://github.com/EpistasisLab/tpot/blob/master/tpot/builtins/one_hot_encoder.py)

Do we add those here as well?

Additionally, TPOT1 started its source files with the following text. Do we do the same in TPOT2?

-- coding: utf-8 --

"""This file is part of the TPOT library.

TPOT was primarily developed at the University of Pennsylvania by:
- Randal S. Olson ([email protected])
- Weixuan Fu ([email protected])
- Daniel Angell ([email protected])
- and many more generous open source contributors

TPOT is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.

TPOT is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with TPOT. If not, see http://www.gnu.org/licenses/.

"""

Make the cv early stop functions into standalone functions? Or remove?

Currently, these are built into the evolver class. If we merge the revolver classes, this may be easier to use as a standalone function #65

The two strategies for pruning CV evaluation early are found in the evaluate_population_selection_early_stop function of the baseevolver. They are controlled by the following parameters, which may be confusing.

                    threshold_evaluation_early_stop = None, 
                    threshold_evaluation_scaling = .5,
                    min_history_threshold = 20,
                    selection_evaluation_early_stop = None,
                    selection_evaluation_scaling = .5,
                    evaluation_early_stop_steps = None, 
                    final_score_strategy = "mean",

It might be easier to pull these out and turn them into individual functions outside of the class, similar to the optuna pruning API.

But also, this feature may not be useful given that we already support successive halving. We could evaluate each independently and both together. If successive halving gives the same performance improvements, it is possible we could drop this feature to simplify the code.

shuffle cv folds every generation?

TPOT can get very good at overfitting the cv score. Maybe shuffling the cv fold every generation would counteract this? It would add noise to the learning process, but in some evolutionary algorithms, noise can be beneficial.

add a debugging tutorial

This tutorial could cover basic debugging steps and common issues.

For example, identifying that the algorithm isn't training due to an incorrectly written objective function, plotting a training curve, checking for overfitting of the CV fold, etc.

Genetic Feature Selection Improvements

Feature selection can be done using the FeatureSetSelector by specifying groups of columns as a "set" or by simply selecting a single column per node. Each set or feature will be encoded in an individual FeatureSetSelector node. To mix and match sets/features, the algorithm must manually move around these nodes.

This can be inefficient with a high number of individual columns or sets to select from. This is because the mutation and crossover operations would generally move single nodes around. Multiple nodes may be included in a crossover, but only if they come attached to a selected downstream node.

Another option could be to create a special case node within the GraphIndividual that has its own mutation and crossover functions. These mutation/crossover functions would be able to mix and match a large number of features in a single operation, similar to other implementations of genetic feature selection in other packages.

Idea for a two step preprocessing pipeline

Currently, the preprocessing pipeline is applied to the entire training set before the evolution of pipelines. This is fine for things like one hot encoding.

We may want some parts of preprocessing to be trained per fold of cross-validation, such as iterative imputer. However, this is expensive and should be done only once.

This is something we may consider implementing into TPOT2. However, it is also something users can implement themselves as a custom objective function as well.

Implement Meta Learner

A meta-learning model could be used to predict optimal parameters for the evolutionary algorithm given a dataset.

For an example, see Auto-Sklearn paper

Can we improve the configuration files to be easier to read and write?

Currently, the configuration files are designed as optuna-compatible functions, but this adds boilerplate (such as f"_{name}".) And maybe confusing and hard to understand.

It would help with clarity if we can have a different format that still supports the features of the optuna suggestions and types, but is easier to read and write.

This may also make it easier to set random states for reproducibility #53

function that generates a learning curve

It looks at the evaluated_individuals log and generates a plot of time or generations vs. each objective function.

This would be helpful for debugging and may provide a visual indication or whether or not training has converged.

Avoiding repeated work : Cache node models with node hashing or manually.

During the evolutionary algorithm, TPOT2 will fit the same exact data to the same estimator. Ideally, we should be able to catch this and use a cached version of the estimator that we previously trained.

Currently, we have support for the Joblib Memory module. The Joblib memory parameter can be used to cache inputs and outputs of the fit functions within the graphpipeline. This is memory/storage intensive. As it needs to store multiple transformations of the data.

Another approach could be to use graph theory to hash node locations within a graph. Then we can store just the fitted estimator without storing the inputs and outputs.

One more idea could be to store fitted pipelines manually within the graphindividual. Mutation/crossover operations could keep track of which nodes need to be refitted. Crossover would copy the fitted estimator to other graphindividuals. We would have to store a fitted module for each node and for each fold. This would work but would add a lot of complexity to the code.

Link GitHub Pages in README.md

Reaching the GitHub Pages from the main site of the repository as a first-time user is counterintuitive and takes too many clicks.

Context of the issue

I was reading this repository for the first time and wanted to find the documentation for the individual functions and inner classes but did not see it in the code; therefore, I went to open an issue about the lack of documentation, found issue #1, and all the way at its bottom issue #21, at the bottom of which I found the link to the GitHub pages.

Process to reproduce the issue

Use the repository for the first time without knowing where to find this documentation.

Expected result

This documentation would be easier-to-find.

Current result

This documentation is hard-to-find.

Possible fix

Link the GitHub pages in the README.md.
Here's the link text: https://jay-m-dev.github.io/tpot2/
Here it is as a clickable link, which could be put in the README.md for new readers to easily find.

Fix miscellaneous invalid hyperparameter options in the configuration dictionary. And add n_jobs=1 when possible.

I went through some logs to check for classifiers, transformers, and selectors issues. Those seem to not generate invalid hyperparameter combinations. I have not checked regression yet.

It would also be good to check for extreme values that may be unreasonably slow or blow up memory. For example, when PolynomialFeatures was set to a high degree, it would often create extremely large matrixes that would blow up memory and potentially crash the system.

Also need to add n_jobs=1 to parameters that have an n_jobs parameter.

edit/consolidate default configuration dictionary

Regressors:

We include ElasticNetCV, LassoLarsCV, SGDRegressor, and RidgeCV. These are all basically linear regression. Let's remove all but SGDRegressor (or maybe ElasticNetCV). SGDRegressor can also potentially overlap with SVR. Or remove all and add sklearn.linear_model.LinearRegression

Transformers:
I don't think that MaxAbsScaler and MinAbsScaler are often useful, so I would argue for their removal.

Similarly, I don't think we need both RobustScaler and StandardScaler

Classifiers:
SGDClassifier can implement both LogisticRegression and SVC. I would keep SVC and Logistic regression and remove SGDClassifier.

related to #60

better thread efficiency for cv prunning with thresholds

We could have better thread efficiency for early stopping with thresholds (when not used with selection early stopping). Currently, all threads need to come together at the end of each round of CV. Would be better if a single thread would evaluate a all the folds until termination (the same as regular evaluation).

related to #69 . It may be better to implement this as a standalone function or create a pruning API.

implement a callback function/class

This would allow users to execute custom code during the evolutionary algorithm. Some ideas for what this could potentially be used for include logging, early stopping, custom changes to offspring generation, or cross-validation pruning.

Population class logging is inefficient and needs to be optimized.

The population class stores its logs in a pandas data frame. This has very slow appends, which get slower the more items are in the data frame. This becomes a bigger issue with long running evolutions or evolutions with short evaluation times that can iterate through individuals quickly. For example, Tutorial 8 becomes noticeably slower as it progresses.

Also related: Pandas 2.0.0 breaks some code in the Population class. Currently, it only supports 1.5.3

The Population class needs to be optimized with a better underlying data structure. Perhaps an SQL structure? Or a dictionary of dictionaries?
I think using a pandas data frame with preallocation would not be ideal.

allow individual modules to be specified by string for config dicts. For example ["LogisticRegression", "KNN"] etc.

Add a function that takes in a string name for a sklearn pipeline or the sklearn type (e.g "LogisticRegression" OR sklearn.linear_model.LogisticRegression) and returns the appropriate params function. So that we can support users easily choosing a specific set of operators one by one. (e.g. root_config_dict=["logisticregression", "randomforest"]). This function should take the following parameters name, n_samples, n_features, classification, subsets=None, feature_names=None, n_classes=None. It will be used in this function https://github.com/EpistasisLab/tpot2/blob/main/tpot2/tpot_estimator/estimator_utils.py#L24C43-L24C130

Idea for Successive Halving to avoid repeated re-evaluations of the same pipeline.

Currently, successive halving reduces computation by subsampling the number of rows. Every time the budget increases, the existing parent population is re-evaluated at the next budget, which may be inefficient. Another approach could be to make budgets related to CV folds. We could use only one fold to evaluate the first N generations. Then evaluate two folds when the budget increases. In this case, the parent population wouldn't have to be re-evaluated from scratch; it would just be evaluated on the next fold of CV.

Benchmarks available?

Been a while since I used Tpot in a project, and would like to try out the V2 version in a subsequent upcoming project. Any benchmarks available to date comparing TPOT2 Alpha to TPOT1? Or is it mainly just a refactoring of the codebase.

better plotting for graph pipeline

Graph pipeline plots could be improved. The graphviz package may be worth looking into.

other DAG sklearn pipeline packages have nice graphs, for example Baikal and FEDOT

Error in Last Block of Tutorial 7

I tried running Tutorial 7 of the dev branch but got an error in the last block.

Context of the issue

After the error in Block 1 was fixed in issue #41 , I cloned the dev branch and ran Tutorial 7 again. All blocks worked except the last one, which yielded, over about ten seconds, a multi-page error log, almost all of which seemed generated by TPOT2 logging, and which ended in a Python error. Wondering if the problem was on my end, I checked the Tutorial 7 on the dev branch of the repository on GitHub. All blocks there had run except the seventh one, which had not started. I wonder if these two observations are related.

Process to reproduce the issue

  1. Clone the dev branch
  2. Open Jupyter
  3. Run Tutorial 7

Expected result

I don't know what I should have expected because the code hasn't run on the Tutorial 7 notebook on the dev branch on GitHub either.

Current result

Including just the Python error because the whole thing would go on too long.

TimeoutError                              Traceback (most recent call last)
Cell In[7], line 27
     24 est = tpot2.TPOTEstimatorSteadyState( client=client, classification=True, max_eval_time_seconds=60, max_time_seconds=120, scorers=['roc_auc_ovr'], scorers_weights=[1], verbose=1)
     25 # this is equivalent to: 
     26 # est = tpot2.TPOTClassifier(population_size= 8, generations=5, n_jobs=4, memory_limit="4GB", verbose=1)
---> 27 est.fit(X_train, y_train)
     28 print(scorer(est, X_test, y_test))
     30 #It is good to close the client and cluster when you are done with them

File ~/Documents/TPOT2/tpot2/tpot_estimator/steady_state_estimator.py:673, in TPOTEstimatorSteadyState.fit(self, X, y)
    662 self.individual_generator_instance = tpot2.individual_representations.graph_pipeline_individual.estimator_graph_individual_generator(   
    663                                                     inner_config_dict=inner_config_dict,
    664                                                     root_config_dict=root_config_dict,
   (...)
    667                                                     linear_pipeline=self.linear_pipeline,
    668                                                         )
    672 if self.scatter:
--> 673     X_future = _client.scatter(X)
    674     y_future = _client.scatter(y)
    675 else:

File ~/Documents/miniconda3/lib/python3.11/site-packages/distributed/core.py:1374, in PooledRPCCall.__getattr__.<locals>.send_recv_from_rpc(**kwargs)
   1372 prev_name, comm.name = comm.name, "ConnectionPool." + key
   1373 try:
-> 1374     return await send_recv(comm=comm, op=key, **kwargs)
   1375 finally:
   1376     self.pool.reuse(self.addr, comm)

File ~/Documents/miniconda3/lib/python3.11/site-packages/distributed/core.py:1158, in send_recv(comm, reply, serializers, deserializers, **kwargs)
   1156     _, exc, tb = clean_exception(**response)
   1157     assert exc
-> 1158     raise exc.with_traceback(tb)
   1159 else:
   1160     raise Exception(response["exception_text"])

File ~/Documents/miniconda3/lib/python3.11/site-packages/distributed/core.py:930, in _handle_comm()
    928     result = handler(**msg)
    929 if inspect.iscoroutine(result):
--> 930     result = await result
    931 elif inspect.isawaitable(result):
    932     raise RuntimeError(
    933         f"Comm handler returned unknown awaitable. Expected coroutine, instead got {type(result)}"
    934     )

File ~/Documents/miniconda3/lib/python3.11/site-packages/distributed/scheduler.py:5872, in scatter()
   5870         break
   5871     if time() > start + timeout:
-> 5872         raise TimeoutError("No valid workers found")
   5873     await asyncio.sleep(0.1)
   5875 nthreads = {ws.address: ws.nthreads for ws in wss}

TimeoutError: No valid workers found

Possible fix

I don't know how to fix this bug but would be delighted to contribute elsewhere in this very important repository.

Idea: Preventing getting stuck with variable number of mutations

TPOT2 can often get stuck without learning for long periods before finding a "breakthrough." One idea to potentially try to break out of this sooner could be to increase the number of mutations applied every generation in which there is no improvement in either the best score or the Pareto front.

Error in Tutorial 7

I tried running Tutorial 7 but got an error in the first cell.

Context of the issue

I wanted to try TPOT2 to benchmark it. I cloned the repository, opened Jupyter, ran Tutorial 7, and got a red error box under the first cell.

Process to reproduce the issue

  1. Clone the git repository
  2. Open Jupyter
  3. Run the first code block of Tutorial 7

Expected result

I don't know what I should have expected exactly, but I did expect the code would run.

Current result

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 13
     11 X, y = sklearn.datasets.load_digits(return_X_y=True)
     12 X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(X, y, train_size=0.75, test_size=0.25)
---> 13 est = tpot2.TPOTClassifier(population_size= 8, generations=5,)
     14 est.fit(X_train, y_train)
     15 print(scorer(est, X_test, y_test))

TypeError: TPOTClassifier.__init__() got an unexpected keyword argument 'population_size'

Rename survival_percentage to something clearer.

right now population_size is the number of generated offspring. survival_percentage*population_size is the number of parents to keep each generation. survival_percentage was developed with successive halving in mind, with the assumption that we want to scale the parent size with the population size.

We could rename survival_percentage to parent_size, and have successive halving scales the parent size proportional to population_size:parent_size

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.