├── DEAP.pdf ├── application ├── img │ ├── CV.png │ ├── DT.png │ ├── Fox.png │ ├── NN.png │ ├── Fox2.png │ ├── Parallel.png │ └── greywolfga.jpg ├── theoretical_analysis.ipynb ├── multiobjective-sr.ipynb ├── alpha_dominance_mogp.py ├── symbolic-regression.ipynb ├── automatically-design-de-operators.ipynb └── TSP.ipynb ├── tricks ├── img │ └── async_parallel_graph.png ├── multiprocess_speedup.py ├── multiprocess_speedup.md ├── numpy-speedup.ipynb ├── compiler-speedup.ipynb ├── numba-lexicase-selection.ipynb └── numpy_speedup_sr.py ├── README.md ├── .gitignore └── operator ├── lexicase-selection.ipynb ├── crossover.ipynb └── varor-varand.ipynb /DEAP.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hengzhe-zhang/DEAP-GP-Tutorial/HEAD/DEAP.pdf -------------------------------------------------------------------------------- /application/img/CV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hengzhe-zhang/DEAP-GP-Tutorial/HEAD/application/img/CV.png -------------------------------------------------------------------------------- /application/img/DT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hengzhe-zhang/DEAP-GP-Tutorial/HEAD/application/img/DT.png -------------------------------------------------------------------------------- /application/img/Fox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hengzhe-zhang/DEAP-GP-Tutorial/HEAD/application/img/Fox.png -------------------------------------------------------------------------------- /application/img/NN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hengzhe-zhang/DEAP-GP-Tutorial/HEAD/application/img/NN.png -------------------------------------------------------------------------------- /application/img/Fox2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hengzhe-zhang/DEAP-GP-Tutorial/HEAD/application/img/Fox2.png -------------------------------------------------------------------------------- /application/img/Parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hengzhe-zhang/DEAP-GP-Tutorial/HEAD/application/img/Parallel.png -------------------------------------------------------------------------------- /application/img/greywolfga.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hengzhe-zhang/DEAP-GP-Tutorial/HEAD/application/img/greywolfga.jpg -------------------------------------------------------------------------------- /tricks/img/async_parallel_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hengzhe-zhang/DEAP-GP-Tutorial/HEAD/tricks/img/async_parallel_graph.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于DEAP的遗传编程系列教程 2 | 3 | 本系列教程主要介绍如何基于DEAP实现一些流行的遗传编程概念,包括: 4 | 5 | * 单树GP 6 | * 多树GP 7 | * 多目标GP 8 | * 集成学习 9 | * 启发式算法生成 10 | 11 | 上述概念通过以下示例实现: 12 | 13 | 1. [基于单树GP的符号回归(Symbolic Regression)](application/symbolic-regression.ipynb) 14 | 2. [基于多树GP的特征工程(Feature Construction)](application/feature-construction.ipynb) 15 | 3. [基于多目标GP的符号回归 (Multi-Objective Symbolic Regression)](application/multiobjective-sr.ipynb) 16 | 4. [基于GP的集成学习(Ensemble Learning)](application/ensemble-learning.ipynb) 17 | 5. [基于GP的旅行商问题规则生成(TSP)](application/TSP.ipynb) 18 | 6. [为什么使用GP而不是神经网络?(Feature Construction)](application/cross-validation-score.ipynb) 19 | 6. [基于GP自动设计优化算法](application/automatically-design-de-operators.ipynb) 20 | 7. [基于不同算子集的多树GP](application/multisets_gp.ipynb) 21 | 22 | 同时,本教程包含了一些工程技巧: 23 | 24 | 1. [基于Numpy实现向量化加速](tricks/numpy-speedup.ipynb) 25 | 2. [基于PyTorch实现GPU加速](tricks/pytorch-speedup.ipynb) 26 | 3. [基于手动编写编译器实现加速](tricks/compiler-speedup.ipynb) 27 | 4. [基于Numba实现Lexicase Selection加速](tricks/numba-lexicase-selection.ipynb) 28 | 5. [基于多进程实现异步并行评估](tricks/multiprocess_speedup.md) 29 | 6. [基于sklearn接口的Numpy加速符号回归](tricks/numpy_speedup_sr.py) 30 | 31 | 此外,DEAP还有一些注意事项: 32 | 33 | 1. [VarAnd和VarOr算子的使用注意事项](operator/varor-varand.ipynb) 34 | 2. [Crossover算子的注意事项](operator/crossover.ipynb) 35 | 2. [Lexicase Selection算子的注意事项](operator/lexicase-selection.ipynb) 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | .idea 3 | __pycache__/ 4 | application/catboost_info 5 | application/kaggle.ipynb 6 | application/data 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | .venv 92 | venv/ 93 | ENV/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # IDE settings 109 | .vscode/ 110 | -------------------------------------------------------------------------------- /application/theoretical_analysis.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "### 北极狐算法是什么?\n", 7 | "在文章《基于遗传编程自动设计优化算法(自动发现北极狐优化算法)》中,我们利用遗传编程算法在五分钟内发现了一个新的优化算法,$X_{\\text{new}} = X + (F \\cdot X - F)$, 命名为北极狐算法。\n", 8 | "众所周知,在设计算法的时候,我们不仅仅希望取得良好的实验效果,还希望设计出的算法有理论保证。那么,我们是否可以证明北极狐算法一定收敛到全局最优呢?\n", 9 | "\n", 10 | "### 北极狐算法的收敛性\n", 11 | "**假设:**\n", 12 | "- $ \\mathcal{S} \\subset \\mathbb{R}^n $ 是一个有界搜索空间,其中包含全局最优解 $ X^* $。\n", 13 | "- $ F $ 是从某个分布中抽取的随机扰动向量,$ F $ 的取值范围是整个搜索空间 $\\mathcal{S}$。\n", 14 | "\n", 15 | "**证明:**\n", 16 | "定义 $ A_\\epsilon $ 为 $ X^* $ 的 $\\epsilon$-邻域,即 $ A_\\epsilon = \\{ X \\in \\mathcal{S} : \\|X - X^*\\| < \\epsilon \\} $。设 $ P_\\epsilon $ 为一次操作中 $X_{\\text{new}} = X + (F \\cdot X - F)$ 落入 $ A_\\epsilon $ 的概率。因为 $ F $ 能覆盖整个搜索空间,所以即便对于足够小的 $ \\epsilon $,依然可以得到$ P_\\epsilon > 0 $。\n", 17 | "\n", 18 | "在 $ N $ 次迭代中,$X_{\\text{new}}$ 至少一次落入 $ A_\\epsilon $ 的概率是 $ 1 - (1 - P_\\epsilon)^N $。使用极限,我们可以表示这个概率在 $ N $ 趋向无穷大时的行为:\n", 19 | "\n", 20 | "$$ \\lim_{N \\to \\infty} \\left[ 1 - (1 - P_\\epsilon)^N \\right] = 1 $$\n", 21 | "\n", 22 | "这个极限表达了随着迭代次数 $ N $ 的增加,$X_{\\text{new}}$ 至少一次落入 $ X^* $ 的 $\\epsilon$-邻域的概率趋近于 1。\n", 23 | "\n", 24 | "### 结论:\n", 25 | "现在,我们证明了北极狐算法 $X_{\\text{new}} = X + (F \\cdot X - F)$ 在无限次迭代的情况下,能够以概率 1 接近全局最优解 $X^*$。" 26 | ], 27 | "metadata": { 28 | "collapsed": false 29 | }, 30 | "id": "b6c87f3572f1a7aa" 31 | }, 32 | { 33 | "cell_type": "code", 34 | "outputs": [], 35 | "source": [], 36 | "metadata": { 37 | "collapsed": false 38 | }, 39 | "id": "e64e0268ee5ee8f7" 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 2 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython2", 58 | "version": "2.7.6" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 5 63 | } 64 | -------------------------------------------------------------------------------- /operator/lexicase-selection.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "## Lexicase Selection注意事项\n", 7 | "\n", 8 | "对于Lexicase Selection,适应度评估需要更改为返回多个误差组成的向量,而不是均方误差(MSE)。这样,Lexicase Selection才能独立考虑每个个体在每个测试样本上的表现,从而提高选择的多样性。" 9 | ], 10 | "metadata": { 11 | "collapsed": false 12 | }, 13 | "id": "ff6050dfa4dc1b6" 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 7, 18 | "source": [ 19 | "import numpy as np\n", 20 | "import math\n", 21 | "import operator\n", 22 | "\n", 23 | "from deap import base, creator, tools, gp\n", 24 | "\n", 25 | "\n", 26 | "# 符号回归\n", 27 | "def evalSymbReg(individual, pset):\n", 28 | " # 编译GP树为函数\n", 29 | " func = gp.compile(expr=individual, pset=pset)\n", 30 | " \n", 31 | " # 使用numpy创建一个向量\n", 32 | " x = np.linspace(-10, 10, 100) \n", 33 | " \n", 34 | " return tuple((func(x) - x**2)**2)\n", 35 | "\n", 36 | "\n", 37 | "# 创建个体和适应度函数,适应度数组大小与数据量相同\n", 38 | "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,) * 100) # 假设我们有20个数据点\n", 39 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)" 40 | ], 41 | "metadata": { 42 | "collapsed": false, 43 | "ExecuteTime": { 44 | "end_time": "2023-11-07T09:06:58.369619300Z", 45 | "start_time": "2023-11-07T09:06:58.365066500Z" 46 | } 47 | }, 48 | "id": "59cfefc0467c74ad", 49 | "outputs": [] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "source": [ 54 | "### 遗传算子\n", 55 | "选择算子需要改成Lexicase Selection,其他不需要改变。对于回归问题,需要使用AutomaticEpsilonLexicase。而对于分类问题,则使用Lexicase即可。" 56 | ], 57 | "metadata": { 58 | "collapsed": false 59 | }, 60 | "id": "956e01e17271daa6" 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 8, 65 | "source": [ 66 | "import random\n", 67 | "\n", 68 | "# 定义函数集合和终端集合\n", 69 | "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n", 70 | "pset.addPrimitive(operator.add, 2)\n", 71 | "pset.addPrimitive(operator.sub, 2)\n", 72 | "pset.addPrimitive(operator.mul, 2)\n", 73 | "pset.addPrimitive(operator.neg, 1)\n", 74 | "pset.addEphemeralConstant(\"rand101\", lambda: random.randint(-1, 1))\n", 75 | "pset.renameArguments(ARG0='x')\n", 76 | "\n", 77 | "# 定义遗传编程操作\n", 78 | "toolbox = base.Toolbox()\n", 79 | "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n", 80 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 81 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 82 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 83 | "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n", 84 | "toolbox.register(\"select\", tools.selAutomaticEpsilonLexicase)\n", 85 | "toolbox.register(\"mate\", gp.cxOnePoint)\n", 86 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)" 87 | ], 88 | "metadata": { 89 | "collapsed": false, 90 | "ExecuteTime": { 91 | "end_time": "2023-11-07T09:06:58.378447200Z", 92 | "start_time": "2023-11-07T09:06:58.370620700Z" 93 | } 94 | }, 95 | "id": "851794d4d36e3681", 96 | "outputs": [] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 9, 101 | "source": [ 102 | "import numpy\n", 103 | "from deap import algorithms\n", 104 | "\n", 105 | "# 定义统计指标\n", 106 | "stats_fit = tools.Statistics(lambda ind: ind.fitness.values)\n", 107 | "stats_size = tools.Statistics(len)\n", 108 | "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n", 109 | "mstats.register(\"avg\", numpy.mean)\n", 110 | "mstats.register(\"std\", numpy.std)\n", 111 | "mstats.register(\"min\", numpy.min)\n", 112 | "mstats.register(\"max\", numpy.max)\n", 113 | "\n", 114 | "# 使用默认算法\n", 115 | "population = toolbox.population(n=20)\n", 116 | "hof = tools.HallOfFame(1)\n", 117 | "pop, log = algorithms.eaSimple(population=population,\n", 118 | " toolbox=toolbox, cxpb=0.5, mutpb=0.2, ngen=20, stats=mstats, halloffame=hof, verbose=True)\n", 119 | "print(str(hof[0]))\n" 120 | ], 121 | "metadata": { 122 | "collapsed": false, 123 | "ExecuteTime": { 124 | "end_time": "2023-11-07T09:07:09.006767300Z", 125 | "start_time": "2023-11-07T09:06:58.377448600Z" 126 | } 127 | }, 128 | "id": "515b587d4f8876ea", 129 | "outputs": [] 130 | } 131 | ], 132 | "metadata": { 133 | "kernelspec": { 134 | "display_name": "Python 3", 135 | "language": "python", 136 | "name": "python3" 137 | }, 138 | "language_info": { 139 | "codemirror_mode": { 140 | "name": "ipython", 141 | "version": 2 142 | }, 143 | "file_extension": ".py", 144 | "mimetype": "text/x-python", 145 | "name": "python", 146 | "nbconvert_exporter": "python", 147 | "pygments_lexer": "ipython2", 148 | "version": "2.7.6" 149 | } 150 | }, 151 | "nbformat": 4, 152 | "nbformat_minor": 5 153 | } 154 | -------------------------------------------------------------------------------- /tricks/multiprocess_speedup.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import random 3 | import time 4 | 5 | import numpy 6 | import numpy as np 7 | from deap import base, creator, gp 8 | from deap import tools 9 | from deap.algorithms import varAnd, eaSimple 10 | from deap.tools import selBest 11 | 12 | # 使用numpy创建一个数据集 13 | x = np.linspace(-10, 10, 1000000) 14 | 15 | 16 | # 符号回归 17 | def evalSymbReg(ind): 18 | func = toolbox.compile(ind) 19 | # 评估生成的函数并计算MSE 20 | mse = np.mean((func(x) - (x + 1) ** 2) ** 2) 21 | return (mse,) 22 | 23 | 24 | # 创建个体和适应度函数 25 | creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) 26 | creator.create("Individual", gp.PrimitiveTree, fitness=creator.FitnessMin) 27 | 28 | # 定义函数和终端变量 29 | pset = gp.PrimitiveSet("MAIN", arity=1) 30 | pset.addPrimitive(np.add, 2) 31 | pset.addPrimitive(np.subtract, 2) 32 | pset.addPrimitive(np.multiply, 2) 33 | pset.addPrimitive(np.negative, 1) 34 | 35 | 36 | def random_int(): 37 | return random.randint(-1, 1) 38 | 39 | 40 | pset.addEphemeralConstant("rand101", random_int) 41 | pset.renameArguments(ARG0="x") 42 | 43 | # 定义遗传编程操作 44 | toolbox = base.Toolbox() 45 | toolbox.register("expr", gp.genHalfAndHalf, pset=pset, min_=2, max_=6) 46 | toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.expr) 47 | toolbox.register("population", tools.initRepeat, list, toolbox.individual) 48 | toolbox.register("compile", gp.compile, pset=pset) 49 | toolbox.register("evaluate", evalSymbReg) 50 | toolbox.register("select", tools.selTournament, tournsize=3) 51 | toolbox.register("mate", gp.cxOnePoint) 52 | toolbox.register("mutate", gp.mutUniform, expr=toolbox.expr, pset=pset) 53 | 54 | # 定义统计指标 55 | stats_fit = tools.Statistics(lambda ind: ind.fitness.values) 56 | stats_size = tools.Statistics(len) 57 | mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size) 58 | mstats.register("avg", numpy.mean) 59 | mstats.register("std", numpy.std) 60 | mstats.register("min", numpy.min) 61 | mstats.register("max", numpy.max) 62 | 63 | def steady_state_gp( 64 | population, 65 | toolbox, 66 | cxpb, 67 | mutpb, 68 | max_evaluations, 69 | stats=None, 70 | halloffame=None, 71 | verbose=__debug__, 72 | ): 73 | logbook = tools.Logbook() 74 | logbook.header = ["evals", "nevals"] + (stats.fields if stats else []) 75 | 76 | executor = concurrent.futures.ProcessPoolExecutor(max_workers=4) 77 | futures = {} 78 | evaluations =0 79 | 80 | # 评估初始种群 81 | for i, ind in enumerate(population): 82 | if not ind.fitness.valid: 83 | future = executor.submit(toolbox.evaluate, ind) 84 | futures[future] = ind 85 | 86 | all_done = population 87 | 88 | while evaluations < max_evaluations: 89 | # 生成新个体 90 | if evaluations + len(futures) <= max_evaluations and len(all_done) >= 2: 91 | selected = toolbox.select(population, len(all_done)) 92 | offspring = varAnd(selected, toolbox, cxpb, mutpb) 93 | all_done = [] 94 | 95 | # 提交评估任务 96 | for child in offspring: 97 | if evaluations + len(futures) <= max_evaluations: 98 | future = executor.submit(toolbox.evaluate, child) 99 | futures[future] = child 100 | else: 101 | break 102 | 103 | # 等待至少一个个体完成评估 104 | current_done, _ = concurrent.futures.wait( 105 | list(futures.keys()), 106 | return_when=concurrent.futures.FIRST_COMPLETED, 107 | ) 108 | 109 | # 处理评估完成的个体 110 | done_inds = [] 111 | for future in current_done: 112 | ind = futures.pop(future) 113 | ind.fitness.values = future.result() 114 | done_inds.append(ind) 115 | all_done.append(ind) 116 | evaluations += 1 117 | 118 | if halloffame is not None: 119 | halloffame.update(done_inds) 120 | 121 | # 用高适应度个体替换低适应度个体 122 | population = selBest(population + done_inds, len(population)) 123 | 124 | if verbose and evaluations % 100 == 0: 125 | record = stats.compile(population) if stats else {} 126 | logbook.record(evals=evaluations, **record) 127 | print(logbook.stream) 128 | 129 | executor.shutdown() 130 | return population, logbook 131 | 132 | 133 | if __name__ == "__main__": 134 | start = time.time() 135 | population = toolbox.population(n=100) 136 | hof = tools.HallOfFame(1) 137 | pop, log = steady_state_gp( 138 | population=population, 139 | toolbox=toolbox, 140 | cxpb=0.9, 141 | mutpb=0.1, 142 | max_evaluations=(5+1) * 100, 143 | stats=mstats, 144 | halloffame=hof, 145 | verbose=True, 146 | ) 147 | end = time.time() 148 | print("time:", end - start) 149 | 150 | start = time.time() 151 | population = toolbox.population(n=100) 152 | hof = tools.HallOfFame(1) 153 | pop, log = eaSimple( 154 | population=population, 155 | toolbox=toolbox, 156 | cxpb=0.9, 157 | mutpb=0.1, 158 | ngen=5, 159 | stats=mstats, 160 | halloffame=hof, 161 | verbose=True, 162 | ) 163 | end = time.time() 164 | print("time:", end - start) -------------------------------------------------------------------------------- /tricks/multiprocess_speedup.md: -------------------------------------------------------------------------------- 1 | ### 动机 2 | 遗传编程在并行评估的过程中,存在一个瓶颈即快的个体需要等待慢的个体评估完成才能进入下一代,导致整体CPU利用率不高。 3 | 4 | 实际上,在演化计算中,评估是可以异步执行的。 5 | 6 | 遗传编程通常使用的编程范式是Generation-based,即每一代的个体需要等待所有个体评估完成才能进入下一代。 7 | 8 | **但是,我们也可以使用Steady-state-based的编程范式,即每一个个体都是异步评估的,从而提高CPU利用率。** 9 | 10 | 这里,我将通过一个简单的例子,来展示出一下如何使用基于Python实现遗传编程的并行评估。 11 | 12 | 13 | ```python 14 | import concurrent.futures 15 | import random 16 | import time 17 | 18 | import numpy 19 | import numpy as np 20 | from deap import base, creator, gp 21 | from deap import tools 22 | from deap.algorithms import varAnd, eaSimple 23 | from deap.tools import selBest 24 | 25 | # 使用numpy创建一个数据集 26 | x = np.linspace(-10, 10, 1000000) 27 | 28 | 29 | # 符号回归 30 | def evalSymbReg(ind): 31 | func = toolbox.compile(ind) 32 | # 评估生成的函数并计算MSE 33 | mse = np.mean((func(x) - (x + 1) ** 2) ** 2) 34 | return (mse,) 35 | 36 | 37 | # 创建个体和适应度函数 38 | creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) 39 | creator.create("Individual", gp.PrimitiveTree, fitness=creator.FitnessMin) 40 | 41 | # 定义函数和终端变量 42 | pset = gp.PrimitiveSet("MAIN", arity=1) 43 | pset.addPrimitive(np.add, 2) 44 | pset.addPrimitive(np.subtract, 2) 45 | pset.addPrimitive(np.multiply, 2) 46 | pset.addPrimitive(np.negative, 1) 47 | 48 | 49 | def random_int(): 50 | return random.randint(-1, 1) 51 | 52 | 53 | pset.addEphemeralConstant("rand101", random_int) 54 | pset.renameArguments(ARG0="x") 55 | 56 | # 定义遗传编程操作 57 | toolbox = base.Toolbox() 58 | toolbox.register("expr", gp.genHalfAndHalf, pset=pset, min_=2, max_=6) 59 | toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.expr) 60 | toolbox.register("population", tools.initRepeat, list, toolbox.individual) 61 | toolbox.register("compile", gp.compile, pset=pset) 62 | toolbox.register("evaluate", evalSymbReg) 63 | toolbox.register("select", tools.selTournament, tournsize=3) 64 | toolbox.register("mate", gp.cxOnePoint) 65 | toolbox.register("mutate", gp.mutUniform, expr=toolbox.expr, pset=pset) 66 | 67 | # 定义统计指标 68 | stats_fit = tools.Statistics(lambda ind: ind.fitness.values) 69 | stats_size = tools.Statistics(len) 70 | mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size) 71 | mstats.register("avg", numpy.mean) 72 | mstats.register("std", numpy.std) 73 | mstats.register("min", numpy.min) 74 | mstats.register("max", numpy.max) 75 | ``` 76 | 77 | ### 异步并行 78 | 在本教程中,异步并行处理是通过ProcessPoolExecutor实现的。 79 | 简单来说,原理是创建一个进程池,然后将评估任务提交给进程池,进程池会自动分配任务给空闲的进程,如下图所示。 80 | 81 | ![异步评估](img/async_parallel_graph.png) 82 | 83 | 当任何一个进程完成评估任务时,我们可以获取其结果。如果新个体的适应度好于种群中的最差个体,我们可以将其加入种群,替换掉最差个体。 84 | 当至少两个任务完成时,我们可以开始下一代的演化。实际上,开始下一代演化的条件是一个可以调节的参数,这里设置的越小,CPU利用率越高。 85 | 86 | ```python 87 | def steady_state_gp( 88 | population, 89 | toolbox, 90 | cxpb, 91 | mutpb, 92 | max_evaluations, 93 | stats=None, 94 | halloffame=None, 95 | verbose=__debug__, 96 | ): 97 | logbook = tools.Logbook() 98 | logbook.header = ["evals", "nevals"] + (stats.fields if stats else []) 99 | 100 | executor = concurrent.futures.ProcessPoolExecutor(max_workers=4) 101 | futures = {} 102 | evaluations =0 103 | 104 | # 评估初始种群 105 | for i, ind in enumerate(population): 106 | if not ind.fitness.valid: 107 | future = executor.submit(toolbox.evaluate, ind) 108 | futures[future] = ind 109 | 110 | all_done = population 111 | 112 | while evaluations < max_evaluations: 113 | # 生成新个体 114 | if evaluations + len(futures) <= max_evaluations and len(all_done) >= 2: 115 | selected = toolbox.select(population, len(all_done)) 116 | offspring = varAnd(selected, toolbox, cxpb, mutpb) 117 | all_done = [] 118 | 119 | # 提交评估任务 120 | for child in offspring: 121 | if evaluations + len(futures) <= max_evaluations: 122 | future = executor.submit(toolbox.evaluate, child) 123 | futures[future] = child 124 | else: 125 | break 126 | 127 | # 等待至少一个个体完成评估 128 | current_done, _ = concurrent.futures.wait( 129 | list(futures.keys()), 130 | return_when=concurrent.futures.FIRST_COMPLETED, 131 | ) 132 | 133 | # 处理评估完成的个体 134 | done_inds = [] 135 | for future in current_done: 136 | ind = futures.pop(future) 137 | ind.fitness.values = future.result() 138 | done_inds.append(ind) 139 | all_done.append(ind) 140 | evaluations += 1 141 | 142 | if halloffame is not None: 143 | halloffame.update(done_inds) 144 | 145 | # 用高适应度个体替换低适应度个体 146 | population = selBest(population + done_inds, len(population)) 147 | 148 | if verbose and evaluations % 100 == 0: 149 | record = stats.compile(population) if stats else {} 150 | logbook.record(evals=evaluations, **record) 151 | print(logbook.stream) 152 | 153 | executor.shutdown() 154 | return population, logbook 155 | ``` 156 | 157 | ### 结果测试 158 | 最后,我们可以测试一下异步并行和传统的串行遗传编程算法的区别。 159 | 从下面的测试结果可以看出,异步并行的遗传编程算法的速度要快于传统的串行遗传编程算法。 160 | 异步并行的遗传编程算法只消耗了10秒,而传统的串行遗传编程算法消耗了16秒。 161 | 162 | ```python 163 | start = time.time() 164 | population = toolbox.population(n=100) 165 | hof = tools.HallOfFame(1) 166 | pop, log = steady_state_gp( 167 | population=population, 168 | toolbox=toolbox, 169 | cxpb=0.9, 170 | mutpb=0.1, 171 | max_evaluations=(5+1) * 100, 172 | stats=mstats, 173 | halloffame=hof, 174 | verbose=True, 175 | ) 176 | end = time.time() 177 | print("time:", end - start) 178 | 179 | start = time.time() 180 | population = toolbox.population(n=100) 181 | hof = tools.HallOfFame(1) 182 | pop, log = eaSimple( 183 | population=population, 184 | toolbox=toolbox, 185 | cxpb=0.9, 186 | mutpb=0.1, 187 | ngen=5, 188 | stats=mstats, 189 | halloffame=hof, 190 | verbose=True, 191 | ) 192 | end = time.time() 193 | print("time:", end - start) 194 | ``` -------------------------------------------------------------------------------- /operator/crossover.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "### Crossover算子\n", 7 | "值得一提的是,DEAP中GP默认实现的Crossover算子不考虑根节点。因此,如果要按照GP的原始论文实现,需要稍作修改。" 8 | ], 9 | "metadata": { 10 | "collapsed": false 11 | }, 12 | "id": "8db4ada5ce6ebf73" 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "source": [ 18 | "import time\n", 19 | "\n", 20 | "import numpy as np\n", 21 | "from deap import base, creator, tools, gp\n", 22 | "\n", 23 | "\n", 24 | "# 符号回归\n", 25 | "def evalSymbReg(individual, pset):\n", 26 | " # 编译GP树为函数\n", 27 | " func = gp.compile(expr=individual, pset=pset)\n", 28 | " \n", 29 | " # 使用numpy创建一个向量\n", 30 | " x = np.linspace(-10, 10, 100) \n", 31 | " \n", 32 | " # 评估生成的函数并计算MSE\n", 33 | " mse = np.mean((func(x) - x**2)**2)\n", 34 | " \n", 35 | " return (mse,)\n", 36 | "\n", 37 | "# 创建个体和适应度函数\n", 38 | "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,))\n", 39 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)\n", 40 | "\n" 41 | ], 42 | "metadata": { 43 | "collapsed": false, 44 | "ExecuteTime": { 45 | "end_time": "2023-11-07T08:49:09.672369400Z", 46 | "start_time": "2023-11-07T08:49:09.564823400Z" 47 | } 48 | }, 49 | "id": "initial_id", 50 | "outputs": [] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "source": [ 55 | "具体来说,需要修改交叉点的取值范围,以包括根节点。" 56 | ], 57 | "metadata": { 58 | "collapsed": false 59 | }, 60 | "id": "e3d94e424b58af5a" 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 2, 65 | "source": [ 66 | "\n", 67 | "from collections import defaultdict\n", 68 | "\n", 69 | "__type__ = object\n", 70 | "\n", 71 | "def cxOnePoint(ind1, ind2):\n", 72 | " # List all available primitive types in each individual\n", 73 | " types1 = defaultdict(list)\n", 74 | " types2 = defaultdict(list)\n", 75 | " if ind1.root.ret == __type__:\n", 76 | " # Not STGP optimization\n", 77 | " types1[__type__] = list(range(0, len(ind1)))\n", 78 | " types2[__type__] = list(range(0, len(ind2)))\n", 79 | " common_types = [__type__]\n", 80 | " else:\n", 81 | " for idx, node in enumerate(ind1[0:], 1):\n", 82 | " types1[node.ret].append(idx)\n", 83 | " for idx, node in enumerate(ind2[0:], 1):\n", 84 | " types2[node.ret].append(idx)\n", 85 | " common_types = set(types1.keys()).intersection(set(types2.keys()))\n", 86 | "\n", 87 | " if len(common_types) > 0:\n", 88 | " type_ = random.choice(list(common_types))\n", 89 | "\n", 90 | " index1 = random.choice(types1[type_])\n", 91 | " index2 = random.choice(types2[type_])\n", 92 | "\n", 93 | " slice1 = ind1.searchSubtree(index1)\n", 94 | " slice2 = ind2.searchSubtree(index2)\n", 95 | " ind1[slice1], ind2[slice2] = ind2[slice2], ind1[slice1]\n", 96 | "\n", 97 | " return ind1, ind2" 98 | ], 99 | "metadata": { 100 | "collapsed": false, 101 | "ExecuteTime": { 102 | "end_time": "2023-11-07T08:49:09.678933300Z", 103 | "start_time": "2023-11-07T08:49:09.675377100Z" 104 | } 105 | }, 106 | "id": "5dde655dc691a423", 107 | "outputs": [] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 3, 112 | "source": [ 113 | "import random\n", 114 | "\n", 115 | "# 定义函数集合和终端集合\n", 116 | "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n", 117 | "pset.addPrimitive(np.add, 2)\n", 118 | "pset.addPrimitive(np.subtract, 2)\n", 119 | "pset.addPrimitive(np.multiply, 2)\n", 120 | "pset.addPrimitive(np.negative, 1)\n", 121 | "pset.addEphemeralConstant(\"rand101\", lambda: random.randint(-1, 1))\n", 122 | "pset.renameArguments(ARG0='x')\n", 123 | "\n", 124 | "# 定义遗传编程操作\n", 125 | "toolbox = base.Toolbox()\n", 126 | "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n", 127 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 128 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 129 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 130 | "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n", 131 | "toolbox.register(\"select\", tools.selTournament, tournsize=3)\n", 132 | "toolbox.register(\"mate\", cxOnePoint)\n", 133 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)" 134 | ], 135 | "metadata": { 136 | "collapsed": false, 137 | "ExecuteTime": { 138 | "end_time": "2023-11-07T08:49:09.694753600Z", 139 | "start_time": "2023-11-07T08:49:09.680991300Z" 140 | } 141 | }, 142 | "id": "cb6cf38094256262", 143 | "outputs": [] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 4, 148 | "source": [ 149 | "import numpy\n", 150 | "from deap import algorithms\n", 151 | "\n", 152 | "# 定义统计指标\n", 153 | "stats_fit = tools.Statistics(lambda ind: ind.fitness.values)\n", 154 | "stats_size = tools.Statistics(len)\n", 155 | "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n", 156 | "mstats.register(\"avg\", numpy.mean)\n", 157 | "mstats.register(\"std\", numpy.std)\n", 158 | "mstats.register(\"min\", numpy.min)\n", 159 | "mstats.register(\"max\", numpy.max)\n", 160 | "\n", 161 | "# 使用默认算法\n", 162 | "start=time.time()\n", 163 | "population = toolbox.population(n=300)\n", 164 | "hof = tools.HallOfFame(1)\n", 165 | "pop, log = algorithms.eaSimple(population=population,\n", 166 | " toolbox=toolbox, cxpb=0.5, mutpb=0.2, ngen=50, stats=mstats, halloffame=hof, verbose=True)\n", 167 | "end=time.time()\n", 168 | "print('time:',end-start)\n", 169 | "print(str(hof[0]))" 170 | ], 171 | "metadata": { 172 | "collapsed": false, 173 | "ExecuteTime": { 174 | "end_time": "2023-11-07T08:49:12.030799500Z", 175 | "start_time": "2023-11-07T08:49:09.694753600Z" 176 | } 177 | }, 178 | "id": "88c62bc071d56191", 179 | "outputs": [] 180 | } 181 | ], 182 | "metadata": { 183 | "kernelspec": { 184 | "display_name": "Python 3", 185 | "language": "python", 186 | "name": "python3" 187 | }, 188 | "language_info": { 189 | "codemirror_mode": { 190 | "name": "ipython", 191 | "version": 2 192 | }, 193 | "file_extension": ".py", 194 | "mimetype": "text/x-python", 195 | "name": "python", 196 | "nbconvert_exporter": "python", 197 | "pygments_lexer": "ipython2", 198 | "version": "2.7.6" 199 | } 200 | }, 201 | "nbformat": 4, 202 | "nbformat_minor": 5 203 | } 204 | -------------------------------------------------------------------------------- /application/multiobjective-sr.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ff6050dfa4dc1b6", 6 | "metadata": {}, 7 | "source": [ 8 | "## 基于多目标GP的符号回归\n", 9 | "\n", 10 | "多目标GP是指使用多个目标函数来评估GP树的适应度。在符号回归问题中,通常使用均方误差(MSE)作为目标函数。然而,MSE并不能很好地反映模型的复杂度,因此,我们还可以使用树的大小作为目标函数。这样,就可以得到更为精简的模型。" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 5, 16 | "id": "59cfefc0467c74ad", 17 | "metadata": { 18 | "ExecuteTime": { 19 | "end_time": "2023-11-10T08:50:31.317854700Z", 20 | "start_time": "2023-11-10T08:50:31.272249300Z" 21 | } 22 | }, 23 | "source": [ 24 | "import math\n", 25 | "import operator\n", 26 | "import random\n", 27 | "from deap import base, creator, tools, gp, algorithms\n", 28 | "\n", 29 | "# 定义评估函数,包含两个目标:均方误差和树的大小\n", 30 | "def evalSymbReg(individual,pset):\n", 31 | " # 编译GP树为函数\n", 32 | " func = gp.compile(expr=individual, pset=pset)\n", 33 | " # 计算均方误差(Mean Square Error,MSE)\n", 34 | " mse = ((func(x) - x**2)**2 for x in range(-10, 10))\n", 35 | " # 计算GP树的大小\n", 36 | " size = len(individual)\n", 37 | " return math.fsum(mse), size\n", 38 | "\n", 39 | "# 修改适应度函数,包含两个权重:MSE和树的大小。MSE是最小化,树的大小也是最小化\n", 40 | "creator.create(\"FitnessMulti\", base.Fitness, weights=(-1.0, -1.0))\n", 41 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMulti)" 42 | ], 43 | "outputs": [] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "id": "956e01e17271daa6", 48 | "metadata": {}, 49 | "source": [ 50 | "### 遗传算子\n", 51 | "遗传算子基本不需要修改。由于是多目标优化问题,所以选择算子需要使用NSGA2(Non-dominated Sorting Genetic Algorithm II)。\n", 52 | "NSGA2算法的基本思想是,首先对种群中的个体进行非支配排序,然后根据非支配排序的结果计算拥挤度距离,最后根据非支配排序和拥挤度距离两个指标对个体进行排序。" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 6, 58 | "id": "851794d4d36e3681", 59 | "metadata": { 60 | "ExecuteTime": { 61 | "end_time": "2023-11-10T08:50:31.317854700Z", 62 | "start_time": "2023-11-10T08:50:31.278882Z" 63 | } 64 | }, 65 | "source": [ 66 | "import random\n", 67 | "\n", 68 | "# 定义函数集合和终端集合\n", 69 | "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n", 70 | "pset.addPrimitive(operator.add, 2)\n", 71 | "pset.addPrimitive(operator.sub, 2)\n", 72 | "pset.addPrimitive(operator.mul, 2)\n", 73 | "pset.addPrimitive(operator.neg, 1)\n", 74 | "pset.addEphemeralConstant(\"rand101\", lambda: random.randint(-1, 1))\n", 75 | "pset.renameArguments(ARG0='x')\n", 76 | "\n", 77 | "# 定义遗传编程操作\n", 78 | "toolbox = base.Toolbox()\n", 79 | "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n", 80 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 81 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 82 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 83 | "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n", 84 | "toolbox.register(\"select\", tools.selNSGA2)\n", 85 | "toolbox.register(\"mate\", gp.cxOnePoint)\n", 86 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)" 87 | ], 88 | "outputs": [] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "id": "62f30d17704db709", 93 | "metadata": {}, 94 | "source": [ 95 | "### 算法模块\n", 96 | "DEAP算法包提供了eaMuPlusLambda函数,可以比较方便地使用NSGA2的环境选择算子。 \n", 97 | "理想情况下,最好还是自行实现演化函数,这样才能完整地使用NSGA-II算法中的锦标赛选择算子。 \n", 98 | "NSGA-II算法中的锦标赛选择算子是指,首先从种群中随机选择两个个体,然后根据非支配排序和拥挤度距离两个指标对两个个体进行排序,最后选择排名较高的个体作为父代。简单起见,我们忽略了锦标赛选择算子。" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": 7, 104 | "id": "515b587d4f8876ea", 105 | "metadata": { 106 | "ExecuteTime": { 107 | "end_time": "2023-11-10T08:50:31.364942900Z", 108 | "start_time": "2023-11-10T08:50:31.284352200Z" 109 | } 110 | }, 111 | "source": [ 112 | "import numpy\n", 113 | "from deap import algorithms\n", 114 | "\n", 115 | "# 统计指标\n", 116 | "stats_fit = tools.Statistics(lambda ind: ind.fitness.values[0])\n", 117 | "stats_size = tools.Statistics(lambda ind: ind.fitness.values[1])\n", 118 | "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n", 119 | "mstats.register(\"avg\", numpy.mean)\n", 120 | "mstats.register(\"std\", numpy.std)\n", 121 | "mstats.register(\"min\", numpy.min)\n", 122 | "mstats.register(\"max\", numpy.max)\n", 123 | "\n", 124 | "population = toolbox.population(n=50)\n", 125 | "pop, log = algorithms.eaMuPlusLambda(population=population,\n", 126 | " toolbox=toolbox, mu=len(population),lambda_=len(population),\n", 127 | " cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=None, verbose=True)\n", 128 | "\n", 129 | "# 最佳个体\n", 130 | "best_ind = tools.selBest(pop, 1)[0]\n", 131 | "print('Best individual is:\\n', best_ind)\n", 132 | "print('\\nWith fitness:', best_ind.fitness.values)\n" 133 | ], 134 | "outputs": [] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "id": "7aa57e0f8b6151ad", 139 | "metadata": {}, 140 | "source": [ 141 | "基于优化结果,我们还可以绘制Pareto前沿,以便于选择最终的模型。" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 8, 147 | "id": "28284e0a0047fcfc", 148 | "metadata": { 149 | "ExecuteTime": { 150 | "end_time": "2023-11-10T08:50:31.483100600Z", 151 | "start_time": "2023-11-10T08:50:31.314335800Z" 152 | } 153 | }, 154 | "source": [ 155 | "from matplotlib import pyplot as plt\n", 156 | "import seaborn as sns\n", 157 | "\n", 158 | "# 非支配排序\n", 159 | "fronts = tools.sortNondominated(pop, len(pop), first_front_only=True)\n", 160 | "\n", 161 | "# Pareto前沿\n", 162 | "pareto_front = fronts[0]\n", 163 | "fitnesses = [ind.fitness.values for ind in pareto_front]\n", 164 | "\n", 165 | "# 分离均方误差和树的大小\n", 166 | "mse = [fit[0] for fit in fitnesses]\n", 167 | "sizes = [fit[1] for fit in fitnesses]\n", 168 | "\n", 169 | "# 使用seaborn绘制散点图\n", 170 | "sns.set(style=\"whitegrid\")\n", 171 | "plt.figure(figsize=(10, 6))\n", 172 | "sns.scatterplot(x=mse, y=sizes, palette=\"viridis\", s=60, edgecolor=\"w\", alpha=0.7)\n", 173 | "plt.xlabel('Mean Square Error')\n", 174 | "plt.ylabel('Size of the GP Tree')\n", 175 | "plt.title('Pareto Front')\n", 176 | "plt.show()" 177 | ], 178 | "outputs": [] 179 | } 180 | ], 181 | "metadata": { 182 | "kernelspec": { 183 | "display_name": "Python 3 (ipykernel)", 184 | "language": "python", 185 | "name": "python3" 186 | }, 187 | "language_info": { 188 | "codemirror_mode": { 189 | "name": "ipython", 190 | "version": 3 191 | }, 192 | "file_extension": ".py", 193 | "mimetype": "text/x-python", 194 | "name": "python", 195 | "nbconvert_exporter": "python", 196 | "pygments_lexer": "ipython3", 197 | "version": "3.11.4" 198 | } 199 | }, 200 | "nbformat": 4, 201 | "nbformat_minor": 5 202 | } 203 | -------------------------------------------------------------------------------- /operator/varor-varand.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "### VarOr/VarAnd\n", 7 | "VarOr和VarAnd是演化算法中的两种范式。VarOr表示交叉和变异必须选择其中一种执行。VarAnd则相对自由,可以同时执行交叉和变异,也可以同时不执行它们。GP的原始论文使用的是VarOr。" 8 | ], 9 | "metadata": { 10 | "collapsed": false 11 | }, 12 | "id": "8db4ada5ce6ebf73" 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "source": [ 18 | "import time\n", 19 | "\n", 20 | "import numpy as np\n", 21 | "from deap import base, creator, tools, gp\n", 22 | "\n", 23 | "\n", 24 | "# 符号回归\n", 25 | "def evalSymbReg(individual, pset):\n", 26 | " # 编译GP树为函数\n", 27 | " func = gp.compile(expr=individual, pset=pset)\n", 28 | "\n", 29 | " # 使用numpy创建一个向量\n", 30 | " x = np.linspace(-10, 10, 100)\n", 31 | "\n", 32 | " # 评估生成的函数并计算MSE\n", 33 | " mse = np.mean((func(x) - x ** 2) ** 2)\n", 34 | "\n", 35 | " return (mse,)\n", 36 | "\n", 37 | "\n", 38 | "# 创建个体和适应度函数\n", 39 | "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,))\n", 40 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)" 41 | ], 42 | "metadata": { 43 | "collapsed": false, 44 | "ExecuteTime": { 45 | "end_time": "2023-11-07T08:45:34.512379200Z", 46 | "start_time": "2023-11-07T08:45:34.394805500Z" 47 | } 48 | }, 49 | "id": "initial_id", 50 | "outputs": [] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 2, 55 | "source": [ 56 | "import random\n", 57 | "\n", 58 | "# 定义函数集合和终端集合\n", 59 | "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n", 60 | "pset.addPrimitive(np.add, 2)\n", 61 | "pset.addPrimitive(np.subtract, 2)\n", 62 | "pset.addPrimitive(np.multiply, 2)\n", 63 | "pset.addPrimitive(np.negative, 1)\n", 64 | "pset.addEphemeralConstant(\"rand101\", lambda: random.randint(-1, 1))\n", 65 | "pset.renameArguments(ARG0='x')\n", 66 | "\n", 67 | "# 定义遗传编程操作\n", 68 | "toolbox = base.Toolbox()\n", 69 | "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n", 70 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 71 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 72 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 73 | "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n", 74 | "toolbox.register(\"select\", tools.selTournament, tournsize=3)\n", 75 | "toolbox.register(\"mate\", gp.cxOnePoint)\n", 76 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)" 77 | ], 78 | "metadata": { 79 | "collapsed": false, 80 | "ExecuteTime": { 81 | "end_time": "2023-11-07T08:45:34.526223500Z", 82 | "start_time": "2023-11-07T08:45:34.514378Z" 83 | } 84 | }, 85 | "id": "cb6cf38094256262", 86 | "outputs": [] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "source": [ 91 | "DEAP默认使用VarAnd范式,如果我们想要实现VarOr,就需要自己修改eaSimple函数。当然,具体选择VarAnd还是VarOr要根据具体问题而定。目前尚无统一的结论表明哪种方式一定更好,需要根据问题的特性来决定。" 92 | ], 93 | "metadata": { 94 | "collapsed": false 95 | }, 96 | "id": "e09fa8e7890d583b" 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 3, 101 | "source": [ 102 | "from deap.algorithms import varOr\n", 103 | "import numpy\n", 104 | "from deap import algorithms\n", 105 | "\n", 106 | "\n", 107 | "def eaSimple(population, toolbox, cxpb, mutpb, ngen, stats=None,\n", 108 | " halloffame=None, verbose=__debug__):\n", 109 | " logbook = tools.Logbook()\n", 110 | " logbook.header = ['gen', 'nevals'] + (stats.fields if stats else [])\n", 111 | "\n", 112 | " # Evaluate the individuals with an invalid fitness\n", 113 | " invalid_ind = [ind for ind in population if not ind.fitness.valid]\n", 114 | " fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)\n", 115 | " for ind, fit in zip(invalid_ind, fitnesses):\n", 116 | " ind.fitness.values = fit\n", 117 | "\n", 118 | " if halloffame is not None:\n", 119 | " halloffame.update(population)\n", 120 | "\n", 121 | " record = stats.compile(population) if stats else {}\n", 122 | " logbook.record(gen=0, nevals=len(invalid_ind), **record)\n", 123 | " if verbose:\n", 124 | " print(logbook.stream)\n", 125 | "\n", 126 | " # Begin the generational process\n", 127 | " for gen in range(1, ngen + 1):\n", 128 | " # Select the next generation individuals\n", 129 | " offspring = toolbox.select(population, len(population))\n", 130 | "\n", 131 | " # Vary the pool of individuals\n", 132 | " offspring = varOr(offspring, toolbox, len(offspring),cxpb, mutpb)\n", 133 | "\n", 134 | " # Evaluate the individuals with an invalid fitness\n", 135 | " invalid_ind = [ind for ind in offspring if not ind.fitness.valid]\n", 136 | " fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)\n", 137 | " for ind, fit in zip(invalid_ind, fitnesses):\n", 138 | " ind.fitness.values = fit\n", 139 | "\n", 140 | " # Update the hall of fame with the generated individuals\n", 141 | " if halloffame is not None:\n", 142 | " halloffame.update(offspring)\n", 143 | "\n", 144 | " # Replace the current population by the offspring\n", 145 | " population[:] = offspring\n", 146 | "\n", 147 | " # Append the current generation statistics to the logbook\n", 148 | " record = stats.compile(population) if stats else {}\n", 149 | " logbook.record(gen=gen, nevals=len(invalid_ind), **record)\n", 150 | " if verbose:\n", 151 | " print(logbook.stream)\n", 152 | "\n", 153 | " return population, logbook\n", 154 | "\n", 155 | "\n", 156 | "# 定义统计指标\n", 157 | "stats_fit = tools.Statistics(lambda ind: ind.fitness.values)\n", 158 | "stats_size = tools.Statistics(len)\n", 159 | "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n", 160 | "mstats.register(\"avg\", numpy.mean)\n", 161 | "mstats.register(\"std\", numpy.std)\n", 162 | "mstats.register(\"min\", numpy.min)\n", 163 | "mstats.register(\"max\", numpy.max)\n", 164 | "\n", 165 | "# 使用默认算法\n", 166 | "start = time.time()\n", 167 | "population = toolbox.population(n=300)\n", 168 | "hof = tools.HallOfFame(1)\n", 169 | "pop, log = algorithms.eaSimple(population=population,\n", 170 | " toolbox=toolbox, cxpb=0.5, mutpb=0.2, ngen=50, stats=mstats, halloffame=hof,\n", 171 | " verbose=True)\n", 172 | "end = time.time()\n", 173 | "print('time:', end - start)\n", 174 | "print(str(hof[0]))" 175 | ], 176 | "metadata": { 177 | "collapsed": false, 178 | "ExecuteTime": { 179 | "end_time": "2023-11-07T08:45:35.379380400Z", 180 | "start_time": "2023-11-07T08:45:34.528223800Z" 181 | } 182 | }, 183 | "id": "88c62bc071d56191", 184 | "outputs": [] 185 | } 186 | ], 187 | "metadata": { 188 | "kernelspec": { 189 | "display_name": "Python 3", 190 | "language": "python", 191 | "name": "python3" 192 | }, 193 | "language_info": { 194 | "codemirror_mode": { 195 | "name": "ipython", 196 | "version": 2 197 | }, 198 | "file_extension": ".py", 199 | "mimetype": "text/x-python", 200 | "name": "python", 201 | "nbconvert_exporter": "python", 202 | "pygments_lexer": "ipython2", 203 | "version": "2.7.6" 204 | } 205 | }, 206 | "nbformat": 4, 207 | "nbformat_minor": 5 208 | } 209 | -------------------------------------------------------------------------------- /tricks/numpy-speedup.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "8db4ada5ce6ebf73", 6 | "metadata": {}, 7 | "source": [ 8 | "### Numpy 加速\n", 9 | "Python 是一种相对较慢的编程语言,但是我们可以通过使用Numpy来加速Python的运算。Numpy是一个基于C语言的库,提供了许多高效的运算函数,例如矩阵运算和线性代数运算等。这些运算都基于C语言实现,因此速度非常快。\n", 10 | "\n", 11 | "GP的性能瓶颈通常在于模型评估。因此,在这里,我们重点关注如何加速评估函数。其实很简单,将数据集转换为Numpy数组,然后使用Numpy函数来计算MSE即可。下面是一个例子。" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "id": "initial_id", 18 | "metadata": { 19 | "ExecuteTime": { 20 | "end_time": "2023-11-14T09:14:24.923043100Z", 21 | "start_time": "2023-11-14T09:14:24.908046400Z" 22 | } 23 | }, 24 | "source": [ 25 | "import time\n", 26 | "\n", 27 | "import numpy as np\n", 28 | "from deap import base, creator, tools, gp\n", 29 | "\n", 30 | "\n", 31 | "# 符号回归\n", 32 | "def evalSymbReg(individual, pset):\n", 33 | " # 编译GP树为函数\n", 34 | " func = gp.compile(expr=individual, pset=pset)\n", 35 | " \n", 36 | " # 使用numpy创建一个向量\n", 37 | " x = np.linspace(-10, 10, 100) \n", 38 | " \n", 39 | " # 评估生成的函数并计算MSE\n", 40 | " mse = np.mean((func(x) - x**2)**2)\n", 41 | " \n", 42 | " return (mse,)\n", 43 | "\n", 44 | "# 创建个体和适应度函数\n", 45 | "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,))\n", 46 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)\n" 47 | ], 48 | "outputs": [] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "id": "e3d94e424b58af5a", 53 | "metadata": {}, 54 | "source": [ 55 | "同时,我们还可以考虑将一些算子替换为Numpy函数。尽管这并不是非常重要,因为Numpy已经重载了许多运算符。" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 2, 61 | "id": "cb6cf38094256262", 62 | "metadata": { 63 | "ExecuteTime": { 64 | "end_time": "2023-11-14T09:14:24.933166800Z", 65 | "start_time": "2023-11-14T09:14:24.927568400Z" 66 | } 67 | }, 68 | "source": [ 69 | "import random\n", 70 | "\n", 71 | "# 定义函数集合和终端集合\n", 72 | "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n", 73 | "pset.addPrimitive(np.add, 2)\n", 74 | "pset.addPrimitive(np.subtract, 2)\n", 75 | "pset.addPrimitive(np.multiply, 2)\n", 76 | "pset.addPrimitive(np.negative, 1)\n", 77 | "def random_int(): return random.randint(-1, 1)\n", 78 | "pset.addEphemeralConstant(\"rand101\", random_int)\n", 79 | "pset.renameArguments(ARG0='x')\n", 80 | "\n", 81 | "# 定义遗传编程操作\n", 82 | "toolbox = base.Toolbox()\n", 83 | "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n", 84 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 85 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 86 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 87 | "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n", 88 | "toolbox.register(\"select\", tools.selTournament, tournsize=3)\n", 89 | "toolbox.register(\"mate\", gp.cxOnePoint)\n", 90 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)" 91 | ], 92 | "outputs": [] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "id": "e09fa8e7890d583b", 97 | "metadata": {}, 98 | "source": [ 99 | "现在,让我们来测试一下加速效果。" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 3, 105 | "id": "88c62bc071d56191", 106 | "metadata": { 107 | "ExecuteTime": { 108 | "end_time": "2023-11-14T09:14:25.525098600Z", 109 | "start_time": "2023-11-14T09:14:24.935256200Z" 110 | } 111 | }, 112 | "source": [ 113 | "import numpy\n", 114 | "from deap import algorithms\n", 115 | "\n", 116 | "# 定义统计指标\n", 117 | "stats_fit = tools.Statistics(lambda ind: ind.fitness.values)\n", 118 | "stats_size = tools.Statistics(len)\n", 119 | "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n", 120 | "mstats.register(\"avg\", numpy.mean)\n", 121 | "mstats.register(\"std\", numpy.std)\n", 122 | "mstats.register(\"min\", numpy.min)\n", 123 | "mstats.register(\"max\", numpy.max)\n", 124 | "\n", 125 | "# 使用默认算法\n", 126 | "np_time=[]\n", 127 | "for i in range(3):\n", 128 | " start=time.time()\n", 129 | " population = toolbox.population(n=300)\n", 130 | " hof = tools.HallOfFame(1)\n", 131 | " pop, log = algorithms.eaSimple(population=population,\n", 132 | " toolbox=toolbox, cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=hof, verbose=True)\n", 133 | " end=time.time()\n", 134 | " print('time:',end-start)\n", 135 | " np_time.append(end-start)\n", 136 | " print(str(hof[0]))" 137 | ], 138 | "outputs": [] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "id": "be250c9740bc2817", 143 | "metadata": {}, 144 | "source": [ 145 | "对比下面的原始评估函数,使用Numpy的加速效果还是非常明显的。" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 4, 151 | "id": "f2ddb57d24051753", 152 | "metadata": { 153 | "ExecuteTime": { 154 | "end_time": "2023-11-14T09:14:27.572677800Z", 155 | "start_time": "2023-11-14T09:14:25.521044600Z" 156 | } 157 | }, 158 | "source": [ 159 | "# 慢速评估\n", 160 | "def evalSymbRegSlow(individual, pset):\n", 161 | " # 编译GP树为函数\n", 162 | " func = gp.compile(expr=individual, pset=pset)\n", 163 | " \n", 164 | " # 创建评估数据\n", 165 | " xs = [x/5.0 for x in range(-50, 51)]\n", 166 | " \n", 167 | " # 评估生成的函数并计算MSE\n", 168 | " mse = sum((func(x) - x**2)**2 for x in xs) / len(xs)\n", 169 | " \n", 170 | " return (mse,)\n", 171 | "\n", 172 | "toolbox.register(\"evaluate\", evalSymbRegSlow, pset=pset)\n", 173 | "\n", 174 | "py_time=[]\n", 175 | "for i in range(3):\n", 176 | " start=time.time()\n", 177 | " population = toolbox.population(n=300)\n", 178 | " hof = tools.HallOfFame(1)\n", 179 | " pop, log = algorithms.eaSimple(population=population,\n", 180 | " toolbox=toolbox, cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=hof, verbose=True)\n", 181 | " end=time.time()\n", 182 | " print('time:',end-start)\n", 183 | " py_time.append(end-start)" 184 | ], 185 | "outputs": [] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "id": "75ed499f209894ae", 190 | "metadata": {}, 191 | "source": [ 192 | "最后,我们可以使用seaborn绘制一个图来比较Numpy和Python的性能。可以看出,Numpy显著提高了速度。" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 7, 198 | "id": "f09f85635ed36092", 199 | "metadata": { 200 | "ExecuteTime": { 201 | "end_time": "2023-11-14T09:24:29.905469100Z", 202 | "start_time": "2023-11-14T09:24:29.810538800Z" 203 | } 204 | }, 205 | "source": [ 206 | "import seaborn as sns\n", 207 | "import matplotlib.pyplot as plt\n", 208 | "import pandas as pd\n", 209 | "data = pd.DataFrame({'Category': ['Numpy'] * len(np_time) + ['Python'] * len(py_time),\n", 210 | " 'Time': np.concatenate([np_time, py_time])})\n", 211 | "\n", 212 | "\n", 213 | "plt.figure(figsize=(4, 3))\n", 214 | "sns.set_style(\"whitegrid\")\n", 215 | "sns.boxplot(data=data, x='Category', y='Time',palette=\"Set3\", width=0.4)\n", 216 | "plt.title('Comparison of Numpy and Python')\n", 217 | "plt.xlabel('')\n", 218 | "plt.ylabel('Time')\n", 219 | "plt.show()" 220 | ], 221 | "outputs": [] 222 | } 223 | ], 224 | "metadata": { 225 | "kernelspec": { 226 | "display_name": "Python 3 (ipykernel)", 227 | "language": "python", 228 | "name": "python3" 229 | }, 230 | "language_info": { 231 | "codemirror_mode": { 232 | "name": "ipython", 233 | "version": 3 234 | }, 235 | "file_extension": ".py", 236 | "mimetype": "text/x-python", 237 | "name": "python", 238 | "nbconvert_exporter": "python", 239 | "pygments_lexer": "ipython3", 240 | "version": "3.11.4" 241 | } 242 | }, 243 | "nbformat": 4, 244 | "nbformat_minor": 5 245 | } 246 | -------------------------------------------------------------------------------- /application/alpha_dominance_mogp.py: -------------------------------------------------------------------------------- 1 | import math 2 | import operator 3 | import random 4 | 5 | import numpy as np 6 | from deap import base, creator, tools, gp, algorithms 7 | from deap.tools import sortNondominated, selNSGA2 8 | 9 | 10 | # 定义评估函数,包含两个目标:均方误差和树的大小 11 | def evalSymbReg(individual, pset): 12 | # 编译GP树为函数 13 | func = gp.compile(expr=individual, pset=pset) 14 | # 计算均方误差(Mean Square Error,MSE) 15 | mse = ((func(x) - (x**2 + x)) ** 2 for x in range(-10, 10)) 16 | # 计算GP树的大小 17 | size = len(individual) 18 | return math.fsum(mse), size 19 | 20 | 21 | # 定义Alpha支配 22 | class AlphaDominance: 23 | def __init__(self, algorithm=None, initial_alpha=0.1, step_size=0.1): 24 | self.historical_largest = 0 25 | self.historical_smallest = math.inf 26 | self.algorithm = algorithm 27 | self.step_size = step_size 28 | self.initial_alpha = initial_alpha 29 | 30 | def update_best(self, population): 31 | self.historical_smallest = min( 32 | self.historical_smallest, min([len(p) for p in population]) 33 | ) 34 | self.historical_largest = max( 35 | self.historical_largest, max([len(p) for p in population]) 36 | ) 37 | 38 | def selection(self, population, offspring, alpha): 39 | # 调整适应度以考虑大小 40 | self.set_fitness_with_size(population, offspring, alpha) 41 | 42 | # 应用NSGA-II选择 43 | first_pareto_front = sortNondominated(offspring + population, len(population))[ 44 | 0 45 | ] 46 | selected_pop = selNSGA2(offspring + population, len(population)) 47 | 48 | if hasattr(self.algorithm, "hof") and self.algorithm.hof is not None: 49 | self.algorithm.hof.update(selected_pop) 50 | 51 | # 恢复原始适应度值 52 | self.restore_original_fitness(selected_pop) 53 | 54 | # 根据大小调整alpha 55 | theta = np.rad2deg(np.arctan(alpha)) 56 | avg_size = np.mean([len(p) for p in first_pareto_front]) 57 | 58 | # 更新历史最大和最小值 59 | self.update_best(first_pareto_front) 60 | 61 | # 计算新的alpha值 62 | new_alpha = self.adjust_alpha(theta, avg_size) 63 | 64 | return selected_pop, new_alpha 65 | 66 | def adjust_alpha(self, theta, avg_size): 67 | historical_largest = self.historical_largest 68 | historical_smallest = self.historical_smallest 69 | 70 | # 防止除以零 71 | if historical_largest == historical_smallest: 72 | return np.tan(np.deg2rad(theta)) 73 | 74 | theta = theta + ( 75 | historical_largest + historical_smallest - 2 * avg_size 76 | ) * self.step_size / (historical_largest - historical_smallest) 77 | theta = np.clip(theta, 0, 90) 78 | return np.tan(np.deg2rad(theta)) 79 | 80 | def restore_original_fitness(self, population): 81 | for ind in population: 82 | ind.fitness.weights = (-1, -1) 83 | ind.fitness.values = getattr(ind, "original_fitness") 84 | 85 | def set_fitness_with_size(self, population, offspring, alpha): 86 | max_size = max([len(x) for x in offspring + population]) 87 | for ind in offspring + population: 88 | assert alpha >= 0, f"Alpha Value {alpha}" 89 | setattr(ind, "original_fitness", ind.fitness.values) 90 | ind.fitness.weights = (-1, -1) 91 | # 修改第二个目标为模型大小和准确率的加权 92 | ind.fitness.values = ( 93 | ind.fitness.values[0], 94 | len(ind) / max_size + alpha * ind.fitness.values[0], 95 | ) 96 | 97 | 98 | # 修改适应度函数,包含两个权重:MSE和树的大小。MSE是最小化,树的大小也是最小化 99 | creator.create("FitnessMulti", base.Fitness, weights=(-1.0, -1.0)) 100 | creator.create("Individual", gp.PrimitiveTree, fitness=creator.FitnessMulti) 101 | 102 | # 定义函数集合和终端集合 103 | pset = gp.PrimitiveSet("MAIN", arity=1) 104 | pset.addPrimitive(operator.add, 2) 105 | pset.addPrimitive(operator.sub, 2) 106 | pset.addPrimitive(operator.mul, 2) 107 | pset.addPrimitive(operator.neg, 1) 108 | pset.addEphemeralConstant("rand101", lambda: random.randint(-1, 1)) 109 | pset.renameArguments(ARG0="x") 110 | 111 | # 定义遗传编程操作 112 | toolbox = base.Toolbox() 113 | toolbox.register("expr", gp.genHalfAndHalf, pset=pset, min_=1, max_=2) 114 | toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.expr) 115 | toolbox.register("population", tools.initRepeat, list, toolbox.individual) 116 | toolbox.register("compile", gp.compile, pset=pset) 117 | toolbox.register("evaluate", evalSymbReg, pset=pset) 118 | toolbox.register("mate", gp.cxOnePoint) 119 | toolbox.register("mutate", gp.mutUniform, expr=toolbox.expr, pset=pset) 120 | 121 | 122 | # 实现基于alpha支配的演化算法 123 | def eaMuPlusLambdaWithAlphaDominance( 124 | population, 125 | toolbox, 126 | mu, 127 | lambda_, 128 | cxpb, 129 | mutpb, 130 | ngen, 131 | stats=None, 132 | halloffame=None, 133 | verbose=__debug__, 134 | initial_alpha=0.1, 135 | step_size=0.1, 136 | ): 137 | """ 138 | 基于alpha支配的(mu + lambda)演化策略 139 | """ 140 | logbook = tools.Logbook() 141 | logbook.header = ["gen", "nevals"] + (stats.fields if stats else []) 142 | 143 | # 评估初始种群 144 | invalid_ind = [ind for ind in population if not ind.fitness.valid] 145 | fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) 146 | for ind, fit in zip(invalid_ind, fitnesses): 147 | ind.fitness.values = fit 148 | 149 | if halloffame is not None: 150 | halloffame.update(population) 151 | 152 | record = stats.compile(population) if stats else {} 153 | logbook.record(gen=0, nevals=len(invalid_ind), **record) 154 | if verbose: 155 | print(logbook.stream) 156 | 157 | # 初始化alpha支配 158 | selector = AlphaDominance( 159 | algorithm=toolbox, initial_alpha=initial_alpha, step_size=step_size 160 | ) 161 | alpha = initial_alpha 162 | 163 | # 开始演化 164 | for gen in range(1, ngen + 1): 165 | # 变异操作 166 | offspring = algorithms.varOr(population, toolbox, lambda_, cxpb, mutpb) 167 | 168 | # 评估后代 169 | invalid_ind = [ind for ind in offspring if not ind.fitness.valid] 170 | fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) 171 | for ind, fit in zip(invalid_ind, fitnesses): 172 | ind.fitness.values = fit 173 | 174 | # 使用alpha支配选择下一代 175 | population[:], alpha = selector.selection(population, offspring, alpha) 176 | 177 | if halloffame is not None: 178 | halloffame.update(population) 179 | 180 | # 记录统计信息 181 | record = stats.compile(population) if stats else {} 182 | logbook.record(gen=gen, nevals=len(invalid_ind), alpha=alpha, **record) 183 | if verbose: 184 | print(logbook.stream) 185 | 186 | return population, logbook 187 | 188 | 189 | # 统计指标 190 | stats_fit = tools.Statistics(lambda ind: ind.fitness.values[0]) 191 | stats_size = tools.Statistics(lambda ind: ind.fitness.values[1]) 192 | mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size) 193 | mstats.register("avg", np.mean) 194 | mstats.register("std", np.std) 195 | mstats.register("min", np.min) 196 | mstats.register("max", np.max) 197 | 198 | # 初始化种群 199 | population = toolbox.population(n=50) 200 | hof = tools.HallOfFame(1) 201 | 202 | # 运行演化算法 203 | pop, log = eaMuPlusLambdaWithAlphaDominance( 204 | population=population, 205 | toolbox=toolbox, 206 | mu=len(population), 207 | lambda_=len(population), 208 | cxpb=0.9, 209 | mutpb=0.1, 210 | ngen=10, 211 | stats=mstats, 212 | halloffame=hof, 213 | verbose=True, 214 | initial_alpha=0.1, # 初始alpha值 215 | step_size=0.1, # 自适应步长 216 | ) 217 | 218 | # 输出最佳个体 219 | best_ind = hof[0] if len(hof) > 0 else tools.selBest(pop, 1)[0] 220 | print("Best individual is:\n", best_ind) 221 | print("\nWith fitness:", best_ind.fitness.values) 222 | 223 | # 绘制Pareto前沿 224 | from matplotlib import pyplot as plt 225 | import seaborn as sns 226 | 227 | # 非支配排序 228 | fronts = tools.sortNondominated(pop, len(pop), first_front_only=True) 229 | 230 | # Pareto前沿 231 | pareto_front = fronts[0] 232 | fitnesses = [ind.fitness.values for ind in pareto_front] 233 | 234 | # 分离均方误差和树的大小 235 | mse = [fit[0] for fit in fitnesses] 236 | sizes = [fit[1] for fit in fitnesses] 237 | 238 | # 使用seaborn绘制散点图 239 | sns.set(style="whitegrid") 240 | plt.figure(figsize=(10, 6)) 241 | sns.scatterplot(x=mse, y=sizes, palette="viridis", s=60, edgecolor="w", alpha=0.7) 242 | plt.xlabel("Mean Square Error") 243 | plt.ylabel("Size of the GP Tree") 244 | plt.title("Pareto Front with Alpha Dominance") 245 | plt.show() 246 | -------------------------------------------------------------------------------- /tricks/compiler-speedup.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "8db4ada5ce6ebf73", 6 | "metadata": {}, 7 | "source": [ 8 | "### 低开销编译器\n", 9 | "DEAP在编译GP时使用了Python的默认编译器,但是Python默认编译器在编译GP时实际上速度较慢,因此我们可以考虑自行实现一个编译器来加速GP运算。更严格来说,应该是自行实现一个GP树的解析函数,从而降低编译的时间开销。" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "id": "initial_id", 16 | "metadata": { 17 | "ExecuteTime": { 18 | "end_time": "2023-12-25T02:37:08.047926400Z", 19 | "start_time": "2023-12-25T02:37:08.020758200Z" 20 | } 21 | }, 22 | "source": [ 23 | "import time\n", 24 | "import warnings\n", 25 | "\n", 26 | "import numpy as np\n", 27 | "from deap import base, creator, tools, gp\n", 28 | "from deap.gp import PrimitiveTree, Primitive, Terminal\n", 29 | "\n", 30 | "warnings.filterwarnings(\"ignore\")\n", 31 | "\n", 32 | "\n", 33 | "def quick_evaluate(expr: PrimitiveTree, pset, data, prefix='ARG'):\n", 34 | " result = None\n", 35 | " stack = []\n", 36 | " for node in expr:\n", 37 | " stack.append((node, []))\n", 38 | " while len(stack[-1][1]) == stack[-1][0].arity:\n", 39 | " prim, args = stack.pop()\n", 40 | " if isinstance(prim, Primitive):\n", 41 | " result = pset.context[prim.name](*args)\n", 42 | " elif isinstance(prim, Terminal):\n", 43 | " if prefix in prim.name:\n", 44 | " result = data[:, int(prim.name.replace(prefix, ''))]\n", 45 | " else:\n", 46 | " result = prim.value\n", 47 | " else:\n", 48 | " raise Exception\n", 49 | " if len(stack) == 0:\n", 50 | " break # 栈为空代表所有节点都已经被访问\n", 51 | " stack[-1][1].append(result)\n", 52 | " return result\n", 53 | "\n", 54 | "\n", 55 | "# 符号回归\n", 56 | "def evalSymbReg(individual, pset):\n", 57 | " # 使用numpy创建一个向量\n", 58 | " x = np.linspace(-10, 10, 100).reshape(-1, 1)\n", 59 | "\n", 60 | " # 评估生成的函数并计算MSE\n", 61 | " mse = np.mean((quick_evaluate(individual, pset, x) - x ** 2) ** 2)\n", 62 | "\n", 63 | " return (mse,)\n", 64 | "\n", 65 | "\n", 66 | "# 创建个体和适应度函数\n", 67 | "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,))\n", 68 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)" 69 | ], 70 | "outputs": [] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "id": "e3d94e424b58af5a", 75 | "metadata": {}, 76 | "source": [] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 2, 81 | "id": "cb6cf38094256262", 82 | "metadata": { 83 | "ExecuteTime": { 84 | "end_time": "2023-12-25T02:37:08.054440500Z", 85 | "start_time": "2023-12-25T02:37:08.049951400Z" 86 | } 87 | }, 88 | "source": [ 89 | "import random\n", 90 | "\n", 91 | "# 定义函数集合和终端集合\n", 92 | "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n", 93 | "pset.addPrimitive(np.add, 2)\n", 94 | "pset.addPrimitive(np.subtract, 2)\n", 95 | "pset.addPrimitive(np.multiply, 2)\n", 96 | "pset.addPrimitive(np.negative, 1)\n", 97 | "pset.addEphemeralConstant(\"rand101\", lambda: random.randint(-1, 1))\n", 98 | "\n", 99 | "# 定义遗传编程操作\n", 100 | "toolbox = base.Toolbox()\n", 101 | "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n", 102 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 103 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 104 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 105 | "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n", 106 | "toolbox.register(\"select\", tools.selTournament, tournsize=3)\n", 107 | "toolbox.register(\"mate\", gp.cxOnePoint)\n", 108 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)" 109 | ], 110 | "outputs": [] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "id": "e09fa8e7890d583b", 115 | "metadata": {}, 116 | "source": [] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 3, 121 | "id": "88c62bc071d56191", 122 | "metadata": { 123 | "ExecuteTime": { 124 | "end_time": "2023-12-25T02:37:08.226956400Z", 125 | "start_time": "2023-12-25T02:37:08.054649800Z" 126 | } 127 | }, 128 | "source": [ 129 | "import numpy\n", 130 | "from deap import algorithms\n", 131 | "\n", 132 | "# 定义统计指标\n", 133 | "stats_fit = tools.Statistics(lambda ind: ind.fitness.values)\n", 134 | "stats_size = tools.Statistics(len)\n", 135 | "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n", 136 | "mstats.register(\"avg\", numpy.mean)\n", 137 | "mstats.register(\"std\", numpy.std)\n", 138 | "mstats.register(\"min\", numpy.min)\n", 139 | "mstats.register(\"max\", numpy.max)\n", 140 | "\n", 141 | "# 使用默认算法\n", 142 | "custom_compiler_time = []\n", 143 | "for i in range(3):\n", 144 | " start = time.time()\n", 145 | " population = toolbox.population(n=100)\n", 146 | " hof = tools.HallOfFame(1)\n", 147 | " pop, log = algorithms.eaSimple(population=population,\n", 148 | " toolbox=toolbox, cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=hof,\n", 149 | " verbose=True)\n", 150 | " end = time.time()\n", 151 | " print('time:', end - start)\n", 152 | " print(str(hof[0]))\n", 153 | " custom_compiler_time.append(end - start)" 154 | ], 155 | "outputs": [] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 4, 160 | "id": "93e26be78cb63ef", 161 | "metadata": { 162 | "ExecuteTime": { 163 | "end_time": "2023-12-25T02:37:08.449160200Z", 164 | "start_time": "2023-12-25T02:37:08.228958300Z" 165 | } 166 | }, 167 | "source": [ 168 | "# 慢速评估\n", 169 | "def evalSymbReg(individual, pset):\n", 170 | " # 编译GP树为函数\n", 171 | " func = gp.compile(expr=individual, pset=pset)\n", 172 | "\n", 173 | " # 使用numpy创建一个向量\n", 174 | " x = np.linspace(-10, 10, 100)\n", 175 | "\n", 176 | " # 评估生成的函数并计算MSE\n", 177 | " mse = np.mean((func(x) - x ** 2) ** 2)\n", 178 | "\n", 179 | " return (mse,)\n", 180 | "\n", 181 | "\n", 182 | "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n", 183 | "\n", 184 | "py_time = []\n", 185 | "for i in range(3):\n", 186 | " start = time.time()\n", 187 | " population = toolbox.population(n=100)\n", 188 | " hof = tools.HallOfFame(1)\n", 189 | " pop, log = algorithms.eaSimple(population=population,\n", 190 | " toolbox=toolbox, cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=hof,\n", 191 | " verbose=True)\n", 192 | " end = time.time()\n", 193 | " print('time:', end - start)\n", 194 | " py_time.append(end - start)" 195 | ], 196 | "outputs": [] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "id": "8c746d5032e852bb", 201 | "metadata": {}, 202 | "source": [ 203 | "下图展示了实验结果,从实验结果可以看出,自行实现的编译器在编译GP树时的速度要快于Python默认编译器。主要是因为自行实现的编译器基本没有额外开销,而Python默认编译器在编译时会进行一些额外的操作,因此速度较慢。" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": 5, 209 | "id": "c6e9fb08c172a7f0", 210 | "metadata": { 211 | "ExecuteTime": { 212 | "end_time": "2023-12-25T02:37:08.749483500Z", 213 | "start_time": "2023-12-25T02:37:08.450148200Z" 214 | } 215 | }, 216 | "source": [ 217 | "import seaborn as sns\n", 218 | "import matplotlib.pyplot as plt\n", 219 | "import pandas as pd\n", 220 | "\n", 221 | "data = pd.DataFrame(\n", 222 | " {'Category': ['Efficient Compiler'] * len(custom_compiler_time) + ['Python Compiler'] * len(py_time),\n", 223 | " 'Time': np.concatenate([custom_compiler_time, py_time])})\n", 224 | "\n", 225 | "plt.figure(figsize=(4, 3))\n", 226 | "sns.set_style(\"whitegrid\")\n", 227 | "sns.boxplot(data=data, x='Category', y='Time', palette=\"Set3\", width=0.4)\n", 228 | "plt.title('Comparison of Efficient Compiler and Python Compiler')\n", 229 | "plt.xlabel('')\n", 230 | "plt.ylabel('Time')\n", 231 | "plt.show()" 232 | ], 233 | "outputs": [] 234 | } 235 | ], 236 | "metadata": { 237 | "kernelspec": { 238 | "display_name": "Python 3 (ipykernel)", 239 | "language": "python", 240 | "name": "python3" 241 | }, 242 | "language_info": { 243 | "codemirror_mode": { 244 | "name": "ipython", 245 | "version": 3 246 | }, 247 | "file_extension": ".py", 248 | "mimetype": "text/x-python", 249 | "name": "python", 250 | "nbconvert_exporter": "python", 251 | "pygments_lexer": "ipython3", 252 | "version": "3.11.4" 253 | } 254 | }, 255 | "nbformat": 4, 256 | "nbformat_minor": 5 257 | } 258 | -------------------------------------------------------------------------------- /application/symbolic-regression.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "cbf709014ce0316e", 6 | "metadata": {}, 7 | "source": [ 8 | "## 基于单树GP的符号回归(Symbolic Regression)\n", 9 | "\n", 10 | "基于单树GP的符号回归是指使用遗传编程(GP)生成数学公式来逼近一组数据的关系,通过组合DEAP的Creator,Toolbox和Algorithms这三个模块即可实现。\n", 11 | "\n", 12 | "\n", 13 | "### Creator类\n", 14 | "Creator是一个工具类,其主要作用是创建新的类。在遗传编程中,通常需要自定义个体(Individual)和适应度(Fitness)类,因为不同的问题可能需要不同的适应度类型和个体结构。在DEAP中,我们可以使用creator来动态地创建这些类。\n", 15 | "\n", 16 | "在下面的例子中,我们创建了一个最基本的单目标单树GP,可以使用base.Fitness和gp.PrimitiveTree来定义。" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 11, 22 | "id": "59cfefc0467c74ad", 23 | "metadata": { 24 | "ExecuteTime": { 25 | "end_time": "2023-11-08T02:39:00.130308400Z", 26 | "start_time": "2023-11-08T02:39:00.012636500Z" 27 | } 28 | }, 29 | "source": [ 30 | "import math\n", 31 | "import operator\n", 32 | "\n", 33 | "from deap import base, creator, tools, gp\n", 34 | "\n", 35 | "\n", 36 | "# 符号回归\n", 37 | "def evalSymbReg(individual, pset):\n", 38 | " # 编译GP树为函数\n", 39 | " func = gp.compile(expr=individual, pset=pset)\n", 40 | " # 计算均方误差(Mean Square Error,MSE)\n", 41 | " mse = ((func(x) - x**2)**2 for x in range(-10, 10))\n", 42 | " return (math.fsum(mse),)\n", 43 | "\n", 44 | "# 创建个体和适应度函数\n", 45 | "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,))\n", 46 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)" 47 | ], 48 | "outputs": [] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "id": "956e01e17271daa6", 53 | "metadata": {}, 54 | "source": [ 55 | "### Toolbox类\n", 56 | "Toolbox的作用类似于一个调度中心,它负责“注册”各种操作和函数。在遗传编程中,这些操作通常包括交叉(crossover)、变异(mutation)、选择(selection)和评估(evaluation)。通过register,我们可以将这些操作和相关的函数绑定在一起,以供后续算法使用。" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 12, 62 | "id": "851794d4d36e3681", 63 | "metadata": { 64 | "ExecuteTime": { 65 | "end_time": "2023-11-08T02:39:00.214209Z", 66 | "start_time": "2023-11-08T02:39:00.052073500Z" 67 | } 68 | }, 69 | "source": [ 70 | "import random\n", 71 | "\n", 72 | "# 定义函数集合和终端集合\n", 73 | "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n", 74 | "pset.addPrimitive(operator.add, 2)\n", 75 | "pset.addPrimitive(operator.sub, 2)\n", 76 | "pset.addPrimitive(operator.mul, 2)\n", 77 | "pset.addPrimitive(operator.neg, 1)\n", 78 | "pset.addEphemeralConstant(\"rand101\", lambda: random.randint(-1, 1))\n", 79 | "pset.renameArguments(ARG0='x')\n", 80 | "\n", 81 | "# 定义遗传编程操作\n", 82 | "toolbox = base.Toolbox()\n", 83 | "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n", 84 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 85 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 86 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 87 | "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n", 88 | "toolbox.register(\"select\", tools.selTournament, tournsize=3)\n", 89 | "toolbox.register(\"mate\", gp.cxOnePoint)\n", 90 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)" 91 | ], 92 | "outputs": [] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "id": "62f30d17704db709", 97 | "metadata": {}, 98 | "source": [ 99 | "### Algorithms类\n", 100 | "Algorithms模块提供了一些现成的遗传算法和遗传编程的实现。例如,eaSimple是一个简单的遗传算法,它可以处理基本的选择、交叉、变异和演化迭代。" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 13, 106 | "id": "515b587d4f8876ea", 107 | "metadata": { 108 | "ExecuteTime": { 109 | "end_time": "2023-11-08T02:39:00.216839200Z", 110 | "start_time": "2023-11-08T02:39:00.068850700Z" 111 | } 112 | }, 113 | "source": [ 114 | "import numpy\n", 115 | "from deap import algorithms\n", 116 | "\n", 117 | "# 定义统计指标\n", 118 | "stats_fit = tools.Statistics(lambda ind: ind.fitness.values)\n", 119 | "stats_size = tools.Statistics(len)\n", 120 | "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n", 121 | "mstats.register(\"avg\", numpy.mean)\n", 122 | "mstats.register(\"std\", numpy.std)\n", 123 | "mstats.register(\"min\", numpy.min)\n", 124 | "mstats.register(\"max\", numpy.max)\n", 125 | "\n", 126 | "# 使用默认算法\n", 127 | "population = toolbox.population(n=100)\n", 128 | "hof = tools.HallOfFame(1)\n", 129 | "pop, log = algorithms.eaSimple(population=population,\n", 130 | " toolbox=toolbox, cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=hof, verbose=True)\n" 131 | ], 132 | "outputs": [] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "id": "237b39454ea988bc", 137 | "metadata": {}, 138 | "source": [ 139 | "由于DEAP重载了字符串运算符,因此可以直接输出结果。" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 14, 145 | "id": "918142f4e60d65a0", 146 | "metadata": { 147 | "ExecuteTime": { 148 | "end_time": "2023-11-08T02:39:00.217794500Z", 149 | "start_time": "2023-11-08T02:39:00.118939200Z" 150 | } 151 | }, 152 | "source": [ 153 | "print(str(hof[0]))" 154 | ], 155 | "outputs": [] 156 | }, 157 | { 158 | "cell_type": "markdown", 159 | "id": "54fe3d72a677307c", 160 | "metadata": {}, 161 | "source": [ 162 | "当然,我们也可以利用NetworkX库来对GP树进行可视化。" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": 15, 168 | "id": "2fa44e7277d90c4c", 169 | "metadata": { 170 | "ExecuteTime": { 171 | "end_time": "2023-11-08T02:39:00.449935300Z", 172 | "start_time": "2023-11-08T02:39:00.134624200Z" 173 | } 174 | }, 175 | "source": [ 176 | "import networkx as nx\n", 177 | "from deap.gp import graph\n", 178 | "from networkx.drawing.nx_agraph import graphviz_layout\n", 179 | "\n", 180 | "function_name = {\n", 181 | " 'add':'Add',\n", 182 | " 'sub':'Sub',\n", 183 | " 'mul':'Mul',\n", 184 | " 'neg':'Neg'\n", 185 | "}\n", 186 | "\n", 187 | "def is_number(string):\n", 188 | " try:\n", 189 | " float(string)\n", 190 | " return True\n", 191 | " except ValueError:\n", 192 | " return False\n", 193 | "\n", 194 | "\n", 195 | "def plot_a_tree(tree=hof[0]):\n", 196 | " red_nodes = []\n", 197 | " purple_nodes = []\n", 198 | " blue_nodes = []\n", 199 | " for gid, g in enumerate(tree):\n", 200 | " if (\n", 201 | " hasattr(g, \"value\")\n", 202 | " and isinstance(g.value, str)\n", 203 | " and g.value.startswith(\"ARG\")\n", 204 | " ):\n", 205 | " g.value = g.value.replace(\"ARG\", \"X\")\n", 206 | "\n", 207 | " if g.name in function_name:\n", 208 | " g.name = function_name[g.name]\n", 209 | "\n", 210 | " if hasattr(g, \"value\") and (\n", 211 | " is_number(g.value)\n", 212 | " or (g.value.startswith(\"X\") and int(g.value[1:]) < X.shape[1])\n", 213 | " ):\n", 214 | " # 基础节点\n", 215 | " red_nodes.append(gid)\n", 216 | " elif hasattr(g, \"value\") and g.value.startswith(\"X\"):\n", 217 | " g.value = \"$\\phi$\" + str(int(g.value.replace(\"X\", \"\")) - X.shape[1] + 1)\n", 218 | " purple_nodes.append(gid)\n", 219 | " elif hasattr(g, \"value\") and g.value.startswith(\"$\\phi$\"):\n", 220 | " purple_nodes.append(gid)\n", 221 | " else:\n", 222 | " # 深蓝色节点\n", 223 | " blue_nodes.append(gid)\n", 224 | " nodes, edges, labels = graph(tree)\n", 225 | " g = nx.Graph()\n", 226 | " g.add_nodes_from(nodes)\n", 227 | " g.add_edges_from(edges)\n", 228 | " pos = graphviz_layout(g, prog=\"dot\")\n", 229 | " red_nodes_idx = [nodes.index(n) for n in nodes if n in red_nodes]\n", 230 | " purple_nodes_idx = [nodes.index(n) for n in nodes if n in purple_nodes]\n", 231 | " blue_nodes_idx = [nodes.index(n) for n in nodes if n in blue_nodes]\n", 232 | " nx.draw_networkx_nodes(\n", 233 | " g, pos, nodelist=red_nodes_idx, node_color=\"darkred\", node_size=500\n", 234 | " )\n", 235 | " nx.draw_networkx_nodes(\n", 236 | " g, pos, nodelist=purple_nodes_idx, node_color=\"indigo\", node_size=500\n", 237 | " )\n", 238 | " nx.draw_networkx_nodes(\n", 239 | " g, pos, nodelist=blue_nodes_idx, node_color=\"darkblue\", node_size=500\n", 240 | " )\n", 241 | " nx.draw_networkx_edges(g, pos)\n", 242 | " nx.draw_networkx_labels(g, pos, labels, font_color=\"white\")\n", 243 | "\n", 244 | "\n", 245 | "plot_a_tree(hof[0])" 246 | ], 247 | "outputs": [] 248 | } 249 | ], 250 | "metadata": { 251 | "kernelspec": { 252 | "display_name": "Python 3 (ipykernel)", 253 | "language": "python", 254 | "name": "python3" 255 | }, 256 | "language_info": { 257 | "codemirror_mode": { 258 | "name": "ipython", 259 | "version": 3 260 | }, 261 | "file_extension": ".py", 262 | "mimetype": "text/x-python", 263 | "name": "python", 264 | "nbconvert_exporter": "python", 265 | "pygments_lexer": "ipython3", 266 | "version": "3.11.4" 267 | } 268 | }, 269 | "nbformat": 4, 270 | "nbformat_minor": 5 271 | } 272 | -------------------------------------------------------------------------------- /tricks/numba-lexicase-selection.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ff6050dfa4dc1b6", 6 | "metadata": {}, 7 | "source": [ 8 | "## Lexicase Selection Numba加速\n", 9 | "\n", 10 | "DEAP中Lexicase Selection的默认实现速度较慢。因此,我们可以尝试使用Numba来加速它。\n", 11 | "Numba的原理是将Python代码编译为LLVM中间代码,然后再编译为机器码。从而显著提高Python代码的运行速度。" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 3, 17 | "id": "59cfefc0467c74ad", 18 | "metadata": { 19 | "ExecuteTime": { 20 | "end_time": "2023-12-25T03:39:37.866831400Z", 21 | "start_time": "2023-12-25T03:39:37.665471400Z" 22 | } 23 | }, 24 | "source": [ 25 | "import numpy as np\n", 26 | "import math\n", 27 | "import operator\n", 28 | "\n", 29 | "from deap import base, creator, tools, gp\n", 30 | "import time\n", 31 | "\n", 32 | "\n", 33 | "# 符号回归\n", 34 | "def evalSymbReg(individual, pset):\n", 35 | " # 编译GP树为函数\n", 36 | " func = gp.compile(expr=individual, pset=pset)\n", 37 | " \n", 38 | " # 使用numpy创建一个向量\n", 39 | " x = np.linspace(-10, 10, 100) \n", 40 | " \n", 41 | " return tuple((func(x) - x**2)**2)\n", 42 | "\n", 43 | "\n", 44 | "# 创建个体和适应度函数,适应度数组大小与数据量相同\n", 45 | "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,) * 100) # 假设我们有100个数据点\n", 46 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)" 47 | ], 48 | "outputs": [] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "id": "956e01e17271daa6", 53 | "metadata": {}, 54 | "source": [ 55 | "### 遗传算子\n", 56 | "在使用Numba进行对Lexicase加速时,只需要重写Lexicase函数,加上@njit(cache=True)这个注解就可以了。\n", 57 | "需要注意一些特殊的函数可能不受Numba支持,但所有基本的Python运算符都是支持的。" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 4, 63 | "id": "c5dbeab9190022", 64 | "metadata": { 65 | "ExecuteTime": { 66 | "end_time": "2023-12-25T03:39:37.891395500Z", 67 | "start_time": "2023-12-25T03:39:37.870837200Z" 68 | } 69 | }, 70 | "source": [ 71 | "from numba import njit\n", 72 | "import numpy as np\n", 73 | "\n", 74 | "\n", 75 | "@njit(cache=True)\n", 76 | "def selAutomaticEpsilonLexicaseNumba(case_values, fit_weights, k):\n", 77 | " selected_individuals = []\n", 78 | " avg_cases = 0\n", 79 | "\n", 80 | " for i in range(k):\n", 81 | " candidates = list(range(len(case_values)))\n", 82 | " cases = np.arange(len(case_values[0]))\n", 83 | " np.random.shuffle(cases)\n", 84 | "\n", 85 | " while len(cases) > 0 and len(candidates) > 1:\n", 86 | " errors_for_this_case = np.array(\n", 87 | " [case_values[x][cases[0]] for x in candidates]\n", 88 | " )\n", 89 | " median_val = np.median(errors_for_this_case)\n", 90 | " median_absolute_deviation = np.median(\n", 91 | " np.array([abs(x - median_val) for x in errors_for_this_case])\n", 92 | " )\n", 93 | " if fit_weights > 0:\n", 94 | " best_val_for_case = np.max(errors_for_this_case)\n", 95 | " min_val_to_survive = best_val_for_case - median_absolute_deviation\n", 96 | " candidates = list(\n", 97 | " [\n", 98 | " x\n", 99 | " for x in candidates\n", 100 | " if case_values[x][cases[0]] >= min_val_to_survive\n", 101 | " ]\n", 102 | " )\n", 103 | " else:\n", 104 | " best_val_for_case = np.min(errors_for_this_case)\n", 105 | " max_val_to_survive = best_val_for_case + median_absolute_deviation\n", 106 | " candidates = list(\n", 107 | " [\n", 108 | " x\n", 109 | " for x in candidates\n", 110 | " if case_values[x][cases[0]] <= max_val_to_survive\n", 111 | " ]\n", 112 | " )\n", 113 | " cases = np.delete(cases, 0)\n", 114 | " avg_cases = (avg_cases * i + (len(case_values[0]) - len(cases))) / (i + 1)\n", 115 | " selected_individuals.append(np.random.choice(np.array(candidates)))\n", 116 | " return selected_individuals, avg_cases\n", 117 | "\n", 118 | "def selAutomaticEpsilonLexicaseFast(individuals, k):\n", 119 | " fit_weights = individuals[0].fitness.weights[0]\n", 120 | " case_values = np.array([ind.fitness.values for ind in individuals])\n", 121 | " index, avg_cases = selAutomaticEpsilonLexicaseNumba(case_values, fit_weights, k)\n", 122 | " selected_individuals = [individuals[i] for i in index]\n", 123 | " return selected_individuals" 124 | ], 125 | "outputs": [] 126 | }, 127 | { 128 | "cell_type": "markdown", 129 | "id": "783887f49d890b79", 130 | "metadata": {}, 131 | "source": [ 132 | "在定义好了新的Lexicase选择算子之后,在注册选择算子的时候,将新的选择算子注册进去就可以了。" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 5, 138 | "id": "851794d4d36e3681", 139 | "metadata": { 140 | "ExecuteTime": { 141 | "end_time": "2023-12-25T03:39:37.964779400Z", 142 | "start_time": "2023-12-25T03:39:37.897670100Z" 143 | } 144 | }, 145 | "source": [ 146 | "import random\n", 147 | "\n", 148 | "# 定义函数集合和终端集合\n", 149 | "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n", 150 | "pset.addPrimitive(operator.add, 2)\n", 151 | "pset.addPrimitive(operator.sub, 2)\n", 152 | "pset.addPrimitive(operator.mul, 2)\n", 153 | "pset.addPrimitive(operator.neg, 1)\n", 154 | "pset.addEphemeralConstant(\"rand101\", lambda: random.randint(-1, 1))\n", 155 | "pset.renameArguments(ARG0='x')\n", 156 | "\n", 157 | "# 定义遗传编程操作\n", 158 | "toolbox = base.Toolbox()\n", 159 | "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n", 160 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 161 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 162 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 163 | "toolbox.register(\"evaluate\", evalSymbReg, pset=pset)\n", 164 | "toolbox.register(\"select\", selAutomaticEpsilonLexicaseFast)\n", 165 | "toolbox.register(\"mate\", gp.cxOnePoint)\n", 166 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)" 167 | ], 168 | "outputs": [] 169 | }, 170 | { 171 | "cell_type": "markdown", 172 | "id": "62f30d17704db709", 173 | "metadata": {}, 174 | "source": [ 175 | "### 演化流程\n", 176 | "演化流程与传统符号回归相同。" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 6, 182 | "id": "515b587d4f8876ea", 183 | "metadata": { 184 | "ExecuteTime": { 185 | "end_time": "2023-12-25T03:39:39.571928900Z", 186 | "start_time": "2023-12-25T03:39:37.971234500Z" 187 | } 188 | }, 189 | "source": [ 190 | "import numpy\n", 191 | "from deap import algorithms\n", 192 | "\n", 193 | "# 定义统计指标\n", 194 | "stats_fit = tools.Statistics(lambda ind: ind.fitness.values)\n", 195 | "stats_size = tools.Statistics(len)\n", 196 | "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n", 197 | "mstats.register(\"avg\", numpy.mean)\n", 198 | "mstats.register(\"std\", numpy.std)\n", 199 | "mstats.register(\"min\", numpy.min)\n", 200 | "mstats.register(\"max\", numpy.max)\n", 201 | "\n", 202 | "# 使用Numba加速\n", 203 | "numba_lexicase_time = []\n", 204 | "for i in range(3):\n", 205 | " start = time.time()\n", 206 | " population = toolbox.population(n=100)\n", 207 | " hof = tools.HallOfFame(1)\n", 208 | " pop, log = algorithms.eaSimple(population=population,\n", 209 | " toolbox=toolbox, cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=hof, verbose=True)\n", 210 | " end = time.time()\n", 211 | " print(str(hof[0]))\n", 212 | " numba_lexicase_time.append(end - start)\n" 213 | ], 214 | "outputs": [] 215 | }, 216 | { 217 | "cell_type": "markdown", 218 | "id": "98efab3037e032ea", 219 | "metadata": {}, 220 | "source": [ 221 | "为了展示Numba加速的效果,我们将使用纯Python实现的Lexicase Selection进行对比。" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": 7, 227 | "id": "e1efd33d5f96a536", 228 | "metadata": { 229 | "ExecuteTime": { 230 | "end_time": "2023-12-25T03:45:33.479324Z", 231 | "start_time": "2023-12-25T03:39:39.565415500Z" 232 | } 233 | }, 234 | "source": [ 235 | "# 使用纯Python实现的Lexicase Selection\n", 236 | "toolbox.register(\"select\", tools.selAutomaticEpsilonLexicase)\n", 237 | "python_lexicase_time = []\n", 238 | "for i in range(3):\n", 239 | " start = time.time()\n", 240 | " population = toolbox.population(n=100)\n", 241 | " hof = tools.HallOfFame(1)\n", 242 | " pop, log = algorithms.eaSimple(population=population,\n", 243 | " toolbox=toolbox, cxpb=0.9, mutpb=0.1, ngen=10, stats=mstats, halloffame=hof, verbose=True)\n", 244 | " end = time.time()\n", 245 | " print(str(hof[0]))\n", 246 | " python_lexicase_time.append(end - start)" 247 | ], 248 | "outputs": [] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "id": "adae32e725d21dcf", 253 | "metadata": {}, 254 | "source": [ 255 | "下面是Numba加速和纯Python实现的Lexicase Selection的运行时间对比。从结果可以看出,Numba加速后的Lexicase Selection的运行速度远优于纯Python实现的Lexicase Selection。" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": 8, 261 | "id": "3caf7686fef519a", 262 | "metadata": { 263 | "ExecuteTime": { 264 | "end_time": "2023-12-25T03:45:33.641210400Z", 265 | "start_time": "2023-12-25T03:45:33.479324Z" 266 | } 267 | }, 268 | "source": [ 269 | "import seaborn as sns\n", 270 | "import matplotlib.pyplot as plt\n", 271 | "import pandas as pd\n", 272 | "\n", 273 | "data = pd.DataFrame(\n", 274 | " {'Category': ['Numba Lexicase'] * len(numba_lexicase_time) + ['Python Lexicase'] * len(python_lexicase_time),\n", 275 | " 'Time': np.concatenate([numba_lexicase_time, python_lexicase_time])})\n", 276 | "\n", 277 | "plt.figure(figsize=(4, 3))\n", 278 | "sns.set_style(\"whitegrid\")\n", 279 | "sns.boxplot(data=data, x='Category', y='Time', palette=\"Set3\", width=0.4)\n", 280 | "plt.title('Comparison of Numba and Pure Python')\n", 281 | "plt.xlabel('')\n", 282 | "plt.ylabel('Time')\n", 283 | "plt.show()" 284 | ], 285 | "outputs": [] 286 | } 287 | ], 288 | "metadata": { 289 | "kernelspec": { 290 | "display_name": "Python 3 (ipykernel)", 291 | "language": "python", 292 | "name": "python3" 293 | }, 294 | "language_info": { 295 | "codemirror_mode": { 296 | "name": "ipython", 297 | "version": 3 298 | }, 299 | "file_extension": ".py", 300 | "mimetype": "text/x-python", 301 | "name": "python", 302 | "nbconvert_exporter": "python", 303 | "pygments_lexer": "ipython3", 304 | "version": "3.11.4" 305 | } 306 | }, 307 | "nbformat": 4, 308 | "nbformat_minor": 5 309 | } 310 | -------------------------------------------------------------------------------- /application/automatically-design-de-operators.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "81d831b3b0e92996", 6 | "metadata": {}, 7 | "source": [ 8 | "### 基于遗传编程自动设计优化算法\n", 9 | "众所周知,演化计算中一个重要的研究课题就是设计新的优化算法。这个过程通常是由人类专家完成的,但是,我们是否可以让计算机自动设计优化算法呢?这个问题的答案是肯定的。本文将介绍如何基于遗传编程自动设计优化算法。\n", 10 | "\n", 11 | "**根据这样一个自动算法设计的工具,我们在得到一个算法公式之后,只要再观察一下自然界中是否有对应的生物行为,就可以得到一个新的智能优化算法。**\n", 12 | "\n", 13 | "比如,本文将尝试使用遗传编程自动设计出北极狐算法!\n", 14 | "\n", 15 | "![北极狐算法](img/Fox2.png)" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "id": "8e3427b91e831fc9", 21 | "metadata": {}, 22 | "source": [ 23 | "### 优化函数\n", 24 | "比如,我们希望自动设计出的算法可以再球型函数上表现良好。球型函数是一个单目标优化领域中的经典测试函数,其公式如下:" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 133, 30 | "id": "initial_id", 31 | "metadata": { 32 | "ExecuteTime": { 33 | "end_time": "2024-02-07T23:56:31.688305600Z", 34 | "start_time": "2024-02-07T23:56:31.666788Z" 35 | }, 36 | "collapsed": true 37 | }, 38 | "outputs": [], 39 | "source": [ 40 | "import operator\n", 41 | "import random\n", 42 | "\n", 43 | "from deap import base, creator, tools, gp, algorithms\n", 44 | "import numpy as np\n", 45 | "\n", 46 | "np.random.seed(0)\n", 47 | "random.seed(0)\n", 48 | "\n", 49 | "\n", 50 | "def sphere(x, c=[1, 1, 1]):\n", 51 | " \"\"\"\n", 52 | " Shifted Sphere function.\n", 53 | "\n", 54 | " Parameters:\n", 55 | " - x: Input vector.\n", 56 | " - c: Shift vector indicating the new optimal location.\n", 57 | "\n", 58 | " Returns:\n", 59 | " - The value of the shifted Sphere function at x.\n", 60 | " \"\"\"\n", 61 | " return sum((xi - ci) ** 2 for xi, ci in zip(x, c))" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "id": "d87e994c3144076d", 67 | "metadata": {}, 68 | "source": [ 69 | "### 经典优化算法\n", 70 | "在文献中,差分演化可以用来求解这个球型函数优化问题。" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 134, 76 | "id": "feb772104d562277", 77 | "metadata": { 78 | "ExecuteTime": { 79 | "end_time": "2024-02-07T23:56:31.817414Z", 80 | "start_time": "2024-02-07T23:56:31.695306200Z" 81 | } 82 | }, 83 | "outputs": [ 84 | { 85 | "name": "stdout", 86 | "output_type": "stream", 87 | "text": [ 88 | "传统DE算法得到的优化结果 4.506377260849465e-05\n" 89 | ] 90 | } 91 | ], 92 | "source": [ 93 | "# DE\n", 94 | "dim = 3\n", 95 | "bounds = np.array([[-5, 5]] * dim)\n", 96 | "\n", 97 | "\n", 98 | "# Define a simple DE algorithm to test the crossover\n", 99 | "def differential_evolution(\n", 100 | " crossover_func, bounds, population_size=10, max_generations=50\n", 101 | "):\n", 102 | " population = [\n", 103 | " np.random.rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0]) + bounds[:, 0]\n", 104 | " for _ in range(population_size)\n", 105 | " ]\n", 106 | " population = np.array(population)\n", 107 | " best = min(population, key=lambda ind: sphere(ind))\n", 108 | " for gen in range(max_generations):\n", 109 | " for i, x in enumerate(population):\n", 110 | " a, b, c = population[np.random.choice(len(population), 3, replace=False)]\n", 111 | " mutant = np.clip(crossover_func(a, b, c, np.random.randn(dim)), bounds[:, 0], bounds[:, 1])\n", 112 | " if sphere(mutant) < sphere(x):\n", 113 | " population[i] = mutant\n", 114 | " if sphere(mutant) < sphere(best):\n", 115 | " best = mutant\n", 116 | " return sphere(best)\n", 117 | "\n", 118 | "\n", 119 | "print(\"传统DE算法得到的优化结果\",\n", 120 | " np.mean([differential_evolution(lambda a, b, c, F: a + F * (b - c), bounds) for _ in range(10)]))" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "id": "e46b8aec8871cdd9", 126 | "metadata": {}, 127 | "source": [ 128 | "可以看到,传统DE算法得到的优化结果是不错的。但是,我们是否可以自动设计出一个更好的算法呢?" 129 | ] 130 | }, 131 | { 132 | "cell_type": "markdown", 133 | "id": "712f8d2a7147ff03", 134 | "metadata": {}, 135 | "source": [ 136 | "### 基于遗传编程的自动设计优化算法\n", 137 | "其实DE的交叉算子本质上就是输入三个向量和一个随机向量,然后输出一个向量的函数。因此,我们可以使用遗传编程来自动设计这个交叉算子。" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": 135, 143 | "id": "3b598a4e994266e8", 144 | "metadata": { 145 | "ExecuteTime": { 146 | "end_time": "2024-02-07T23:56:46.285724800Z", 147 | "start_time": "2024-02-07T23:56:31.818414300Z" 148 | } 149 | }, 150 | "outputs": [ 151 | { 152 | "name": "stdout", 153 | "output_type": "stream", 154 | "text": [ 155 | "gen\tnevals\tavg \tmin \tmax \n", 156 | "0 \t50 \t2.6796\t0.0112234\t15.2248\n", 157 | "1 \t50 \t2.41407\t0.00253387\t17.9657\n", 158 | "2 \t45 \t1.41727\t0.0205569 \t18.5921\n", 159 | "3 \t47 \t0.99445\t0.00658522\t14.4601\n", 160 | "4 \t47 \t0.929668\t0.005623 \t13.84 \n", 161 | "5 \t48 \t1.61888 \t0.00913134\t13.9251\n", 162 | "6 \t50 \t1.18172 \t0.000383948\t14.9727\n", 163 | "7 \t48 \t0.624159\t0.000705421\t12.3018\n", 164 | "8 \t50 \t0.765903\t0.00214913 \t8.71667\n", 165 | "9 \t43 \t0.3652 \t0.0110385 \t3.56652\n", 166 | "10 \t47 \t1.39889 \t0.00685267 \t22.123 \n", 167 | "11 \t43 \t1.27877 \t0.00685267 \t20.31 \n", 168 | "12 \t48 \t1.82377 \t0.0027862 \t11.4693\n", 169 | "13 \t49 \t0.736725\t0.0108848 \t12.7022\n", 170 | "14 \t50 \t1.39344 \t0.0102804 \t12.8329\n", 171 | "15 \t47 \t0.847688\t0.00398283 \t11.3424\n", 172 | "16 \t44 \t0.9867 \t0.0067096 \t15.8511\n", 173 | "17 \t48 \t0.971622\t0.0180985 \t9.05041\n", 174 | "18 \t42 \t0.843393\t0.00948021 \t11.9563\n", 175 | "19 \t47 \t0.849741\t0.00759852 \t10.9686\n", 176 | "20 \t47 \t0.999861\t0.00425035 \t14.4111\n", 177 | "21 \t42 \t1.18842 \t0.00665311 \t13.5106\n", 178 | "22 \t46 \t1.41895 \t0.00320289 \t15.9007\n", 179 | "23 \t47 \t1.19332 \t0.00406941 \t9.579 \n", 180 | "24 \t48 \t0.923953\t0.00313277 \t11.4326\n", 181 | "25 \t45 \t0.599486\t0.00469191 \t8.87691\n", 182 | "26 \t43 \t1.06541 \t3.39457e-29\t15.4452\n", 183 | "27 \t44 \t1.38335 \t0.00224764 \t13.3298\n", 184 | "28 \t48 \t1.45239 \t0.017065 \t9.51407\n", 185 | "29 \t48 \t1.08886 \t0.00518668 \t12.8216\n", 186 | "30 \t48 \t0.55234 \t0.00209358 \t6.49766\n", 187 | "Best Crossover Operator:\n", 188 | "add(ARG0, subtract(multiply(ARG0, ARG3), ARG3))\n", 189 | "Fitness: (3.3945670827791664e-29,)\n" 190 | ] 191 | } 192 | ], 193 | "source": [ 194 | "# GP 算子\n", 195 | "pset = gp.PrimitiveSetTyped(\"MAIN\", [np.ndarray, np.ndarray, np.ndarray, np.ndarray], np.ndarray)\n", 196 | "pset.addPrimitive(np.add, [np.ndarray, np.ndarray], np.ndarray)\n", 197 | "pset.addPrimitive(np.subtract, [np.ndarray, np.ndarray], np.ndarray)\n", 198 | "pset.addPrimitive(np.multiply, [np.ndarray, np.ndarray], np.ndarray)\n", 199 | "pset.addEphemeralConstant(\"rand100\", lambda: np.random.randn(dim), np.ndarray)\n", 200 | "\n", 201 | "pset.context[\"array\"] = np.array\n", 202 | "\n", 203 | "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,))\n", 204 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)\n", 205 | "\n", 206 | "toolbox = base.Toolbox()\n", 207 | "toolbox.register(\"expr\", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)\n", 208 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 209 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 210 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 211 | "\n", 212 | "\n", 213 | "# Evaluate function for GP individuals\n", 214 | "def evalCrossover(individual):\n", 215 | " # Convert the individual into a function\n", 216 | " func = toolbox.compile(expr=individual)\n", 217 | " return (differential_evolution(func, bounds),)\n", 218 | "\n", 219 | "\n", 220 | "toolbox.register(\"evaluate\", evalCrossover)\n", 221 | "toolbox.register(\"select\", tools.selTournament, tournsize=3)\n", 222 | "toolbox.register(\"mate\", gp.cxOnePoint)\n", 223 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)\n", 224 | "\n", 225 | "# Evolve crossover strategies\n", 226 | "population = toolbox.population(n=50)\n", 227 | "hof = tools.HallOfFame(1)\n", 228 | "stats = tools.Statistics(lambda ind: ind.fitness.values)\n", 229 | "stats.register(\"avg\", np.mean)\n", 230 | "stats.register(\"min\", np.min)\n", 231 | "stats.register(\"max\", np.max)\n", 232 | "\n", 233 | "algorithms.eaSimple(population, toolbox, 0.9, 0.1, 30, stats, halloffame=hof)\n", 234 | "\n", 235 | "# Best crossover operator\n", 236 | "best_crossover = hof[0]\n", 237 | "print(f\"Best Crossover Operator:\\n{best_crossover}\")\n", 238 | "print(f\"Fitness: {best_crossover.fitness.values}\")" 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "id": "cf2c8fb3e5d148c6", 244 | "metadata": {}, 245 | "source": [ 246 | "### 分析新算法\n", 247 | "现在,我们得到了一个新的交叉算子。我们可以看一下这个交叉算子的公式。\n", 248 | "$X_{new}=X+(F*X-F)$, F是一个随机变量。" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 137, 254 | "id": "71c1e9de586767b0", 255 | "metadata": { 256 | "ExecuteTime": { 257 | "end_time": "2024-02-07T23:58:03.859051200Z", 258 | "start_time": "2024-02-07T23:58:03.730618Z" 259 | } 260 | }, 261 | "outputs": [ 262 | { 263 | "name": "stdout", 264 | "output_type": "stream", 265 | "text": [ 266 | "新优化算法得到的优化结果 1.0213225557390857e-19\n" 267 | ] 268 | } 269 | ], 270 | "source": [ 271 | "add = np.add\n", 272 | "subtract = np.subtract\n", 273 | "multiply = np.multiply\n", 274 | "square = np.square\n", 275 | "array = np.array\n", 276 | "\n", 277 | "crossover_operator = lambda ARG0, ARG1, ARG2, ARG3: add(ARG0, subtract(multiply(ARG0, ARG3), ARG3))\n", 278 | "print(\"新优化算法得到的优化结果\", np.mean([differential_evolution(crossover_operator, bounds) for _ in range(10)]))\n" 279 | ] 280 | }, 281 | { 282 | "cell_type": "markdown", 283 | "id": "c39ad9e7553bc87", 284 | "metadata": {}, 285 | "source": [ 286 | "从结果可以看到,新的优化算法得到的优化结果优于传统DE算法。这证明GP发现了一个更好的新算法。" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "id": "37941230eb02cbab", 292 | "metadata": {}, 293 | "source": [ 294 | "### 北极狐算法\n", 295 | "现在,这个算法我们可以命名为北极狐算法。北极狐的毛色会根据季节变化。在这个公式中,X会根据随机变量F的变化而变化。这个公式的形式和北极狐的毛色变化有些相似。因此,我们可以将这个算法命名为北极狐算法。\n", 296 | "![北极狐算法](img/Fox.png)\n", 297 | "\n", 298 | "该算法的交叉算子为$X_{new}=X+(F*X-F)$。" 299 | ] 300 | } 301 | ], 302 | "metadata": { 303 | "kernelspec": { 304 | "display_name": "Python 3 (ipykernel)", 305 | "language": "python", 306 | "name": "python3" 307 | }, 308 | "language_info": { 309 | "codemirror_mode": { 310 | "name": "ipython", 311 | "version": 3 312 | }, 313 | "file_extension": ".py", 314 | "mimetype": "text/x-python", 315 | "name": "python", 316 | "nbconvert_exporter": "python", 317 | "pygments_lexer": "ipython3", 318 | "version": "3.11.4" 319 | } 320 | }, 321 | "nbformat": 4, 322 | "nbformat_minor": 5 323 | } 324 | -------------------------------------------------------------------------------- /tricks/numpy_speedup_sr.py: -------------------------------------------------------------------------------- 1 | import random 2 | import numpy as np 3 | import sympy as sp 4 | from sklearn.base import BaseEstimator, RegressorMixin 5 | from sklearn.preprocessing import StandardScaler 6 | from sklearn.utils import check_array, check_random_state 7 | from deap import base, creator, tools, gp, algorithms 8 | 9 | 10 | if not hasattr(creator, "FitnessMin"): 11 | creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) 12 | if not hasattr(creator, "Individual"): 13 | creator.create("Individual", gp.PrimitiveTree, fitness=creator.FitnessMin) 14 | 15 | 16 | class SymbolicRegressor(BaseEstimator, RegressorMixin): 17 | """ 18 | Symbolic Regression estimator using DEAP with numpy acceleration. 19 | 20 | Parameters 21 | ---------- 22 | population_size : int, default=300 23 | Number of individuals in the population. 24 | 25 | n_generations : int, default=10 26 | Number of generations to evolve. 27 | 28 | cxpb : float, default=0.9 29 | Crossover probability. 30 | 31 | mutpb : float, default=0.1 32 | Mutation probability. 33 | 34 | tournsize : int, default=3 35 | Tournament size for selection. 36 | 37 | min_depth : int, default=1 38 | Minimum tree depth for initialization. 39 | 40 | max_depth : int, default=2 41 | Maximum tree depth for initialization. 42 | 43 | mut_min_depth : int, default=0 44 | Minimum tree depth for mutation. 45 | 46 | mut_max_depth : int, default=2 47 | Maximum tree depth for mutation. 48 | 49 | verbose : bool, default=False 50 | Whether to print progress during evolution. 51 | 52 | random_state : int or None, default=None 53 | Random seed for reproducibility. 54 | 55 | Attributes 56 | ---------- 57 | best_individual_ : PrimitiveTree 58 | The best individual found during evolution. 59 | 60 | pset_ : PrimitiveSet 61 | The primitive set used for evolution. 62 | 63 | toolbox_ : Toolbox 64 | The DEAP toolbox with registered operations. 65 | 66 | feature_names_ : list of str 67 | Names of the input features. 68 | """ 69 | 70 | def __init__( 71 | self, 72 | population_size=300, 73 | n_generations=10, 74 | cxpb=0.9, 75 | mutpb=0.1, 76 | tournsize=3, 77 | min_depth=1, 78 | max_depth=2, 79 | mut_min_depth=0, 80 | mut_max_depth=2, 81 | verbose=False, 82 | random_state=None, 83 | ): 84 | self.population_size = population_size 85 | self.n_generations = n_generations 86 | self.cxpb = cxpb 87 | self.mutpb = mutpb 88 | self.tournsize = tournsize 89 | self.min_depth = min_depth 90 | self.max_depth = max_depth 91 | self.mut_min_depth = mut_min_depth 92 | self.mut_max_depth = mut_max_depth 93 | self.verbose = verbose 94 | self.random_state = random_state 95 | 96 | self.best_individual_ = None 97 | self.pset_ = None 98 | self.toolbox_ = None 99 | self.feature_names_ = None 100 | self.n_features_ = None 101 | self.scaler_X_ = StandardScaler() 102 | self.scaler_y_ = StandardScaler() 103 | 104 | def _create_primitive_set(self, n_features): 105 | """Create a primitive set for the given number of features.""" 106 | pset = gp.PrimitiveSet("MAIN", arity=n_features) 107 | 108 | pset.addPrimitive(np.add, 2, name="add") 109 | pset.addPrimitive(np.subtract, 2, name="subtract") 110 | pset.addPrimitive(np.multiply, 2, name="multiply") 111 | pset.addPrimitive(np.negative, 1, name="negative") 112 | 113 | def protected_div(x1, x2): 114 | with np.errstate(divide="ignore", invalid="ignore"): 115 | return np.where(np.abs(x2) > 0.001, np.divide(x1, x2), 1.0) 116 | 117 | pset.addPrimitive(protected_div, 2, name="protected_div") 118 | 119 | pset.addPrimitive(np.sin, 1, name="sin") 120 | pset.addPrimitive(np.cos, 1, name="cos") 121 | pset.addPrimitive(np.exp, 1, name="exp") 122 | pset.addPrimitive(np.log, 1, name="log") 123 | 124 | def random_float(): 125 | return random.uniform(-1.0, 1.0) 126 | 127 | pset.addEphemeralConstant("rand", random_float) 128 | 129 | if self.feature_names_ is not None: 130 | arg_names = {f"ARG{i}": name for i, name in enumerate(self.feature_names_)} 131 | pset.renameArguments(**arg_names) 132 | 133 | return pset 134 | 135 | def _evaluate_func(self, func, X): 136 | return ( 137 | func(X.ravel()) 138 | if self.n_features_ == 1 139 | else func(*[X[:, i] for i in range(self.n_features_)]) 140 | ) 141 | 142 | def _eval_symb_reg(self, individual, pset, X, y): 143 | """Evaluate a symbolic regression individual using numpy.""" 144 | func = gp.compile(expr=individual, pset=pset) 145 | try: 146 | y_pred = self._evaluate_func(func, X) 147 | mse = np.mean((y_pred - y) ** 2) 148 | return (1e10 if not np.isfinite(mse) else mse,) 149 | except (ValueError, TypeError, ZeroDivisionError, OverflowError): 150 | return (1e10,) 151 | 152 | def _create_toolbox(self, pset): 153 | """Create and configure the DEAP toolbox.""" 154 | toolbox = base.Toolbox() 155 | 156 | toolbox.register( 157 | "expr", 158 | gp.genHalfAndHalf, 159 | pset=pset, 160 | min_=self.min_depth, 161 | max_=self.max_depth, 162 | ) 163 | 164 | toolbox.register( 165 | "individual", tools.initIterate, creator.Individual, toolbox.expr 166 | ) 167 | toolbox.register("population", tools.initRepeat, list, toolbox.individual) 168 | toolbox.register("compile", gp.compile, pset=pset) 169 | 170 | def evaluate(individual): 171 | return self._eval_symb_reg(individual, pset, self.X_train_, self.y_train_) 172 | 173 | toolbox.register("evaluate", evaluate) 174 | toolbox.register("select", tools.selTournament, tournsize=self.tournsize) 175 | toolbox.register("mate", gp.cxOnePoint) 176 | toolbox.register( 177 | "expr_mut", gp.genFull, min_=self.mut_min_depth, max_=self.mut_max_depth 178 | ) 179 | toolbox.register("mutate", gp.mutUniform, expr=toolbox.expr_mut, pset=pset) 180 | 181 | return toolbox 182 | 183 | def fit(self, X, y): 184 | """ 185 | Fit the symbolic regression model. 186 | 187 | Parameters 188 | ---------- 189 | X : array-like of shape (n_samples, n_features) 190 | Training data. 191 | 192 | y : array-like of shape (n_samples,) 193 | Target values. 194 | 195 | Returns 196 | ------- 197 | self : object 198 | Returns self. 199 | """ 200 | X = check_array(X, ensure_2d=True, ensure_all_finite=True) 201 | y = np.asarray(y).ravel() 202 | 203 | self.n_features_ = X.shape[1] 204 | 205 | X_normalized = self.scaler_X_.fit_transform(X) 206 | y_normalized = self.scaler_y_.fit_transform(y.reshape(-1, 1)).ravel() 207 | 208 | self.X_train_ = X_normalized 209 | self.y_train_ = y_normalized 210 | 211 | if hasattr(X, "columns"): 212 | self.feature_names_ = list(X.columns) 213 | else: 214 | self.feature_names_ = [f"x{i}" for i in range(self.n_features_)] 215 | 216 | if self.random_state is not None: 217 | rng = check_random_state(self.random_state) 218 | random.seed(rng.randint(0, 2**31)) 219 | np.random.seed(self.random_state) 220 | 221 | self.pset_ = self._create_primitive_set(self.n_features_) 222 | self.toolbox_ = self._create_toolbox(self.pset_) 223 | 224 | stats = tools.Statistics(lambda ind: ind.fitness.values) 225 | for stat_name, stat_func in [ 226 | ("avg", np.mean), 227 | ("std", np.std), 228 | ("min", np.min), 229 | ("max", np.max), 230 | ]: 231 | stats.register(stat_name, stat_func) 232 | 233 | population = self.toolbox_.population(n=self.population_size) 234 | for ind in population: 235 | ind.fitness.values = self.toolbox_.evaluate(ind) 236 | 237 | hof = tools.HallOfFame(1) 238 | hof.update(population) 239 | 240 | algorithms.eaSimple( 241 | population=population, 242 | toolbox=self.toolbox_, 243 | cxpb=self.cxpb, 244 | mutpb=self.mutpb, 245 | ngen=self.n_generations, 246 | stats=stats, 247 | halloffame=hof, 248 | verbose=self.verbose, 249 | ) 250 | 251 | self.best_individual_ = hof[0] 252 | 253 | return self 254 | 255 | def predict(self, X): 256 | """ 257 | Predict using the symbolic regression model. 258 | 259 | Parameters 260 | ---------- 261 | X : array-like of shape (n_samples, n_features) 262 | Samples. 263 | 264 | Returns 265 | ------- 266 | y_pred : ndarray of shape (n_samples,) 267 | Predicted values. 268 | """ 269 | if self.best_individual_ is None: 270 | raise ValueError("Model has not been fitted yet. Call fit() first.") 271 | 272 | X = check_array(X, ensure_2d=True, ensure_all_finite=True) 273 | X_normalized = self.scaler_X_.transform(X) 274 | n_samples = X.shape[0] 275 | 276 | func = gp.compile(expr=self.best_individual_, pset=self.pset_) 277 | 278 | try: 279 | y_pred_normalized = self._evaluate_func(func, X_normalized) 280 | except (ValueError, TypeError, ZeroDivisionError, OverflowError): 281 | y_pred_normalized = np.zeros(n_samples) 282 | 283 | y_pred_normalized = np.asarray(y_pred_normalized).ravel() 284 | if len(y_pred_normalized) != n_samples: 285 | y_pred_normalized = np.resize(y_pred_normalized, n_samples) 286 | 287 | y_pred_normalized = np.nan_to_num( 288 | y_pred_normalized, nan=0.0, posinf=0.0, neginf=0.0 289 | ) 290 | y_pred = self.scaler_y_.inverse_transform( 291 | y_pred_normalized.reshape(-1, 1) 292 | ).ravel() 293 | 294 | return y_pred 295 | 296 | def model(self): 297 | """ 298 | Return the symbolic expression of the model as a SymPy expression. 299 | 300 | This method implements the SRbench algorithm interface. 301 | 302 | Returns 303 | ------- 304 | sympy_expr : sympy.Expr 305 | The symbolic expression representing the model. 306 | """ 307 | if self.best_individual_ is None: 308 | raise ValueError("Model has not been fitted yet. Call fit() first.") 309 | 310 | feature_names = self.feature_names_ or [ 311 | f"x{i}" for i in range(self.n_features_) 312 | ] 313 | symbols = {name: sp.Symbol(name) for name in feature_names} 314 | symbols.update( 315 | {f"ARG{i}": symbols[feature_names[i]] for i in range(self.n_features_)} 316 | ) 317 | 318 | stack = [] 319 | 320 | for node in reversed(self.best_individual_): 321 | if isinstance(node, gp.Primitive): 322 | name = node.name 323 | arity = node.arity 324 | 325 | args = [stack.pop() for _ in range(arity)] 326 | args.reverse() 327 | 328 | func_map = { 329 | "add": lambda a, b: a + b, 330 | "subtract": lambda a, b: a - b, 331 | "multiply": lambda a, b: a * b, 332 | "negative": lambda a: -a, 333 | "protected_div": lambda a, b: a / b, 334 | "sin": sp.sin, 335 | "cos": sp.cos, 336 | "exp": sp.exp, 337 | "log": sp.log, 338 | } 339 | if name in func_map: 340 | result = func_map[name](*args) 341 | else: 342 | result = sp.Function(name)(*args) 343 | 344 | stack.append(result) 345 | else: 346 | if isinstance(node, gp.Terminal): 347 | node_name = node.name 348 | 349 | if node_name in symbols: 350 | stack.append(symbols[node_name]) 351 | elif node_name.startswith("ARG"): 352 | try: 353 | arg_num = int(node_name[3:]) 354 | if self.feature_names_ is not None and arg_num < len( 355 | self.feature_names_ 356 | ): 357 | var_name = self.feature_names_[arg_num] 358 | if var_name not in symbols: 359 | symbols[var_name] = sp.Symbol(var_name) 360 | stack.append(symbols[var_name]) 361 | else: 362 | stack.append(sp.Symbol(node_name)) 363 | except (ValueError, IndexError): 364 | stack.append(sp.Symbol(node_name)) 365 | else: 366 | try: 367 | value = ( 368 | node.value 369 | if hasattr(node, "value") 370 | else float(node_name) 371 | ) 372 | stack.append(sp.Float(value)) 373 | except (ValueError, TypeError): 374 | stack.append(sp.Symbol(node_name)) 375 | else: 376 | try: 377 | stack.append(sp.Float(float(node))) 378 | except (ValueError, TypeError): 379 | stack.append(sp.Symbol(str(node))) 380 | 381 | if len(stack) != 1: 382 | raise ValueError( 383 | f"Invalid expression tree. Stack size: {len(stack)}, expected 1." 384 | ) 385 | 386 | return stack[0] 387 | 388 | def __str__(self): 389 | """String representation of the model.""" 390 | if self.best_individual_ is None: 391 | return "SymbolicRegressor(not fitted)" 392 | return f"SymbolicRegressor: {self.best_individual_}" 393 | 394 | def __repr__(self): 395 | """Detailed string representation.""" 396 | return ( 397 | f"SymbolicRegressor(population_size={self.population_size}, " 398 | f"n_generations={self.n_generations}, " 399 | f"cxpb={self.cxpb}, mutpb={self.mutpb})" 400 | ) 401 | 402 | 403 | if __name__ == "__main__": 404 | import numpy as np 405 | 406 | X = np.linspace(-10, 10, 100).reshape(-1, 1) 407 | y = X.ravel() ** 2 408 | 409 | regressor = SymbolicRegressor( 410 | population_size=300, n_generations=10, verbose=True, random_state=0 411 | ) 412 | 413 | regressor.fit(X, y) 414 | 415 | X_test = np.linspace(-5, 5, 20).reshape(-1, 1) 416 | y_pred = regressor.predict(X_test) 417 | 418 | print(f"\nPredictions: {y_pred[:5]}") 419 | print(f"True values: {(X_test[:5].ravel() ** 2)}") 420 | 421 | model_expr = regressor.model() 422 | print(f"\nSymbolic model: {model_expr}") 423 | print(f"Model type: {type(model_expr)}") 424 | -------------------------------------------------------------------------------- /application/TSP.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "5205e210503bfb61", 6 | "metadata": {}, 7 | "source": [ 8 | "### GP求解旅行商问题\n", 9 | "\n", 10 | "GP当然也可以用于求解组合优化问题。在这里,我们将使用GP来解决旅行商问题(TSP)。TSP是指一个旅行商要拜访n个城市,他必须从自己所在的城市出发,到这n个城市中的每一个城市去一次,最后回到自己所在的城市,而且每个城市只能去一次,求解从出发到回到自己所在城市的最短路径。\n", 11 | "\n", 12 | "旅行商问题是一个NP难问题,通常可以使用遗传算法求解。但是遗传算法的缺点是需要大量的迭代次数才能收敛到最优解。因此我们将使用启发式函数来求解这个问题。启发式函数是一个根据特征输入返回排序分数的函数。排序分数将用于选择下一个城市。\n", 13 | "\n", 14 | "在本教程中,我们将使用距离作为启发式函数的输入,也就是说,我们的启发式函数是距离的非线性变换函数。我们将尝试使用GP来演化这个函数。" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "id": "a74153623d4c5a7e", 20 | "metadata": {}, 21 | "source": [ 22 | "### 评估函数\n", 23 | "对于组合优化问题,评估函数相对复杂,需要根据领域知识进行设计。" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 1, 29 | "id": "initial_id", 30 | "metadata": { 31 | "ExecuteTime": { 32 | "end_time": "2023-11-12T06:28:22.884624500Z", 33 | "start_time": "2023-11-12T06:28:22.815968500Z" 34 | } 35 | }, 36 | "outputs": [], 37 | "source": [ 38 | "import numpy as np\n", 39 | "from deap import algorithms, base, creator, tools, gp\n", 40 | "\n", 41 | "def select_next_city(current_city, unvisited_cities, heuristic, distance_matrix):\n", 42 | " scores = []\n", 43 | " for next_city in unvisited_cities:\n", 44 | " # 计算从当前城市到下一个可能城市的距离\n", 45 | " distance = distance_matrix[current_city][next_city]\n", 46 | " # 使用距离作为启发式函数的输入,计算得分\n", 47 | " heuristic_score = heuristic(distance)\n", 48 | " # 计算得分用于选择下一个城市\n", 49 | " # 这里可以直接使用heuristic_score,也可以与距离结合\n", 50 | " # 例如,使用距离的倒数与启发式得分的和\n", 51 | " score = 1 / distance + heuristic_score\n", 52 | " scores.append((next_city, score))\n", 53 | "\n", 54 | " # 选择得分最高的城市作为下一站\n", 55 | " selected_city = max(scores, key=lambda c: c[1])[0]\n", 56 | " return selected_city\n", 57 | "\n", 58 | "def decode(individual, distance_matrix):\n", 59 | " # 初始化城市列表和路线\n", 60 | " unvisited_cities = list(range(len(distance_matrix))) # 未访问的城市列表\n", 61 | " route = [unvisited_cities.pop(0)] # 从第一个城市开始\n", 62 | "\n", 63 | " # 编译 GP 树为函数\n", 64 | " heuristic = gp.compile(expr=individual, pset=pset)\n", 65 | "\n", 66 | " # 循环直到所有城市都被访问\n", 67 | " while unvisited_cities:\n", 68 | " # 根据启发式函数和距离选择下一个城市\n", 69 | " current_city = route[-1] # 获取路线中的最后一个城市作为当前城市\n", 70 | " next_city = select_next_city(current_city, unvisited_cities, heuristic, distance_matrix)\n", 71 | " # 将下一个城市添加到路线中,并将其从未访问城市列表中移除\n", 72 | " route.append(next_city)\n", 73 | " unvisited_cities.remove(next_city)\n", 74 | "\n", 75 | " # 路径闭环,返回起始城市\n", 76 | " route.append(route[0])\n", 77 | " return route\n", 78 | "\n", 79 | "# 每个城市的坐标在一个单位正方形内随机生成\n", 80 | "coordinates = np.random.rand(10, 2) # 生成10个城市的x,y坐标\n", 81 | "\n", 82 | "# 计算距离矩阵\n", 83 | "def calculate_distance_matrix(coords):\n", 84 | " num_cities = len(coords)\n", 85 | " distance_matrix = np.zeros((num_cities, num_cities))\n", 86 | " for i in range(num_cities):\n", 87 | " for j in range(num_cities):\n", 88 | " if i != j:\n", 89 | " # 计算欧几里得距离\n", 90 | " distance_matrix[i][j] = np.sqrt(np.sum((coords[i] - coords[j])**2))\n", 91 | " else:\n", 92 | " # 城市不与自己连接\n", 93 | " distance_matrix[i][j] = np.inf\n", 94 | " return distance_matrix\n", 95 | "\n", 96 | "distance_matrix = calculate_distance_matrix(coordinates)\n", 97 | "\n", 98 | "# 评价函数,计算路径的总距离\n", 99 | "def evalTSP(individual):\n", 100 | " path = decode(individual, distance_matrix)\n", 101 | " distance = sum(distance_matrix[path[i]][path[i-1]] for i in range(1,len(path)))\n", 102 | " return distance,\n" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "id": "ff38033627ba98b4", 108 | "metadata": {}, 109 | "source": [ 110 | "### 遗传编程算子\n", 111 | "一旦问题定义好了,遗传编程算子的定义就变得相对简单。我们可以使用DEAP中内置的算子。" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 2, 117 | "id": "5b804caa1f073b17", 118 | "metadata": { 119 | "ExecuteTime": { 120 | "end_time": "2023-11-12T06:28:22.892859300Z", 121 | "start_time": "2023-11-12T06:28:22.889153400Z" 122 | } 123 | }, 124 | "outputs": [], 125 | "source": [ 126 | "# 初始化GP\n", 127 | "creator.create(\"FitnessMin\", base.Fitness, weights=(-1.0,)) # 最小化问题\n", 128 | "creator.create(\"Individual\", gp.PrimitiveTree, fitness=creator.FitnessMin)\n", 129 | "\n", 130 | "# 基本函数\n", 131 | "pset = gp.PrimitiveSet(\"MAIN\", arity=1)\n", 132 | "pset.addPrimitive(np.add, 2)\n", 133 | "pset.addPrimitive(np.subtract, 2)\n", 134 | "pset.addPrimitive(np.multiply, 2)\n", 135 | "pset.addPrimitive(np.negative, 1)\n", 136 | "\n", 137 | "toolbox = base.Toolbox()\n", 138 | "toolbox.register(\"expr\", gp.genGrow, pset=pset, min_=1, max_=3)\n", 139 | "toolbox.register(\"individual\", tools.initIterate, creator.Individual, toolbox.expr)\n", 140 | "toolbox.register(\"population\", tools.initRepeat, list, toolbox.individual)\n", 141 | "toolbox.register(\"compile\", gp.compile, pset=pset)\n", 142 | "toolbox.register(\"evaluate\", evalTSP)\n", 143 | "toolbox.register(\"select\", tools.selTournament, tournsize=3)\n", 144 | "toolbox.register(\"mate\", gp.cxOnePoint)\n", 145 | "toolbox.register(\"mutate\", gp.mutUniform, expr=toolbox.expr, pset=pset)\n", 146 | "toolbox.register(\"expr_mut\", gp.genGrow, min_=0, max_=2)" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "id": "a60b7c6d4d31cd23", 152 | "metadata": {}, 153 | "source": [ 154 | "接下来,我们将使用DEAP内置的演化流程来运行已定义的函数。" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 3, 160 | "id": "24db570e7597ba08", 161 | "metadata": { 162 | "ExecuteTime": { 163 | "end_time": "2023-11-12T06:28:23.107994600Z", 164 | "start_time": "2023-11-12T06:28:22.894867100Z" 165 | } 166 | }, 167 | "outputs": [ 168 | { 169 | "name": "stdout", 170 | "output_type": "stream", 171 | "text": [ 172 | " \t \t fitness \t size \n", 173 | " \t \t------------------------------------------------\t----------------------------------------------\n", 174 | "gen\tnevals\tavg \tgen\tmax \tmin \tnevals\tstd \tavg \tgen\tmax\tmin\tnevals\tstd \n", 175 | "0 \t100 \t3.19162\t0 \t3.5749\t3.15372\t100 \t0.120534\t5.86\t0 \t14 \t2 \t100 \t3.5497\n", 176 | "1 \t89 \t3.17056\t1 \t3.5749\t3.15372\t89 \t0.0825342\t6.54\t1 \t15 \t2 \t89 \t3.34192\n", 177 | "2 \t94 \t3.15793\t2 \t3.5749\t3.15372\t94 \t0.0419069\t6.81\t2 \t17 \t2 \t94 \t3.52901\n", 178 | "3 \t93 \t3.18741\t3 \t3.5749\t3.15372\t93 \t0.114263 \t7.25\t3 \t17 \t2 \t93 \t4.1143 \n", 179 | "4 \t94 \t3.17477\t4 \t3.5749\t3.15372\t94 \t0.0917942\t7.33\t4 \t17 \t2 \t94 \t4.13051\n", 180 | "5 \t89 \t3.17056\t5 \t3.5749\t3.15372\t89 \t0.0825342\t8.08\t5 \t22 \t2 \t89 \t4.58406\n", 181 | "6 \t86 \t3.17477\t6 \t3.5749\t3.15372\t86 \t0.0917942\t8.34\t6 \t22 \t2 \t86 \t4.62649\n", 182 | "7 \t86 \t3.17056\t7 \t3.5749\t3.15372\t86 \t0.0825342\t7.77\t7 \t23 \t2 \t86 \t4.48521\n", 183 | "8 \t92 \t3.18741\t8 \t3.5749\t3.15372\t92 \t0.114263 \t8.15\t8 \t19 \t2 \t92 \t4.36892\n", 184 | "9 \t92 \t3.16214\t9 \t3.5749\t3.15372\t92 \t0.0589653\t8.38\t9 \t23 \t2 \t92 \t5.00755\n", 185 | "10 \t94 \t3.15793\t10 \t3.5749\t3.15372\t94 \t0.0419069\t8.36\t10 \t25 \t2 \t94 \t5.05474\n", 186 | "Best individual: subtract(ARG0, ARG0) (3.1537154779102305,)\n" 187 | ] 188 | } 189 | ], 190 | "source": [ 191 | "# 统计函数\n", 192 | "stats_fit = tools.Statistics(lambda ind: ind.fitness.values)\n", 193 | "stats_size = tools.Statistics(len)\n", 194 | "mstats = tools.MultiStatistics(fitness=stats_fit, size=stats_size)\n", 195 | "mstats.register(\"avg\", np.mean)\n", 196 | "mstats.register(\"std\", np.std)\n", 197 | "mstats.register(\"min\", np.min)\n", 198 | "mstats.register(\"max\", np.max)\n", 199 | "\n", 200 | "population = toolbox.population(n=100)\n", 201 | "hof = tools.HallOfFame(1)\n", 202 | "\n", 203 | "# 运行遗传编程算法\n", 204 | "algorithms.eaSimple(population, toolbox, 0.9, 0.1, 10, mstats, halloffame=hof)\n", 205 | "\n", 206 | "# 输出最好的个体\n", 207 | "best_ind = hof[0]\n", 208 | "print('Best individual:', best_ind, best_ind.fitness)" 209 | ] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "execution_count": 4, 214 | "id": "f614c8f953aaf380", 215 | "metadata": { 216 | "ExecuteTime": { 217 | "end_time": "2023-11-12T06:28:23.401796500Z", 218 | "start_time": "2023-11-12T06:28:23.107994600Z" 219 | } 220 | }, 221 | "outputs": [ 222 | { 223 | "data": { 224 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+0AAALACAYAAADmApNPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAADYV0lEQVR4nOzdd3xUZdr/8c9kMpNeIQ2SECBACITekaaChaIUC67Ysa/7uCtueXZX3UfX3dX96a6Kve/a6agooiDSLXQSCBBISCOk98nM/P6YJBCpgSRnknzfrxcv4cw5Z67xZsJcc933dZucTqcTEREREREREXE7HkYHICIiIiIiIiKnpqRdRERERERExE0paRcRERERERFxU0raRURERERERNyUknYRERERERERN6WkXURERERERMRNKWkXERERERERcVNK2kVERERERETclJJ2ERERaVWcTqfRIYiIiLQYT6MDEBERkVP73e9+x6JFi854TufOnfn6668BKCgo4KWXXmLVqlVkZ2fj6+tL7969ueGGG7jsssvqr1m4cCG///3vG9zHw8MDf39/kpKSuO+++xg8ePBpn/Piiy/myJEjp7w+ISGBu+++m9GjRzf25Z5VcXExTzzxBLNmzWLo0KFNfn8RERF3pKRdRETETd17771cf/319X+eP38+u3fv5vnnn68/ZrVaAaisrOQXv/gFNTU1zJ07l7i4OEpKSvj888954IEH+P3vf88tt9zS4P7PP/88YWFhADgcDvLy8njhhRe4+eab+eSTT0hISDhtbOPGjePee++t/3NNTQ2HDx/mlVde4a677mLBggX06tWrKf431NuzZw+LFy9mxowZTXpfERERd6akXURExE3FxsYSGxtb/+fQ0FCsVisDBgw46dwVK1awf/9+VqxYQdeuXeuPX3rppVRWVvLcc88xZ84czGZz/WO9e/cmOjq6wX0SExOZOHEi7733Hn/5y19OG1toaOhJcQwZMoQBAwZwxRVXsGTJEh5++OFGvmIRERH5Oa1pFxERaQPy8vKAU6/3vuuuu7j33nuprq4+632io6MJCQkhMzPzvOIICAg46VhVVRUvvPACl19+OUlJSUyaNIlXXnkFh8NRf86cOXOYM2dOg+s2bdpEr1692LRpE5s2beKmm24C4Kabbmpw7ldffcWMGTNISkpi9OjRPP7445SXl59X/CIiIu5GSbuIiEgbMGbMGDw9Pbn55pt5/vnn2bp1KzabDYB+/fpx++234+Pjc9b7FBQUUFBQ0KDCfypOp5Oampr6X5WVlaSkpPC73/0OT09PpkyZUn/e3XffzWuvvcasWbN46aWXuPzyy3n22Wd55JFHzvn19enThz//+c8A/PnPf66/dtmyZdx3331069aNF154gfvvv5+lS5dy7733qmGdiIi0CZoeLyIi0gb06tWLZ555hscee4znnnuO5557Dm9vb4YMGcLMmTO58sorT7rG4XBQU1MDuKrhhw4d4qmnnsLDw4PrrrvujM+3ePFiFi9e3OCYp6cnffv25Y033iAxMRGAb7/9lvXr1/PUU08xbdo0AEaPHo23tzf/+te/uPnmm4mPjz/r6/P3968/Lz4+nvj4eJxOJ08//TRjxozh6aefrj83Li6OW265hTVr1jB+/Piz3ltERMSdKWkXERFpIyZNmsSECRPYuHEj69evZ9OmTaxfv57vvvuOFStW8K9//QuTyVR//sSJE0+6R+fOnXnqqafO2kRuwoQJ3HfffTidTtLS0vh//+//ERERwXPPPUd4eHj9eZs3b8ZsNp/0pcG0adP417/+xaZNm84paT+VAwcOkJ2dzV133VX/5QPA0KFD8ff3Z926dUraRUSk1VPSLiIi0oZYLBbGjBnDmDFjAMjNzeXxxx/niy++YPXq1UyYMKH+3BdffLG+e7zFYiEkJISIiIhzep7g4GCSkpIA1/T7Pn36MHPmTObOnctHH32El5cXAEVFRYSEhODp2fAjR93zlpSUnPdrLSwsBOCxxx7jscceO+nx3Nzc8763iIiIu1DSLiIi0gZcf/31dO3alSeffLLB8fDw8PqkPTU1tUHS3rNnz5O6x5+v7t2786tf/Yq//e1vvPDCC/z6178GICgoiIKCAmpqahok7nUJdUhISP0xu93e4J5nayYXGBgIwMMPP8ywYcNOejwoKOj8XoyIiIgbUSM6ERGRNqBz586sWLGC9PT0kx47ePAg4ErSm9OcOXPo2bMnb7zxRv1zDhs2DLvdzmeffdbg3KVLlwIwePBgwLVmPTs7u8E5P/74Y4M/n7hdHUC3bt3o0KEDGRkZJCUl1f+KjIzkn//8J7t3727S1yciImIEVdpFRETagAcffJBNmzYxa9YsbrrpJgYOHIiHhwc7duzgjTfeYOzYsYwdO7ZZY/D09OQPf/gDt9xyC48//jivv/46Y8eOZfjw4TzyyCPk5uaSmJjI5s2befXVV5k+fXr9evYJEybw9ddf88QTT3DppZfyww8/nNTorm47udWrVxMUFERCQgIPPvggf/7znzGbzUyYMIHi4mLmz59PTk4Offr0adbXKyIi0hKUtIuIiLQB0dHRLFq0iJdffplly5bx6quv4nQ66dKlC7fffjs33XRTgyZ0zWXkyJFcdtllfPHFF3z11VdceumlvPzyy/z73//mnXfeIT8/n+joaB588EFuvfXW+utmzpzJ4cOHWbRoER9++CHDhg3jX//6F7Nnz64/p0ePHkyZMoX//ve/rF27luXLl3PNNdfg5+fHa6+9xocffoivry+DBg3i6aefJiYmptlfr4iISHMzObWJqYiIiIiIiIhb0pp2ERERERERETelpF1ERERERETETSlpFxEREREREXFThiftDoeDf//734wZM4b+/ftz2223cejQodOef/ToUX79618zfPhwhg8fzq9+9auTtogRERERERERaQsMT9rnz5/PBx98wOOPP86HH36IyWRi7ty5VFdXn/L8Bx98kKysLN58803efPNNsrOzuffee1s4ahEREREREZHmZ2jSXl1dzRtvvMEvf/lLxo0bR0JCAs888ww5OTmsXLnypPOLi4vZsmULc+fOJTExkcTERO6880527dpFQUGBAa9AREREREREpPkYuk97cnIyZWVljBgxov5YYGAgiYmJbNmyhcmTJzc438vLC19fXxYvXsywYcMAWLJkCXFxcQQFBZ1XDD/99BNOpxOLxXL+L0RERERERETkHNlsNkwmEwMHDjzruYYm7XVr0aOiohocDw8PJysr66Tzvby8eOKJJ/jLX/7CkCFDMJlMhIWF8Z///AcPj/ObNOB0Out/XQin00lNTQ2enp6YTKYLupc0D42R+9MYuTeNj/vTGLk/jZH70xi5P42R+9MYnV1j8k9Dk/aKigoArFZrg+NeXl4UFRWddL7T6SQlJYWBAwdyxx13YLfbeeaZZ7jvvvt4//338ff3b3QMFouF6upqbDbb+b2In6mpqWmS+0jz0Ri5P42Re9P4uD+NkfvTGLk/jZH70xi5P43RmZ3rbG9Dk3Zvb2/Atba97vcAVVVV+Pj4nHT+p59+ynvvvcc333xTn6C/9NJL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qIYOHarf/OY3qqysDGLFoaela9TweXfccYeee+65IFQZ2lq6Rjt37tRdd92l4cOH6/zzz9e0adN04MCBIFYcWlq6Plu2bNGtt96qQYMGacSIEXriiSdUWloaxIpDz8l+nZOk9957T3369NG+ffvauMrQ1tI1evfdd9WnT59Gv5q7rmi5lq6Rx+PR008/rVGjRmngwIGaPHmysrKyglhx6GnJGj333HPHfA316dNHjz32WJArDx0tfR0VFBTo4Ycf1vDhwzV8+HA98MADOnjwYBArbt8I7c00f/58LV68WLNmzdKSJUtks9k0depUVVdXH/P4adOmKScnR6+++qrmzZunL774Qk8++WSQqw4tLV0jSXK73Zo+fbo+//zzIFYaulqyRkVFRZoyZYo6deqkRYsW6eWXX1ZRUZHuvPNOVVVVmVB9x9eS9cnPz9eUKVPUs2dPvfvuu5o/f76+++47PfrooyZUHjpO5uucJO3fv5//g4KkpWu0fft2DRs2TJ9//nnAr7S0tCBXHjpauka//e1v9c477+h3v/udli5dqoSEBE2dOlVlZWVBrjx0tGSNbr/99kavnwcffFBOp1O33nqrCdWHhpa+jh566CHl5ubqf//3f/W///u/OnjwoH75y18Guep2zECTqqqqjEGDBhlvvPFG/WMlJSVG//79jZUrVzY6/rvvvjPS09ONXbt21T+2bt06o0+fPsbBgweDUnOoaekaGYZhfPvtt8YVV1xhXHLJJcaQIUOMefPmBavckNTSNXrrrbeMzMxMw+121z+Wm5trpKenG19++WVQag4lJ/N17qGHHjI8Hk/9Y6+++qoxYMCAYJQbkk7m65xhGIbP5zNuuukm45ZbbjHS09ONnJycYJQbkk5mjaZMmWLMmjUrWCWGvJau0d69e4309HTj448/Djh+7Nix/F/URk72a12dPXv2GAMGDAh4PlpXS9eopKTESE9PNz766KP6x9asWWOkp6cbhYWFQam5vaPT3gzbtm1TRUWFRowYUf9YXFyc+vbtqw0bNjQ6fuPGjUpJSdFZZ51V/9iwYcNks9n07bffBqXmUNPSNZKkdevWady4cVq+fLliY2ODVWrIaukanX/++XrhhRcUGRnZ6H0lJSVtWmsoaun6DBo0SM8884zCw8MlSbt27dK7776rCy64IGg1h5qT+TonSS+++KI8Ho/u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paamasaMGfrss880atQozZ49WyNHjtTcuXPrz52ZmamrrrpKr7zyiqZPn17/+D333KMxY8ZIqtm1ff78+br88sv1+OOPS5IuvPBClZeXa/HixSesvaysTMuWLVPPnj0l1fzgYvLkyfr66691+eWXa9u2bRo5cqRmz56tsLCw+to/+eQTbdiwQePHj9fAgQMlSV27dq3/8/PPPy+n06lXX31VMTExkqTzzz9fl156qf7yl790iNsNAAChgU47AAAW4na79emnn8pms2n9+vXKzs5u8jkbN25URESExo4dW/+YzWbTm2++qQceeEDffPONJGn8+PEBz7v66qtlt9u1fv167d69WwUFBY2O6dmzpwYNGlTfsa+Tnp5e/+effvpJhw8frv+BQZ0rr7yyydqTkpLqA7tUE7wl1W8qN3HiRL388svyeDzauXOn1qxZo+eee04+n08ej+e45/366681fPhwOZ1Oeb1eeb1excTEaMiQIfryyy+brAsAAKug0w4AgIXMmjVLu3fv1nPPPacZM2bokUce0ZtvvqmIiIjjPqe4uFgJCQn1neij1V1qnpKSEvB4eHi4EhMTVVZWpuLiYklS586dGz2/c+fO2rp1a8BjycnJjc6flJQUcMzRH+9YoqKiAt622WySVH+/utvt1u9+9zutWLFCXq9XaWlpGjRokMLDw084l724uFirVq3SqlWrGr3v6DoBALAyQjsAABaxatUqvf3223r44Yc1btw4/dd//Zcef/xxPffcc3r44YeP+7zY2FgVFxfL7/cHBPesrCx5vV7Fx8dLkgoKCpSWllb/fo/Ho6KiIiUmJiohIUGSdOjQoUbnLygoUGJi4nE/ft37jr5XvO4HAafi97//vf7xj3/o2Wef1ciRIxUdHS2p5lL3E4mNjdXIkSM1ZcqURu8LD+fbHwBA+8Hl8QAAWEBOTo5+85vfaNiwYZo6daok6YYbbtAll1yil19+WRs2bDjuc4cMGSKPxxOw67phGPr1r3+tBQsWaNiwYZKk9957L+B577//vnw+nwYPHqwzzjhDKSkpjY7JycnRP//5T2VmZh734/fq1UvdunXTBx98EPD4xx9/3LxP/gS+/fZbDR8+XJdeeml9YN+yZYsKCwvru/GSGl1lMGzYMO3atUvnnHOO+vXrp379+um8887Tq6++qtWrV59yXQAABAuhHQAAk3k8Hj300EOy2+0BG65JNZfLJyUlacaMGSotLT3m88eMGaNBgwbpscce05tvvqkvv/xS//Vf/6UdO3Zo6tSp6t27t6699lo9//zzmjt3rr788kstXLhQTz75pIYPH65Ro0YpLCxMDz/8sL788ks99NBD+vTTT7V8+XJNmTJF8fHxx+xY17HZbHrkkUf08ccf6/HHH9fnn3+u559/Xm+++eYp/930799fn3/+ud5880198803eu211zR16lTZbLb6+94lKS4uTt9//702bNggwzD0y1/+Unv37tXdd9+tNWvWaN26dbr//vv1/vvvKyMj45TrAgAgWLg+DAAAk82ZM0ebN2/WvHnz6jdiq5OUlKT//u//1l133aWZM2cG7Oxex2636+WXX9bTTz+t5557TpWVlcrIyNBf/vIXDRo0SFLNZeann366li5dqoULFyo1NVX//u//rvvuu6/+hwSTJk1Sp06d9Oc//1n33XefYmJiNGrUKD388MNN3p/+s5/9TGFhYZo/f75WrFih9PR0PfXUUye8rL85fvWrX8nj8ejZZ59VdXW10tLSdO+992rXrl1au3atfD6f7Ha77rnnHs2fP19Tp07VqlWrlJGRoddff11z587VjBkzZBiG0tPT9cILLzTaMA8AACuzGSfaxQUAAAAAAJiGy+MBAAAAALAoQjsAAAAAABZFaAcAAAAAwKII7QAAAAAAWBShHQAAAAAAiyK0AwAAAABgUYR2AAAAAAAsitAOAAAAAIBFEdoBAAAAALAoQjsAAAAAABZFaAcAAAAAwKII7QAAAAAAWNT/Bwg0z7GV9KQHAAAAAElFTkSuQmCC", 225 | "text/plain": [ 226 | "
" 227 | ] 228 | }, 229 | "metadata": {}, 230 | "output_type": "display_data" 231 | } 232 | ], 233 | "source": [ 234 | "import seaborn as sns\n", 235 | "import matplotlib.pyplot as plt\n", 236 | "\n", 237 | "sns.set_theme(context='notebook', style='whitegrid')\n", 238 | "\n", 239 | "# 绘图展示最佳路径\n", 240 | "def plot_route(route, coords):\n", 241 | " plt.figure(figsize=(12, 8))\n", 242 | " \n", 243 | " # 绘制路线\n", 244 | " ax = sns.lineplot(x=coords[route, 0], y=coords[route, 1], marker='o', sort=False)\n", 245 | " \n", 246 | " # 用不同颜色突出显示回到起点的路径\n", 247 | " ax.plot([coords[route[-2]][0], coords[route[0]][0]],\n", 248 | " [coords[route[-2]][1], coords[route[0]][1]], 'r-o')\n", 249 | " \n", 250 | " # 添加城市编号标签\n", 251 | " for i, (x, y) in enumerate(coords):\n", 252 | " plt.text(x, y, f'{i}')\n", 253 | " \n", 254 | " plt.title('TSP Route')\n", 255 | " plt.xlabel('X coordinate')\n", 256 | " plt.ylabel('Y coordinate')\n", 257 | " plt.show()\n", 258 | "\n", 259 | "best_route = decode(best_ind, distance_matrix)\n", 260 | "plot_route(best_route, coordinates)" 261 | ] 262 | } 263 | ], 264 | "metadata": { 265 | "kernelspec": { 266 | "display_name": "Python 3 (ipykernel)", 267 | "language": "python", 268 | "name": "python3" 269 | }, 270 | "language_info": { 271 | "codemirror_mode": { 272 | "name": "ipython", 273 | "version": 3 274 | }, 275 | "file_extension": ".py", 276 | "mimetype": "text/x-python", 277 | "name": "python", 278 | "nbconvert_exporter": "python", 279 | "pygments_lexer": "ipython3", 280 | "version": "3.11.4" 281 | } 282 | }, 283 | "nbformat": 4, 284 | "nbformat_minor": 5 285 | } 286 | --------------------------------------------------------------------------------