咱们有一个必须在n个城市之间旅行的推销员。他不在意什么顺序。他最早或最后访问的城市除外。他惟一关心的是他会去拜访每个人,每一个城市只有一次,最后一站是他得家。算法
每一个城市都是一个节点,每一个节点经过一条边与其余封闭节点相连(能够将其想象成公路、飞机、火车、汽车等)编程
每一个链接都有一个或多个权值与之相关,咱们称之为成本。数组
成本描述了沿着该链接旅行的困难程度,如机票成本、汽车所需的汽油量等。网络
他的首要任务是尽量下降成本和旅行距离。框架
对于那些学过或熟悉图论的人,但愿大家还记得无向加权图。dom
城市是顶点,路径是边,路径距离是边的权值。本质上,咱们有一个最小化的问题,即在访问了其余每一个顶点一次以后,从一个特定的顶点开始和结束。实际上,当咱们完成的时候,可能会获得一个完整的图,其中每一对顶点都由一条边链接起来。函数
接下来,咱们必须讨论不对称和对称的问题,由于这个问题最终多是其中之一。究竟是什么意思?咱们有一个非对称旅行推销员问题或者一个对称旅行推销员问题。这彻底取决于两座城市之间的距离。若是每一个方向上的距离相等,咱们有一个对称的旅行推销员问题,对称性帮助咱们获得可能的解。若是两个方向上的路径不存在,或者距离不一样,咱们就有一个有向图。下图显示了前面的描述: 工具
旅行推销员问题能够是对称的,也能够是非对称的。让咱们从对将要发生的事情从最简描述开始。学习
在生物界,当咱们想要建立一个新的基因型时,咱们会从父a那里取一点,从父b那里取一点。这叫作交叉突变。在这以后,这些基因型就会受到轻微的干扰或改变。这被称为突变。这就是遗传物质产生的过程。测试
接下来,咱们删除原始代,代之以新的代,并测试每一个基因型。更新的基因型,做为其先前组成部分的更好部分,如今将向更高的适应度倾斜;平均而言,这一代人的得分应该高于上一代人。
这一过程将持续许多代,随着时间的推移,人口的平均适应度将不断进化和提升。在现实生活中,这并不老是有效的,但通常来讲,它是有效的。
在后面会有一个遗传算法编程的讲解,以便让咱们深刻研究咱们的应用程序。
下面是咱们的示例应用程序。它是基于Accord.NET框架的。在定义了须要访问的房屋数量以后,只需单击生成按钮:
在咱们的测试应用程序中,咱们能够很是容易地更改咱们想要访问的房屋的数量,如高亮显示的区域所示。
咱们能够获得一个很是简单的空间问题或者更复杂的空间问题。这是一个很是简单的空间问题的例子:
这是一个更复杂的空间问题的例子:
最后,设置咱们但愿算法使用的迭代总数。点击计算路线按钮,假设一切顺利,咱们的地图看起来应该像这样:
让咱们看看当咱们选择咱们想要的城市数量,而后点击生成按钮,会发生什么:
/// <summary> /// 从新生成地图 /// </summary> private void GenerateMap() { Random rand = new Random((int)DateTime.Now.Ticks); // 建立坐标数组 map = new double[citiesCount, 2]; for (int i = 0; i < citiesCount; i++) { map[i, 0] = rand.Next(1001); map[i, 1] = rand.Next(1001); } //设置地图 chart.UpdateDataSeries("cities", map); //删除路径 chart.UpdateDataSeries("path", null); }
咱们要作的第一件事就是初始化随机数生成器并对其进行种子化。接下来,咱们获得用户指定的城市总数,而后从中建立一个新数组。最后,咱们绘制每一个点并更新地图。这张地图是来自Accord.NET的图表控件,它将为咱们提供大量可视化绘图。完成这些以后,咱们就能够计算路径并解决问题了。
接下来,让咱们看看咱们的主要搜索解决方案是什么样的:
// 建立网络 DistanceNetwork network = new DistanceNetwork(2, neurons); // 设置随机发生器范围 foreach (var neuron in network.Layers.SelectMany(layer => layer?.Neurons).Where(neuron => neuron != null)) { neuron.RandGenerator = new UniformContinuousDistribution(new Range(0, 1000)); } // 建立学习算法 ElasticNetworkLearning trainer = new ElasticNetworkLearning(network); double fixedLearningRate = learningRate / 20; double driftingLearningRate = fixedLearningRate * 19; double[,] path = new double[neurons + 1, 2]; double[] input = new double[2]; int i = 0; while (!needToStop) { // 更新学习速度和半径 trainer.LearningRate = driftingLearningRate * (iterations - i) / iterations + fixedLearningRate; trainer.LearningRadius = learningRadius * (iterations - i) / iterations; // 设置网络输入 int currentCity = rand.Next(citiesCount); input[0] = map[currentCity, 0]; input[1] = map[currentCity, 1]; // 运行一个训练迭代 trainer.Run(input); // 显示当前路径 for (int j = 0; j < neurons; j++) { path[j, 0] = network.Layers[0].Neurons[j].Weights[0]; path[j, 1] = network.Layers[0].Neurons[j].Weights[1]; } path[neurons, 0] = network.Layers[0].Neurons[0].Weights[0]; path[neurons, 1] = network.Layers[0].Neurons[0].Weights[1]; chart.UpdateDataSeries("path", path); i++; SetText(currentIterationBox, i.ToString()); if (i >= iterations) break; }
如今咱们已经解决了问题,让咱们看看是否能够应用咱们在前面关于自组织映射(SOM)一章中学到的知识,从不一样的角度来处理这个问题。
咱们将使用一种叫作弹性网络训练的技术来解决咱们遇到的问题,这是一种很好的无监督的方法。
首先简单介绍一下什么是弹性映射。
弹性映射为建立非线性降维提供了一种工具。它们是数据空间中的弹性弹簧系统,近似于低维流形。利用这种能力,咱们能够从彻底无结构聚类(无弹性)到更接近线性主成分分析流形(高弯曲/低拉伸)的弹簧。
在使用咱们的示例应用程序时,您将看到这些线并不必定像在之前的解决方案中那样僵硬。在许多状况下,它们甚至不可能进入咱们所访问的城市的中心(这条线从中心生成),而是只接近城市边界的边缘,如前面的示例所示。
接下来,介绍下神经元。此次咱们将有更多的控制,经过指定咱们的学习速率和半径。与前面的示例同样,咱们将可以指定销售人员今天必须访问的城市总数。
首先,咱们将访问50个城市,使用0.3的学习率和0.75的半径。最后,咱们将运行50,000次迭代(不用担忧,这很快的)。咱们的输出是这样的:
如今,若是咱们改变半径为不一样的值,好比0.25,会发生什么?注意咱们在一些城市之间的角度变得更加明显:
接下来,咱们将学习率从0.3改成0.75:
尽管获得路线最终看起来很是类似,但有一个重要的区别。在前面的示例中,直到全部迭代完成,才绘制销售人员的路由路径。
咱们所作的第一件事就是建立一个DistanceNetwork对象。这个对象只包含一个DistanceLayer,它是一个距离神经元的单层。距离神经元将其输出计算为其权值与输入值之间的距离,即权值与输入值之间的绝对差值之和。全部这些组成了SOM,更重要的是,咱们的弹性网络。
接下来,咱们必须用一些随机权值来初始化咱们的网络。咱们将为每一个神经元建立一个均匀连续的分布。均匀连续分布,或称矩形分布,是一种对称的几率分布,对于族中的每个成员,在分布的支撑点上相同长度的全部区间具备相同的几率。你一般会看到这写成U(a, b)参数a和b分别是最小值和最大值。
// 设置随机发生器范围 foreach (var neuron in network.Layers.SelectMany(layer => layer?.Neurons).Where(neuron => neuron != null)) { neuron.RandGenerator = new UniformContinuousDistribution(new Range(0, 1000)); }
接下来,咱们建立弹性学习对象,它容许咱们训练咱们的距离网络:
// 建立学习算法 ElasticNetworkLearning trainer = new ElasticNetworkLearning(network);
下面是ElasticNetworkLearning构造函数内部的样子:
如今咱们计算学习速率和半径:
double fixedLearningRate = learningRate / 20; double driftingLearningRate = fixedLearningRate * 19;
最后,进入咱们的主循环:
while (!needToStop) { // 更新学习速度和半径 trainer.LearningRate = driftingLearningRate * (iterations - i) / iterations + fixedLearningRate; trainer.LearningRadius = learningRadius * (iterations - i) / iterations; // 设置网络输入 int currentCity = rand.Next(citiesCount); input[0] = map[currentCity, 0]; input[1] = map[currentCity, 1]; // 运行一个训练迭代 trainer.Run(input); // 显示当前路径 for (int j = 0; j < neurons; j++) { path[j, 0] = network.Layers[0].Neurons[j].Weights[0]; path[j, 1] = network.Layers[0].Neurons[j].Weights[1]; } path[neurons, 0] = network.Layers[0].Neurons[0].Weights[0]; path[neurons, 1] = network.Layers[0].Neurons[0].Weights[1]; chart.UpdateDataSeries("path", path); i++; SetText(currentIterationBox, i.ToString()); if (i >= iterations) break; }
在前面的循环中,训练器每次循环增量运行一个epoch(迭代)。这是trainer.Run函数的样子,咱们能够看到发生了什么。基本上,该方法找到获胜的神经元(权重值最接近指定输入向量的神经元)。而后更新它的权重以及相邻神经元的权重:
这个方法的两个主要功能是计算网络和得到获胜者(突出显示的项目)。
如今,简要介绍一下咱们能够在屏幕上输入的参数。
学习速率是决定学习速度的一个参数。更正式地说,它决定了咱们根据损失梯度调整网络权重的程度。若是过低,咱们沿着斜坡向下的速度就会变慢。即便咱们但愿有一个较低的学习率,这可能意味着咱们将须要很长时间来达到趋同。学习速率也会影响咱们的模型收敛到最小值的速度。
在处理神经元时,它决定了有权重用于训练的神经元的获取时间(对新体验作出反应所需的时间)。
学习半径决定了获胜神经元周围要更新的神经元数量。在学习过程当中,半径圆内的任何神经元都会被更新。神经元越靠近,发生的更新就越多。距离越远,数量越少。
在这一章中,咱们学习了神经元,还学习了著名的旅行推销员问题,它是什么,以及咱们如何用电脑解决它。这个小例子在现实世界中有着普遍的应用。
在下一章中,咱们将回答咱们全部开发人员都面临的问题:我应该接受这份工做吗?