我們有一個必須在n個城市之間旅行的推銷員。他不在乎什么順序。他最先或最后訪問的城市除外。他唯一關心的是他會去拜訪每一個人,每個城市只有一次,最后一站是他得家。
每個城市都是一個節點,每個節點通過一條邊與其他封閉節點相連(可以將其想象成公路、飛機、火車、汽車等)
每個連接都有一個或多個權值與之相關,我們稱之為成本。
成本描述了沿着該連接旅行的困難程度,如機票成本、汽車所需的汽油量等。
他的首要任務是盡可能降低成本和旅行距離。
對於那些學過或熟悉圖論的人,希望你們還記得無向加權圖。
城市是頂點,路徑是邊,路徑距離是邊的權值。本質上,我們有一個最小化的問題,即在訪問了其他每個頂點一次之后,從一個特定的頂點開始和結束。實際上,當我們完成的時候,可能會得到一個完整的圖,其中每一對頂點都由一條邊連接起來。
接下來,我們必須討論不對稱和對稱的問題,因為這個問題最終可能是其中之一。到底是什么意思?我們有一個非對稱旅行推銷員問題或者一個對稱旅行推銷員問題。這完全取決於兩座城市之間的距離。如果每個方向上的距離相等,我們有一個對稱的旅行推銷員問題,對稱性幫助我們得到可能的解。如果兩個方向上的路徑不存在,或者距離不同,我們就有一個有向圖。下圖顯示了前面的描述:
旅行推銷員問題可以是對稱的,也可以是非對稱的。讓我們從對將要發生的事情從最簡描述開始。
在生物界,當我們想要創建一個新的基因型時,我們會從父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函數的樣子,我們可以看到發生了什么。基本上,該方法找到獲勝的神經元(權重值最接近指定輸入向量的神經元)。然后更新它的權重以及相鄰神經元的權重:
這個方法的兩個主要功能是計算網絡和獲得獲勝者(突出顯示的項目)。
現在,簡要介紹一下我們可以在屏幕上輸入的參數。
學習速率
學習速率是決定學習速度的一個參數。更正式地說,它決定了我們根據損失梯度調整網絡權重的程度。如果太低,我們沿着斜坡向下的速度就會變慢。即使我們希望有一個較低的學習率,這可能意味着我們將需要很長時間來達到趨同。學習速率也會影響我們的模型收斂到最小值的速度。
在處理神經元時,它決定了有權重用於訓練的神經元的獲取時間(對新體驗做出反應所需的時間)。
學習半徑
學習半徑決定了獲勝神經元周圍要更新的神經元數量。在學習過程中,半徑圓內的任何神經元都會被更新。神經元越靠近,發生的更新就越多。距離越遠,數量越少。
總結
在這一章中,我們學習了神經元,還學習了著名的旅行推銷員問題,它是什么,以及我們如何用電腦解決它。這個小例子在現實世界中有着廣泛的應用。
在下一章中,我們將回答我們所有開發人員都面臨的問題:我應該接受這份工作嗎?