題記:
這幾天,一直在學習人工智能的進化算法,今天想通過一些簡單的介紹,來介紹進化算法中的一種——
遺傳算法(GA)
正文:
首先呢,我們來簡答介紹一下什麼是進化算法:
進化算法:
進化算法(evolutionary algorithms,EA)是基於自然選擇和自然遺傳等生物進化機制的一種搜索算法。生物進化是通過繁殖、變異、競爭和選擇實現的;而進化算法則主要通過選擇、重組和變異這三種操作實現優化問題的求解。進化算法是一個“算法簇”,包括遺傳算法(GA)、遺傳規劃、進化策略和進化規劃等,進化算法的基本框架是遺傳算法所描述的框架。
既然我們已經瞭解到了進化算法的基本框架是遺傳算法所描述的框架,那麼我們就來簡單的講述一下遺傳算法。
遺傳算法:
首先我們來了解一下基本思想:
也就是說,在求解問題時從多個解開始,然後通過一定的法則進行逐步迭代以產生新的解。
我個人認爲遺傳算法就是一種概率型算法,通過不斷的擇優,選擇出最接近最優解的的解(不一定就是最優解,但是可能無限接近)。
而他們到底是如何擇優的呢?
舉個小例子:
例如在一個種羣內有5個人,他們分別叫:
- hello
- hoole
- hallo
- hooal
- oalha
我們可以把這5個人的名字看成5條染色體,這裏他們的名字就可以看做爲他們的染色體。
如果我們想培育出染色體爲hello的人的話,我們是不是需要先選擇出染色體最接近hello的人呢?(或者說名字最像hello的人)來組成下一代,當然了我們可以一眼看出第一項hello,他的染色體就是最接近hello的,但是,這個種羣中還有存在交叉與變異的可能性。
只能暫且說,單在選擇下,第一項的生存下來成爲第二代種羣的概率比較大,而其他人也可能發生交叉與變異,他們也有可能孕育出染色體爲hello的人,只是概率比較小而已。但不代表他們不可能成爲第二代種羣。
那麼下面有幾個問題:
- 如何選擇?
- 如何交叉?
- 如何變異?
1.對於選擇,我們就引入了適應度的概念,適應度越高,就越有可能活下去成爲下一代種羣中的一份子,我們在選擇個體中有很多方法:輪盤賭選擇,錦標賽選擇方法,Boltzmann錦標賽選擇,最佳個體保存方法等。
我們選擇比較簡單的輪盤賭方法:
又稱比例選擇方法.其基本思想是:各個個體被選中的概率與其適應度大小成正比.
舉個例子:
如果第一輪我們產生一個隨機數爲0.13,0.13<=0.14 我們選擇第一個
第二輪我們產生一個隨機數爲0.65, 0.65<=0.69 則我們選擇第三個
這就是輪盤賭的簡單原理
2.對於交叉,存在很多:一點交叉、二點交叉、均勻交叉等
我們選擇比較簡單的一點交叉:
舉個例子:
兩個字符串
var str1 = hello;
var str2 = world;
假設他們兩個相鄰,字符串長度都爲5,我隨機選擇選一個交叉點,我選擇第0位進行交叉
兩者交叉後,變爲
horld
wello
即第一位以後的內容發送交換
3.對於變異,同樣存在:位點變異,逆轉變異,插入變異,交換變異等
我們選擇的是位點變異,也就是說針對於某一位進行變異
舉個例子:
10010
如果第2位發生變異,第2位爲 0 變異爲 1 ,結果如下:
10110
對於遺傳算法的基礎知識我們就介紹到這裏。
那麼問題就來了,也可以引出我們今天的主題:如何用JavaScript來實現這樣的選擇呢?
基於JavaScript的遺傳算法的實現
我們來利用遺傳算法解決一個實際問題:
二進制編碼也就是指用二進制數來代替我們的染色體,這個染色體的範圍在十進制數內是0-30
編碼長度爲5,也就是指5位
首先,我們要初始化一些變量:
var totalNum = 10, // 種羣中染色體的總數
bit = 5, // 基因數爲5位
total = new Array(); // 種羣中的染色體
bestFitness = 0, // 最佳適應值
generation = 0, // 染色體代號
bestGeneration = 0, // 最好的一代染色體的代號
bestStr = ''; // 最好的染色體基因序列
接下來,我們就要初始化一條染色體,如何定義一條染色體呢?我們可以用一個二進制字符串來代表一條染色體,通過隨機數,隨機生成長度爲5的字符串
/**
* 初始化一條染色體
*/
function initChar() {
var res = '';
for(var i=0;i<bit;i++){
if (Math.random() > 0.5) { // 隨機創建一個二進制長度爲5的字符串
res += '0';
}
else{
res += '1';
}
}
return res;
}
接下來初始化一個種羣,也就是根據我種羣的大小,對應生成多少條染色體
/* 初始化一個種羣 */
function initTotal() {
for(var i=0;i<totalNum;i++){
total[i] = initChar()
}
return total;
}
接下來我們就要將染色體轉化爲十進制的數,進行運算,算出對應的值與適應度
/* 將染色體轉化爲十進制的值 */
function calculateFitness(string) {
var a = parseInt(string,2);
var x = a*30/(Math.pow(2,bit) - 1); // 二進制編碼與參數的關係,一個默認公式
var fitness = Math.pow(x,3)-60*Math.pow(x,2)+900*x+100; // 根據題目中的公式來算
var result = new Array();
result.push({x,fitness});
return result;
}
之後就要進入到我們的選擇函數了
/* 輪盤選擇 */
function select() {
var evals = new Array(totalNum); // 所有染色體適應值
var p = new Array(totalNum); // 各染色體選擇概率
var q = new Array(totalNum); // 累計概率
var F = 0; // 累計適應值總合
for(var i=0;i<totalNum;i++){ // 記錄下種羣的最優解
evals[i] = calculateFitness(total[i]);
if(evals[i][0].fitness > bestFitness) {
bestFitness = evals[i][0].fitness;
bestGeneration = generation;
bestStr = total[i];
}
F += evals[i][0].x;
}
for(var j=0;j<totalNum;j++){ // 計算累計概率
p[j] = evals[j][0].x/F;
if(j == 0){
q[j] = p[j];
}
else{
q[j]=q[j-1]+p[j];
}
}
var temp = new Array(10);
for(var k=0;k<totalNum;k++){ //將新的一代,每一個產生的內容放到一個臨時變量temp中,選10次
var r = Math.random();
if(r <= q[0]){ // 這個就是根據累計概率來算出到底取哪一個(選擇的關鍵點),每一次選的結果
temp[k] = total[0];
}
else{
for(var z=1;z<totalNum;z++){
if(r<q[z]){
temp[k] = total[z];
break;
}
}
}
}
total = temp; // 最後將完全生成好的一代
}
接下來是發生交叉,如何交叉,是每一條染色體都與前後的進行交叉
/* 交叉 設概率爲70%*/
function cross() {
var temp1,
temp2;
for(var i=0;i<totalNum;i++){ // 設共有10條,1與2判斷是否可能交叉,2與3....最後是1與10
if(Math.random() < 0.7){ // 判斷每一次是否發生了交叉
// 字符串的切割
var pos = Math.ceil(Math.random()*bit); // 向上取整
temp1 = total[i].substring(0,pos) + total[(i+1)%totalNum].substring(pos);
temp2 = total[(i+1)%totalNum].substring(0,pos)+total[i].substring(pos);
}
}
}
接下來是變異,首先我們要看這個種羣中10條染色體,每一條是否會發生變異,如果變異,再看是第幾位發生變異。然後通過看第幾位是0或1,如果是1變異爲0,如果是0變異爲1
/* 變異,百分之一的機率出現變異*/
function mutation() {
var s = new Array();
for(var i=0;i<totalNum;i++){
s = total[i];
if(Math.random()<0.01){
var temp = Math.ceil(Math.random()*bit); // 向上取整,表示第幾位
if(total[i].charAt(temp) == '0'){
changeNode = total[i].split(''); //將changeNode 字符串轉換成數組
changeNode.splice(temp,1,'1'); //將1這個位置的字符,替換成'1'. 用的是原生js的splice方法。
changeNode.join('');
}
else{
changeNode = total[i].split(''); //將changeNode 字符串轉換成數組
changeNode.splice(temp,1,'0'); //將1這個位置的字符,替換成'0'. 用的是原生js的splice方法。
changeNode.join('');
}
}
}
最後就是執行,進行多少代的優勝劣汰
/*執行*/
document.getElementById('button').onclick = function () {
total = initTotal();
for(var i=0;i<10000;i++){
select();
cross();
mutation();
generation = i;
}
var x = calculateFitness(bestStr);
console.log(bestFitness);
console.log(x[0]);
}
完整代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>遺傳算法</title>
</head>
<body>
<button type="button" id="button">計算結果</button>
<h1>計算結果在控制檯輸出:</h1>
<script>
var totalNum = 10, // 種羣中染色體的總數
bit = 5, // 基因數爲5位
total = new Array(); // 種羣中的染色體
bestFitness = 0, // 最佳適應值
generation = 0, // 染色體代號
bestGeneration = 0, // 最好的一旦染色體代號
bestStr = ''; // 最好的染色體基因序列
/**
* 初始化一條染色體
*/
function initChar() {
var res = '';
for(var i=0;i<bit;i++){
if (Math.random() > 0.5) { // 隨機創建一個二進制長度爲5的字符串
res += '0';
}
else{
res += '1';
}
}
return res;
}
/* 初始化一個種羣 */
function initTotal() {
for(var i=0;i<totalNum;i++){
total[i] = initChar()
}
return total;
}
/* 將染色體轉化爲十進制的值 */
function calculateFitness(string) {
var a = parseInt(string,2);
var x = a*30/(Math.pow(2,bit) - 1);
var fitness = Math.pow(x,3)-60*Math.pow(x,2)+900*x+100;
var result = new Array();
result.push({x,fitness});
return result;
}
/* 輪盤選擇 */
function select() {
var evals = new Array(totalNum); // 所有染色體適應值
var p = new Array(totalNum); // 各染色體選擇概率
var q = new Array(totalNum); // 累計概率
var F = 0; // 累計適應值總合
for(var i=0;i<totalNum;i++){ // 記錄下種羣的最優解
evals[i] = calculateFitness(total[i]);
if(evals[i][0].fitness > bestFitness) {
bestFitness = evals[i][0].fitness;
bestGeneration = generation;
bestStr = total[i];
}
F += evals[i][0].x;
}
for(var j=0;j<totalNum;j++){ // 計算累計概率
p[j] = evals[j][0].x/F;
if(j == 0){
q[j] = p[j];
}
else{
q[j]=q[j-1]+p[j];
}
}
var temp = new Array(10);
for(var k=0;k<totalNum;k++){ //
var r = Math.random();
if(r <= q[0]){
temp[k] = total[0];
}
else{
for(var z=1;z<totalNum;z++){
if(r<q[z]){
temp[k] = total[z];
break;
}
}
}
}
total = temp;
}
/* 交叉 設概率爲70%*/
function cross() {
var temp1,
temp2;
for(var i=0;i<totalNum;i++){
if(Math.random() < 0.7){
var pos = Math.ceil(Math.random()*bit); // 向上取整
temp1 = total[i].substring(0,pos) + total[(i+1)%totalNum].substring(pos);
temp2 = total[(i+1)%totalNum].substring(0,pos)+total[i].substring(pos);
}
}
}
/* 變異,百分之一的機率出現變異*/
function mutation() {
var s = new Array();
for(var i=0;i<totalNum;i++){
s = total[i];
if(Math.random()<0.01){
var temp = Math.ceil(Math.random()*bit); // 向上取整
if(total[i].charAt(temp) == '0'){
changeNode = total[i].split('');
changeNode.splice(temp,1,'1');
changeNode.join('');
}
else{
changeNode = total[i].split('');
changeNode.splice(temp,1,'0');
changeNode.join('');
}
}
}
}
/*執行*/
document.getElementById('button').onclick = function () {
total = initTotal();
for(var i=0;i<10000;i++){
select();
cross();
mutation();
generation = i;
}
var x = calculateFitness(bestStr);
console.log(bestFitness);
console.log(x[0]);
}
</script>
</body>
</html>
供大家參考學習,之後還會更新相關算法。