Giter Site home page Giter Site logo

wang-zerui / optaplanner-alter Goto Github PK

View Code? Open in Web Editor NEW
10.0 1.0 0.0 208.36 MB

optaplanner核心替换项目

License: Apache License 2.0

Groovy 0.09% Shell 0.04% Batchfile 0.02% Java 96.13% FreeMarker 0.94% CSS 0.27% XSLT 0.01% JavaScript 2.45% HTML 0.06%

optaplanner-alter's Introduction

〽️Optaplanner算法替换文档

一、概要描述

本文档主要讲解在optaplanner代码中进行算法替换的流程和方法。

1.1 数据字典

序号 名称 含义
1 solution 问题的解决方案,一个规划问题有很多解决方案,算法的目的就是得到一个最优或次优的解决方案
2 constraints 问题约束,表示对问题约束条件,同时也是计算solution的score的规则。数学上一般是很多不等式
3 workingSolution 工作解决方案(当前),问题搜索最佳解决方案过程中经过的solution
4 bestSolution 最佳解决方案(当前),问题求解过程中对当前得分最高的solution的称呼
5 solver 问题求解器,可以通过配置文件进行配置,配置内容包括算法配置和顺序、终止条件、随机数设置等
6 phase 问题“求解阶段”,一般一个问题的求解会有多个阶段,每个阶段使用一种算法
7 score 一个solution的得分,分数计算规则的在contraints中定义
8 bestScore bestSolution的得分(score)
9 domain model 领域模型,对规划问题的定义,包含若干特定属性——问题事实(problem fact),计划实体、计划变量等
10 move 从一个solution到另一个solution的改变
11 邻域 一个solution经过一个move所能到达的所有solution的集合

1.2 主要类

​ optaplanner代码中多用工厂设计模式,template method等。

  • 这一小节主要帮助理解后面的算法替换
  • 实际的算法替换过程不一定用得上其中的某些类,因为optaplanner会对他们进行操作,不用算法进行操作

1.2.1 domain类

​ 包括如下类

  • 计划实体(planning entity),问题求解过程中可以改变的类
  • 计划变量(panning variable),规划实体类的属性(或属性) ,在解决过程中发生变化。在此示例中,它是类 Process 上的属性计算机。
  • 影子变量(shadow variable),如果存在多个关系和字段是计划变量,那么可能存在影子变量,它的值基于一个或多个计划变量计算得来
  • 问题事实(problem fact),optaplanner无法改变的输入数据,一般是规划实体的数量等
  • 问题(planning solution),解决方案类

1.2.2 solverConfig

​ 问题求解器配置,有两种创建方式

  • 直接在代码中嵌入

    SolverFactory<CloudBalance> solverFactory = SolverFactory.create(new SolverConfig()
                    .withSolutionClass(CloudBalance.class)
                    .withEntityClasses(CloudProcess.class)
                    .withConstraintProviderClass(CloudBalancingConstraintProvider.class)
                    .withTerminationSpentLimit(Duration.ofMinutes(2)));
  • 基于xml配置文件定义

    <solver>
      <randomSeed>0</randomSeed>
     <solutionClass>org.optaplanner.examples.nqueens.domain.NQueens</solutionClass>
      <entityClass>org.optaplanner.examples.nqueens.domain.Queen</entityClass>
      <scoreDirectorFactory>
        <constraintProviderClass>org.optaplanner.examples.nqueens.score.NQueensConstraintProvider</constraintProviderClass>
        <initializingScoreTrend>ONLY_DOWN</initializingScoreTrend>
      </scoreDirectorFactory>
    
      <constructionHeuristic>
    	...
      </constructionHeuristic>
      <localSearch>
    	...
      </localSearch>
    </solver>
    

​ solverCofig中主要包含如下定义

  • Score configuration:分数配置,包括硬约束和软约束。optaplanner支持多种实现方式(java se,ConstraintStreams,drools引擎)

  • Domain model configuration,领域模型配置(What optaplanner can change)

    <solutionClass>org.optaplanner.examples.cloudbalancing.domain.CloudBalance</solutionClass>
    <entityClass>org.optaplanner.examples.cloudbalancing.domain.CloudProcess</entityClass>
  • Optimization algorithms configuration,算法配置。包含求解使用算法类型和算法属性和参数配置。我们的替换算法的配置方式后面会讲解

    <!-- 局部搜索算法的配置 --> 
    <localSearch>
      <!-- 终止条件配置 --> 
        <termination>
          <bestScoreLimit>0</bestScoreLimit>
        </termination>
      <!-- moveSelector配置 -->
        <unionMoveSelector>
          <changeMoveSelector/>
          <swapMoveSelector/>
          <pillarChangeMoveSelector/>
          <pillarSwapMoveSelector/>
        </unionMoveSelector>
      <!-- 禁忌搜索配置 -->
        <acceptor>
          <entityTabuSize>7</entityTabuSize>
        </acceptor>
      </localSearch>
    <!-- 自定义算法配置 -->
    <customPhase>-->
        <customPhaseCommandClass>      org.optaplanner.core.impl.phase.custom.NoChangeCustomPhaseCommand
    		</customPhaseCommandClass>
    </customPhase>

1.2.3 move类

move是从$solution_A$到$solution_B$的变化。例如,下面的移动将皇后c从第0行改到第2行

image-20211201171735465

  • move

这里仅介绍基本定义,optaplanner还定义了更多的move种类,比如swapmove

image-20211202114016079

更详细内容请参考官方文档:https://www.optaplanner.org/docs/optaplanner/latest/move-and-neighborhood-selection/move-and-neighborhood-selection.html#genericMoveSelectors

1.2.4 moveSelector类

MoveSelector 的主要功能是在需要的时候创建 Iterator<move> 。即邻域中的move,优化算法将循环遍历这些move所到达的solutions。

这个不是算法的必要部分,但是是一个重要且好用的轮子,可配置性强,optaplanner自带算法大部分都用到该轮子。

详细内容参考,https://www.optaplanner.org/docs/optaplanner/latest/move-and-neighborhood-selection/move-and-neighborhood-selection.html#genericMoveSelectors

文档详细介绍了move和moveSelector的配置方法,这里没必要过于详细描述。第三部分的示例中会使用

image-20211201173431203

1.2.5 phase类

问题求解阶段类,其抽象类定义多个生命周期接口

  • solve:算法主要逻辑
  • calculateWorkingStepScore 计算进行一次move得到的workingSolution的得分
  • solvingStarted:生命周期方法,solver求解开始之前调用
  • solvingEnded:生命周期方法,solver求解结束
  • phaseStarted:生命周期方法,
    • 一个算法求解开始时调用
    • 其中对一些类的实例进行初始化
  • phaseEnded:算法结束时调用
  • 其他

solver类中包含一个phaseListsolver.solve()中会逐个调用phase.solve().

1.2.6 scope类

  • solverScope
  • phaseScope
  • stepScope
  • moveScope
  1. solver会迭代运行多个phase(算法)
  2. 一个phase会进行多个step
  3. 进行一次step需要尝试多个move

这些形成了如下的嵌套关系,每个scope类中保留了每个步骤的信息,比如

  • moveScope中保存所进行的move实例
  • stepScope中会保存遍历邻域之后选择的move

⚠️我们替换算法不会用到这些类的全部功能,只会用到一部分,其中大部分是规定动作

image-20211201210521947

二、算法替换方法

算法替换主要流程

流程图1的副本

替换算法主要逻辑(phase.solve())编写流程

流程图2

2.1 配置文件的编写方式

2.1.1 书写规则

optaplanner-examples/src/main/resources/org/optaplanner/examples/

​ 各个示例的算法配置文件都在这个目录下,第一部分讲解solverConfig时说过,求解器(solver)可以由xml文件构建。我们以nqueen问题的optaplanner-examples/src/main/resources/org/optaplanner/examples/nqueens/nqueensSolverConfig.xml为例

​ 在代码倒数第二行加入,如下代码

<customPhase>
    <customPhaseCommandClass>
      org.optaplanner.core.impl.phase.custom.NoChangeCustomPhaseCommand
    </customPhaseCommandClass>
 </customPhase>

这是optaplanner给开发者预留的自定义算法接口

image-20211201215853690

2.1.2 算法参数配置方法

我们也可以像其他算法一样进行一定的参数配置,

<customPhase>
  <customPhaseCommandClass>...MyCustomPhase</customPhaseCommandClass>
  <customProperties>
    <property name="mySelectionSize" value="5"/>
  </customProperties>
</customPhase>

这些参数可以在工厂类optaplanner-core/src/main/java/org/optaplanner/core/impl/phase/custom/DefaultCustomPhaseFactory.java里使用

phaseConfig.getCustomProperties()

获取供算法求解或配置算法使用

2.2 算法替换模板

optaplanner给开发者预留了自定义算法接口,但是与我们的需求有一定差距。所以替换算法不能直接在原仓库进行代码增添,请克隆我们提供的仓库

git clone https://github.com/xinwuyun/optaplanner-alter.git

2.2.1 算法替换位置

optaplanner-core/src/main/java/org/optaplanner/core/impl/phase/custom

算法替换在此处进行,文件夹下包含

image-20211201223557867

我们主要的工作可以只在这个文件夹下进行

如果完全按照opta的设计模式,则也需要在/optaplanner-core/src/main/java/org/optaplanner/core/config文件夹下进行相应编写,这样完全可以,但认为过于繁琐。

2.2.2 算法类(DefaultCustomPhase)

算法主要逻辑放在DefaultCustomPhase.java的solve方法中即可,修改好配置文件后,optaplanner会自动调用

image-20211201224132394

2.2.3 工厂类说明

​ 工厂类即该目录下的DefaultCustomPhase.java,工厂类创建中的各个组件(属性)将他们组装成DefaultCustomPhase

​ 当我们想要在DefaultCustomPhase添加某个组件(属性)时,按照如下步骤进行

  1. 首先肯定定义该类(及其工厂类),如果这个类是已有的轮子则不用定义
  2. DefaultCustomPhase中引入并在类中添加该属性及其getter、setter方法
  3. DefaultCustomPhaseFactory中的buildPhase方法中创建这个类的实例,调用setter方法。

2.3 算法输入

整体的求解器的输入是问题定义,optaplanner会将问题定义转换为算法的输入。

算法主要逻辑在phase.solve(),传入的参数是solverScope

image-20211201225154104

solverScope中包含算法求解必须的所有资料。opta提供了诸多接口供算法使用。所以从更抽象的角度来理解输入如下:

  1. 当前solution,即当前解决方案,算法会对这个solution进行优化
  2. scoreDirector:用于计算分数

这里不理解请继续看下面两个小节

2.4 算法输出

算法输出是优化后的bestSolution和bestScore

算法实际上不会返回某个值,但是会在算法进行过程中对solverScope中的bestScore和bestSolution直接进行修改(前提是算法发现了更优解)

2.5 算法进行

​ 对于启发式算法,算法的主要求解流程

  1. 对邻域(当前solution下所有move)进行遍历
  2. 依据各个move得到的新的solution的得分(score)和算法规则,挑选出合适的move
  3. 对当前bestSolution进行该move得到新的bestSolution
  4. 不断执行该步骤直到满足算法的终止条件(比如达到局部最优解、10步之内没有得到更优解、得到了分数大于0的solution、求解时长超过2分钟等)

对于其中的每一步,opta都提供了相应的接口来实现

2.5.1 对邻域进行遍历

使用moveSelector,在第三部分中有应用的示例。

for(Move<Solution_> move : moveSelector){

}

2.5.2 依据各个move得到的新的solution的得分(score)和算法规则,挑选出合适的move

得分使用scoreDirector获取,scoreDirector通过下面的代码调用

InnerScoreDirector<Solution_, ?> scoreDirector = solverScope.getScoreDirector();

调用move.doMove(scoreDirector);可以在workingSolution上进行改变,并返回一个move的反动作,再调用scoreDirector.calculateScore()可以得到分数。

最后,调用undoMove.doMove(scoreDirector)可以将workingSolution退回到初始的状态。

注意,这里所谓的workingSolution在scoreDirector实例中维护,开发者不必关心

Move<Solution_> undoMove = move.doMove(scoreDirector);
Score score = scoreDirector.calculateScore();
undoMove.doMove(scoreDirector);

2.5.3 对当前bestSolution进行该move得到新的bestSolution

遍历过邻域后,算法能够得到一个move作为nextStep,同时获得它的分数。

进行如下调用,更新bestSolution和相关属性。

stepScope.setScore(maxScore);
doStep(stepScope, nextStep);
stepEnded(stepScope);
phaseScope.setLastCompletedStepScope(stepScope);

doStep的内容如下

protected void doStep(CustomStepScope<Solution_> stepScope, Move<Solution_> step) {
        Move<Solution_> undoStep = step.doMove(stepScope.getScoreDirector());
        predictWorkingStepScore(stepScope, step);
        solver.getBestSolutionRecaller().processWorkingSolutionDuringStep(stepScope);
    }

2.5.4 不断执行该步骤直到满足算法的终止条件

算法的终止将在下一小节讲解,接口如下。

while(!phaseTermination.isPhaseTerminated(phaseScope))

也就是说本文档会对启发式算法的替换用处更大,如果是实现其他算法,比如粒子群算法,本小节算法的进行可能会有一定的变化

2.6 算法终止(Termination)

2.6.1 代码中调用

​ 上面说了算法会不断进行直到*当前状态(运行时间、bestScore等)*满足算法的终止条件,optaplanner提供了相应接口,我们可以在配置文件中对termination条件进行配置,在算法代码中只需要调用如下代码即可得知算法是否达到终止条件

phaseTermination.isPhaseTerminated(phaseScope)

image-20211202094715511

2.6.2 配置文件配置方式

(1)可配置条件
  • 达到一定时间时停止
  • 一定时间内没有最优解更新
  • 达到需要的分数
  • 走过的step数超过一定值
    • 走过的step数表示2.5中步骤2、3的重复次数
  • 一定step数内没有最优解更新
    • 注意和一定时间内区分
  • 其他(不常用)
(2)配置编写规则和编写位置

在配置文件中有两种添加方式

  • 全局termination

    • ji

    • 写在相应算法中,

    • 注意,必须作为算法的第一个子标签

      ![image-20211202094512799](/Users/xinwuyun/Library/Application Support/typora-user-images/image-20211202094512799.png)

配置规则书写见官方文档,链接和目录如下

https://docs.optaplanner.org/8.11.1.Final/optaplanner-docs/html_single/index.html#termination

image-20211202091428417

2.7 可用的轮子

2.7.1 MoveSelector

MoveSelector前面已经讲解过,MoveSelector 的主要功能是在需要的时候创建 Iterator<move> 。优化算法将循环遍历这些步骤的子集。

moveSelector是一个在optaplanner提供的其他算法中经常用的工具

在第三部分的实例中,我们会实践在算法中使用他,这里不细说。

三、算法替换示例

3.1 克隆仓库

  • 该仓库基于仓库进行了一定修改
  • 主分支是等待进行算法替换的分支
  • finish分支是完成本文算法替换的分支
git clone https://github.com/xinwuyun/optaplanner-alter.git

接下来的文档基于主分支(main)进行,也可以使用如下命令切换到finish分支直接查看完成后的代码

git checkout finish

3.2 尝试运行示例

运行任一示例,以nqueen为例,在IDE中做好运行配置

如果是idea,设置workingDirectory为/Users/xinwuyun/Documents/code/optaplanner/optaplanner/optaplanner-examples

image-20211121144849877

运行optaplanner-examples/src/main/java/org/optaplanner/examples/nqueens/app/NQueensHelloWorld.java,看一下输出

最后能看到如下结果

image-20211122142901212

结尾展示了棋盘的排布,算法先后使用了构造启发式算法和禁忌搜索

3.3 要实现的算法

作为示例,这里实现最简单的贪心算法

  1. 探索初始状态的邻域K
  2. 在K中选择得分最高的状态;
  3. 若此状态得分大于当前,则选择此状态为下一个状态;否则算法停止;
  4. 重复2,3直至最优或算法停止

3.4 开始算法替换

3.4.1 开始之前

solver的配置保存在一个xml文件中。nqueensolver配置保存在org/optaplanner/examples/nqueens/nqueensSolverConfig.xml中。

这个路径在helloWorld.java指定image-20211121151040343

xml文件内容如下:

image-20211122143007756

通过配置该文件,可以定义运行示例时使用什么算法和算法次序。这里先后定义了两个算法:1. 构造启发式;2. 局部搜索算法。

两个算法的代码分别在

image-20211121170813870

示例运行过程中,optaplanner对于每个算法会首先构建对应的Factory类,再build每一个算法的实例。使用算法时,调用***Phase.solve


开始替换算法

3.4.2 修改配置文件

nqueensSolverConfig.xml中的倒数第二行下方添加如下标签,注释掉LocalSeach算法配置

<customPhase>
  <customPhaseCommandClass>
    org.optaplanner.core.impl.phase.custom.NoChangeCustomPhaseCommand
   </customPhaseCommandClass>
</customPhase>

接下来的工作主要围绕这里进行optaplanner-core/src/main/java/org/optaplanner/core/impl/phase/custom

image-20211121164349602

这里有最基础的代码。我们需要在上面增加我们需要的部分

3.4.3 创建moveSelector

  1. DefaultCustomPhase.java中定义一个moveSelector

在类中添加如下代码

import org.optaplanner.core.impl.heuristic.selector.move.MoveSelector;

protected MoveSelector<Solution_> moveSelector;

public MoveSelector<Solution_> getMoveSelector() {
  return moveSelector;
}

public void setMoveSelector(MoveSelector<Solution_> moveSelector){
  this.moveSelector = moveSelector;
}
  1. DefaultCustomPhaseFactory.java创建moveSelector

image-20211121170114619

添加如下方法

import org.optaplanner.core.config.heuristic.selector.common.SelectionCacheType;
import org.optaplanner.core.config.heuristic.selector.common.SelectionOrder;
import org.optaplanner.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig;
import org.optaplanner.core.impl.heuristic.selector.move.MoveSelector;
import org.optaplanner.core.impl.heuristic.selector.move.generic.ChangeMoveSelectorFactory;

protected MoveSelector<Solution_> buildMoveSelector(HeuristicConfigPolicy<Solution_> configPolicy) {
  			// 定义move的缓存类型 https://docs.optaplanner.org/8.9.1.Final/optaplanner-docs/html_single/index.html#generalSelectorFeatures
  			// JUST_IN_TIME表示不使用缓存
        SelectionCacheType defaultCacheType = SelectionCacheType.JUST_IN_TIME;
  			
  			// 定义实体和变量的选择次序
  			// https://docs.optaplanner.org/8.9.1.Final/optaplanner-docs/html_single/index.html#selectionOrder
  			// 按照默认次序(类似顺序)
        SelectionOrder defaultSelectionOrder = SelectionOrder.ORIGINAL;
        ChangeMoveSelectorConfig changeMoveSelectorConfig = new ChangeMoveSelectorConfig();
        MoveSelector<Solution_> moveSelector = new ChangeMoveSelectorFactory<Solution_>(changeMoveSelectorConfig)
                .buildMoveSelector(configPolicy, defaultCacheType, defaultSelectionOrder);
        return moveSelector;
    }
}

buildPhase方法中调用buildMoveSelctor得到moveSelector示例,将其交给phase

phase.setMoveSelector(buildMoveSelector(phaseConfigPolicy));

DefaultCustomPhase.java中的phaseStartedphaseEnded中分别添加

moveSelector.phaseStarted(phaseScope);

moveSelector.phaseEnded(phaseScope);
  1. solve中编写算法逻辑

image-20211202105635126

我们在中间注释处编写算法逻辑

3.4.4 判定算法终止

上面我们说过使用下面这段代码进行判断

while(!phaseTermination.isPhaseTerminated(phaseScope))

本算法要求:如果在邻域中没有发现更优解,则算法停止,转换为配置文件中的语言为

<termination>
  <bestScoreLimit>0</bestScoreLimit>
  <unimprovedStepCountLimit>1</unimprovedStepCountLimit>
</termination>

上面的配置表示,当bestScore$\ge$0时算法停止;当有1个step内没有发现更优解时算法停止(即邻域内没有发现更优解)

image-20211202111904958

// 括号中的内容即可检查上图中的Termination是否成立
while(!phaseTermination.isPhaseTerminated(phaseScope)){
  
  // **********************************
  // 遍历邻域,从中挑选一个move作为nextstep
  // **********************************
  
}

3.4.5 遍历邻域并计算每个状态的得分(主要逻辑)

先import

import org.optaplanner.core.api.score.Score;
import org.optaplanner.core.impl.heuristic.move.Move;
import org.optaplanner.core.impl.heuristic.selector.move.MoveSelector;
import java.util.ArrayList;
import java.util.Collections;

将下面代码添加到solve方法中间的注释处

// scoreDirector用于计算得分
InnerScoreDirector<Solution_, ?> scoreDirector = solverScope.getScoreDirector();
while(!phaseTermination.isPhaseTerminated(phaseScope)){
  //******
  // 固定操作
  //******
  CustomStepScope<Solution_> stepScope = new CustomStepScope<>(phaseScope);
  
  // 用数组保存领域中和相应得分
  List<Move<Solution_>> moves = new ArrayList<>();
  List<Score> scores = new ArrayList<>();
	// 固定
  stepStarted(stepScope);
  
  // 遍历邻域
  for(Move<Solution_> move : moveSelector){
    // 进行move,会对当前solution进行修改
    // 返回一个move的反动作,方便回滚
    Move<Solution_> undoMove = move.doMove(scoreDirector);
    // 计算改变的solution得分
    Score score = scoreDirector.calculateScore();
    // 保存得分和move
    moves.add(move);
    scores.add(score);
    // 回滚
    undoMove.doMove(scoreDirector);
  }			
  
  // 选择最高得分的move
  Score maxScore = Collections.max(scores);
  int i = scores.indexOf(maxScore);
  Move<Solution_> nextStep = moves.get(i);
  // 将最高分存放到stepScope中
  stepScope.setScore(maxScore);
  

  //******
  // 固定操作
  //******
  doStep(stepScope, nextStep);
  stepEnded(stepScope);
  phaseScope.setLastCompletedStepScope(stepScope);
}

3.4.6 编写doStep

doStep应在选定nextStep,并且nextStep可以接受之后调用

protected void doStep(CustomStepScope<Solution_> stepScope, Move<Solution_> step) {
        Move<Solution_> undoStep = step.doMove(stepScope.getScoreDirector());
        predictWorkingStepScore(stepScope, step);
        solver.getBestSolutionRecaller().processWorkingSolutionDuringStep(stepScope);
    }

3.5 运行

运行org.optaplanner.examples.nqueens.app.NQueensHelloWorld#main

注意此时nqueenSolverConfig.xml,注释掉<LocalSearch>

image-20211202111904958

输出如下

image-20211202111720838

根据输出可以看到,替换算法进行了两步后到达局部最优,算法停止。

image-20211202111742740

image-20211121215958319

这些是构造器启发式算法的输出。由于前面我们配置文件中的localSearch算法,所以示例没有调用localSearch

四、如何替换更复杂的算法

比如如果我们要实现禁忌搜索,以避免陷入局部最优解。则可以在算法进行过程中维护一个step数组储存最近的若干nextStep,遍历邻域过程中增加一个isAccepted(move/stepScope),筛选掉禁忌move

禁忌搜索optaplanner已经实现了,代码可供参考

封装

上面的描述比较面向过程,较好的方式是定义决定器(Decider),捕捉器Forager,接受器Acceptor。对于不同算法或者同一算法的不同变体,这些xx器有不同的配置实现

举例

比如,我们希望在我们替换的贪婪算法基础上实现禁忌搜索,则可以实现一个Acceptor,其中维护一个step队列,保存最近几个step,实现一个isAccepted方法。

customPhase中创建一个acceptor实例,遍历邻域时,对每个move调用accoptor.isAccepted(move)

while(!phaseTermination.isPhaseTerminated(phaseScope)){
  ...
	for(move : moveSelector){
    if(!accoptor.isAccepted(move)){
      continue;
    }
    ...
  }
  ...
}

最佳实践参考optapanner实现的LocalSearch算法(及其变体)的实现.

optaplanner-alter's People

Contributors

ge0ffrey avatar triceo avatar rsynek avatar yurloc avatar oskopek avatar mdproctor avatar christopher-chianelli avatar mbiarnes avatar wlaun avatar snurkabill avatar etirelli avatar radtriste avatar krisv avatar mcimbora avatar michaelneale avatar enzhao avatar rikkola avatar dupliaka avatar ginxo avatar marianmacik avatar bradh avatar lczmdr avatar psiroky avatar wang-zerui avatar emmurphy1 avatar mariofusco avatar irooskov avatar jervisliu avatar manstis avatar mramendi avatar

Stargazers

 avatar  avatar Lee.Lee avatar  avatar  avatar Tshine avatar  avatar  avatar 黑牛 avatar YKTian avatar

Watchers

 avatar

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.