今天看到了一个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.js
Array.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();
}