今天看到了一個JS程式,來源程式為:https://github.com/parano/GeneticAlgorithm-TSP,樣本見:
http://parano.github.io/GeneticAlgorithm-TSP/
覺得這個程式寫的非常好,仔細閱讀源碼之後,認真做了筆記,在此記錄。 初始化部分 計算距離
距離計算在countDistances()函數中完成。
函數把距離儲存在dis變數中。
dis變數是一個二維數組,其中的[i][j]元素是點i和點j的距離。
關鍵語句:
for (var j = 0; j < length; j++) { // >> 0 是為了取整 dis[i][j] = distance(points[i], points[j]) >> 0;}
其中,distance函數在utils.js中定義,定義為:
function distance(p1, p2) { return euclidean(p1.x - p2.x, p1.y - p2.y);}function euclidean(dx, dy) { return Math.sqrt(dx*dx, dy*dy);}
至於points數組,則是在main.js中初始化,可以從剛剛的程式中看出,數組中的元素都是對象,有x和y屬性。
在main.js的init_mouse()中,可以看到其初始化:
points.push(new Point(x, y));// utils.js中有Point的定義function Point(x, y) { this.x = x; this.y = y;}產生種群
單獨的DNA是由ramdomIndivial函數產生的,其實現為:
function randomIndivial(n) { var a = []; for (var i = 0; i < n; i++) { a.push(i); } return a.shuffle();}
這是一個隨機的排列,意思是一條隨機的路徑(到達節點的先後順序)。其中的shuffle函數在utils.js中有定義,這個定義是經驗代碼,因此高效、可重用但是不是很具有可讀性:
Array.prototype.shuffle = function() { for (var j, x, i = this.length-1; i; j = randomNumber(i), x = this[--i], this[i] = this[j], this[j] = x); return this;};function randomNumber(boundary) { return parseInt(Math.random() * boundary);}
現在可以隨機產生一個個體DNA了,產生一定量的隨機個體,就可以成為一個種群了:
for (var i = 0; i < POPULATION_SIZE; i++) { population.push(randomIndivial(points.length));}
這段代碼產生了population變數,這是一個二維數組,有POPULATION_SIZE個元素,其中每個元素是一個長度為節點個數的數組,這些數組是隨機的節點排列,代表路徑。 得到適應值
遺傳演算法當有優勝劣汰,因此對於每個個體應該有一個數字來衡量個體對於自然界的適應程度。在TSP問題中,自然是路徑長度了,路徑越短,越有優勢。這個值由setBestValue函數得到:
function setBestValue() { for (var i = 0; i < population.length; i++) { //為種群中的每個個體計算適應度 //並將其儲存在values的對應位置 values[i] = evaluate(population[i]); } //得到最佳路徑和值 currentBest = getCurrentBest(); if (bestValue == undefined || bestValue > currentBest.bestValue) { best =population [currentBest.bestPosition].clone(); bestValue = currentBest.bestValue; UNCHANGED_GENS = 0; } else { UNCHANGED_GENS += 1; }}function getCurrentBest() { // 最佳個體位置和值 var bestP = 0; currentBestValue = values[0]; for (var i = 1; i < population.length; i++) { if (values[i] < currentBestValue) { currentBestValue = values[i]; bestP = i; } } return { bestPosition : bestP, bestValue : currentBestValue }}// 計算個體適應度即路徑(迴路)長度function evaluate(indivial) { var sum= dis [indivial[0]][indivial[indivial.length - 1]]; for (var i = 1; i < indivial.length; i++) { sum += dis[indivial[i]][indivial[i-1]]; } return sum;}優勝劣汰、自然選擇
現在開始正式的演算法。第一步是選擇出適應度比較高的個體(DNA),然後讓他們優先產出下一代。
適應度高的個體遺傳的可能性大,因此有初始化一個轉盤,轉盤上不同的地區(扇形)代表不同的個體。轉盤上的指標隨機轉動。個體適應度越高,對應的地區的圓心角就越大,也就越有可能被選中。
轉盤的設定由setRoulette函數完成:
function setRoulette() { // 計算所有個體適應度: 路徑長度的倒數 for (var i = 0; i < values.length; i++) { fitnessValues[i] = 1.0/values[i]; } // 設定轉盤 var sum = 0; for (var i = 0; i < fitnessValues.length; i++) { sum += fitnessValues[i]; } for (var i = 0; i < roulette.length; i++) { roulette[i] = fitnessValues[i] / sum; } // 這是一個機率分布 // 可以從wheelOut中看出這樣設定的原因 for (var i = 1; i < roulette.length; i++) { roulette[i] += roulette[i-1]; }}function wheelOut(rand) { var i; for (i = 0; i < roulette.length; i++) { if (rand <= roulette[i]) { return i; } }}// 在main.js的initData函數中有roulette的定義roulette = new Array(POPULATION_SIZE);
設定出轉盤後,開始選擇個體,由selection函數完成:
function selection() { var parents = new Array(); var initnum = 4; parents.push (population[currentBest.bestPosition]); parents.push(doMutate(best.clone())); parents.push(pushMutate(best.clone())); parents.push(best.clone()); setRoulette(); for (var i = initnum; i < POPULATION_SIZE; i++) { parents.push(population[wheelOut(Math.random())]); } population = parents;}
parents的前4項分別是:最佳個體,最佳個體的兩種變異,最佳個體。後面的按照轉盤的方式隨機播放。
第一種變異是doMutate,方法是在DNA中間選一段,將其翻轉。第二種變異是pushMutate,方法是把DNA截成三段,交換順序。實現代碼為:
function doMutate(seq) { // 全域變數,記錄變異次數 mutationTimes++; do { m = randomNumber(seq.length - 2); n = randomNumber(seq.length); } while (m >= n); for (var i = 0, j = (n - m + 1) >> 1; i < j; i++) { seq.swap(m + i, n - i); } return seq;}function pushMutate(seq) { mutationTimes++; var m, n; do { m = randomNumber(seq.length >> 1); n = randomNumber(seq.length); } while (m >= n); var s1 = seq.slice(0, m); var s2 = seq.slice(m, n); var s3 = seq.slice(n, seq.length); return s2.concat(s1).concat(s3).clone();}// utils.jsArray.prototype.swap = function(x, y) { if (x > this.length || y > this.length || x === y) { return; } var tem = this[x]; this[x] = this[y]; this[y] = tem;}遺傳變異
下面是演算法最關鍵的部分:兩個個體如何交配產生下一代。下一代應該有兩個父本的特徵,以不斷朝最佳方向進化。
先看幾個為數組添加的方法:
Array.prototype.indexOf = function (value) { for (var i = 0; i < this.length; i++) { if (this[i] === value) { return i; } }}Array.prototype.deleteByValue = function (value) { var pos = this.indexOf(value); this.splice(pos, 1);}Array.prototype.next = function (index) { if (index === this.length - 1) { return this[0]; } else { return this[index + 1]; }}Array.prototype.previous = function (index) { if (index === 0) { return this[this.length - 1]; } else { return this[index - 1]; }}
這些方法分別是按值找下標,按值刪除,返回下一個和返回前一個。
交配的代碼為:
// x, y 是父母的下標function doCrossover(x, y) { child1 = getChild('next', x, y); child2 = getChild('previous', x, y); population[x] = child1; population[y] = child2;}function getChild(fun, x, y) { solution = new Array(); var px = population[x].clone(); var py = population[y].clone(); var dx, dy; var c = px[randomNumber(px.length)]; solution.push(c); while(px.length > 1) { dx = px[fun](px.indexOf(c)); dy = py[fun](py.indexOf(c)); px.deleteByValue(c); py.deleteByValue(c); c = dis[c][dx] < dis[c][dy] ? dx : dy; solution.push(c); } return solution;}
即:先從父親DNA(節點)中任選一點c,放在下一代中,然後不斷向後找,並更新c為最近的下一跳(或者上一跳),直到找完。
種群的全體交配由crossover函數完成:
function crossover() { var queue = new Array(); for (var i = 0; i < POPULATION_SIZE; i++) { if (Math.random() < CROSSOVER_PROBABILITY) { queue.push(i); } } queue.shuffle(); for (var i = 0; i < queue.length - 1; i+= 2) { doCrossover(queue[i], queue[i+1]); }}變異
為了防止演算法收斂於錯誤結果,演算法需要變異的功能。變異由mutation函數完成:
function mutation() { for (var i = 0; i < POPULATION_SIZE; i++) { if (Math.random() < MUTATION_PROBABILITY) { if (Math.random() > 0.5) { population[i] = pushMutate(population[i]); } else { population[i] = doMutate(population[i]); } i--; } }}
兩種變異方法隨機播放。 演算法總覽
演算法的總體步驟為:
GAInitialize();function GANextGeneration() { currentGeneration++; selection(); crossover(); multation(); setBestValue();}