機器學習之Javascript篇:遺傳算法介紹

做者:Burak Kanberjavascript

翻譯:王維強java

原文:http://burakkanber.com/blog/machine-learning-in-other-languages-introduction/程序員


 

遺傳算法應該是我接觸到的機器學習算法中的最後一個,可是我喜歡把它做爲這個系列文章的開始,由於這個算法很是適合介紹「價值函數」或稱「偏差函數」,還有就是局部和全局最優概念,兩者都是機器學習中的重要概念。web

遺傳算法的發明受天然界和進化論的啓發,這對我來講很是酷。這並不奇怪,即便是人工神經網絡(NN)也是從生物學發展起來的,進化是咱們體會到的最好的通用學習算法,咱們都知道人類的大腦是解決通用問題的最好利器。在人工智能和機器學習研究中兩個成長最快的領域,也是咱們生物存在的及其重要的兩個部分,這就是我所感興趣的遺傳算法和神經網絡,如今我把兩者濃縮在一塊兒。算法

我在前面使用的術語「通用」極其重要,對大多數的特別計算問題,你可能會找到比遺傳算法更高效的方案,可是關鍵點不在於具體的實施,也不在於遺傳算法。 使用遺傳算法並非在你遇到複雜的問題時,而是問題的複雜度已經成爲問題,又或者你有一些徹底不相干的參數須要面對。數組

一個典型的應用就是兩足機器人行走問題。能讓機器人靠兩足行走是很是困難的,硬編碼行走程序幾乎不可能成功,即便你真能令機器人走起來,下一個機器人的平衡重心可能會輕微不一樣,也會使你的程序沒法運行。你能夠選擇使用遺傳算法來教會機器人如何學習行走,而不是直接教機器人行走。網絡

咱們這就來用Javascript搭建一個遺傳算法。dom

 

問題

用Javascript寫出一個算法繁殖出一段文本「Hello, World!"。機器學習

對程序員來講「Hello, World!」幾乎是萬物之始,咱們使用遺傳算法繁殖出這段文字也算是師出有名。注意這個問題有很高的人工參與性,固然咱們能夠直接在源碼中打印出「Hello, World!」。不過這看起來很傻,既然已經知道告終果,還要這算法作什麼呢?答案很簡單,這只是個學習的訓練,下一個遺傳算法(使用PHP)將減小人工痕跡,可是咱們總要先開始。函數

 

遺傳算法基礎

算法的基本目的就是生成一串「備選答案」並使用一系列的反饋知道這些備選離最優方案還有多遠。離最優方案最遠的的被淘汰掉,離最優方案近的留下來和其餘備選方案結合並作輕微的突變,一次次修改備選方案並時刻檢查離最優解的距離。

這些「備選答案」稱做染色體。

染色體間結合,產生後代而且突變,優勝劣汰,適者生存,它們產生的後代或許具備更多適應天然選擇的特性。

對於解決「Hello, World!」這樣的問題,如此是否是很詭異?放心吧,遺傳算法毫不是隻善於解決這類問題。

 

染色體

染色體就是一個備選方案的表達,在咱們的例子中,染色體自己就是一段字符,咱們設定全部的染色體都是長度爲13的字符串(Hello, World! 的長度就是13)。下面列出了一些符合備選方案的染色體:

  • Gekmo+ xosmd!
  • Gekln, worle"
  • Fello, wosld!
  • Gello, wprld!
  • Hello, world!

很明顯,最後一個是「正確」(或全局最優的)的染色體,可是咱們如何測量染色體是否優秀呢?

價值函數

價值函數(或偏差函數)是一個測量染色體優秀程度的方法,若是咱們把他們稱爲「適應度函數」,那麼所得分數越高越好,若是咱們使用的是「價值函數」,固然分數越低越好。

在本例中,咱們須要按如下規則定義價值函數:

 

針對字符串的每一個字節,指出備選方案和目標方案之間在ASCII碼上的差值,而後取差值的平方,以確保值爲正數。

 

舉例:若是咱們有個字符「A」 (ASCII 65) ,可是指望的字符應該是「C」(ASCII 67),那麼價值計算的結果就爲4(67 - 65 = 2, 2^2 = 4).

之因此採用平方,就是要確保值爲正數,你固然也能夠取絕對值。爲了加深學習,請在實際操做中靈活應用。

採用這樣的規則,咱們能計算出如下5例染色體的價值:

  • Gekmo+ xosmd! (7)
  • Gekln, worle" (5)
  • Fello, wosld! (5)
  • Gello, wprld! (2)
  • Hello, world! (0)

在本例中,該方法簡單並且人工痕跡明顯,很顯然咱們的目標是使代價(cost)爲零,一旦爲零,程序就能夠停下來了。有時狀況並不如此,好比當你在尋找最低代價時,須要用不一樣的方法結束計算,反之,若是尋找的是適應性最高分值時,可能須要用到其餘的條件來中止計算。

價值函數是遺傳算法中很是重要的內容,由於若是你足夠聰明就能使用它來調和徹底不相干的參數。 在本例中,咱們只關注字符。可是若是你是在創建一套駕駛導航應用,須要權衡過路費,距離,速度,交通燈,糟糕的鄰車還有橋樑等等狀況,把這些徹底不相干的參數封裝進統一,優美,整潔的價值函數中來處理,最終依據參數不一樣的權重得到路徑信息。

 

交配和死亡

交配是生活中的一個常態,咱們會在遺傳算法中大量使用它。交配絕對是一個魔幻時刻,兩段染色體爲分享彼此的信息墜入愛河。從技術層面描述交配就是「交叉」,可是我仍是願意稱呼其爲「交配」,由於能使所描繪的圖景更加具備直覺性。

到目前爲止咱們尚未談到遺傳算法中的「種羣」概念,可是我敢說只要你運行一個遺傳算法,你某個時刻看到的可不只僅是一個染色體這麼簡單。你可能會同時擁有20,100或5000條染色體,就像進化同樣,你可能會傾向於讓那些最強壯的染色體彼此交配,但願獲得的後代比其父母更優秀。

實際上讓字符交配是很是簡單的,好比咱們的例子「Hello,World!」,選取兩段備選字符串(染色體),各自從中間截斷成兩個片斷,這裏你可使用任何方法,若是你願意甚至能夠選取隨機的點位進行截取。咱們就選取中間位置吧,而後用第一段字符串的前半部分和第二段字符串的後半部分合成一個新的染色體(字符串)。繼續用一樣的方法把第二段字符串的前半部分和第一段字符串的後半部分合併成另外一個新的染色體(字符串)。

如下面兩個字符串爲例:

  • Hello, wprld! (1)
  • Iello, world! (1)
從中間斷開經過合併得到兩個新的字符串,也就是兩個新的孩子:
  • Iello, wprld! (2)
  • Hello, world! (0)

如上所見,兩個後代中,有一個包含了父母的最佳特質,簡直完美,另外一個則很是糟糕。

交配就是把基因從父代傳遞到子代的過程。

 

突變

獨自交配會產生一個問題:近親繁殖。若是你只是讓候選者們一代一代地交配下去,你會到達一個「局部最優」的境地並卡在那裏出不來,這個答案雖然看起來還不錯,但並非你想要的「全局最優」。

把基因生活的世界想象成一個物理設定,這裏具備起伏的山峯和溝谷,有那麼一個山谷是這個世界中的最低處,同時也有不少其餘小一些的谷地,偏偏基因被這些較小的谷地圍繞,總體而言還在海平面之上。須要尋找一個解決方案,就像從山頂不一樣的隨機位置滾落一些球,很顯然這些球會卡在某個低處,他們中的不少會被山上的微小凹陷(局部最優)卡住。你的工做就是確保至少有一個球抵達整個世界的最低處:全局最優。既然球是從隨機位置開始滾落的,就很難從開始處掌控過程,幾乎不可能預測哪一個球會被卡在哪裏。可是你能作的是隨機挑選一些球並給他們一腳,可能就是這一腳會幫助他們滾向更低處,想法就是稍微晃動一下系統使得這些球不要在局部最優處停留過久。

這就是突變,這是一個徹底隨機的由你選定一個神祕的未知基因產生必定比例個數的字符隨機變化。

以下例所示,你停在了這兩個染色體上面。

  • Hfllp, worlb!
  • Hfllp, worlb!

沒錯這是一我的爲的案例,但真的會發生。你的兩條染色體如出一轍,意味着他們的子代與父代也如出一轍,什麼進展都沒產生。可是若是100條染色體中有一個在某個字節上發生了突變,如上所示,第二條染色體僅僅發生一個突變,從 "Hfllp, worlb!" 變成了 "Ifllp, worlb!"。那麼進化就會繼續,由於子代和父代間再次產生了差別,是突變推進進化前行。

何時怎麼突變徹底取決於你本身。

再次,咱們開始實驗,後面我所提供的代碼會有高達50%的突變概率,可是這也只是爲了示範目的。你可讓它的突變概率低一些,好比1% 。個人代碼中是讓字符在ASCII碼上移動1,你能夠有本身更激進的設定。實驗,測試,學習,這就是惟一的途徑。

 

染色體:總結

染色體表明你要解決問題的備選方案,他們由表達自己組成(在咱們的例子中,是一個長度爲13的字符串),一個價值或適應性分數以及其函數,交配及突變的能力。

我喜歡把這些東西用OOP的觀念考慮進去,染色體的類能夠像下面這樣定義:

屬性:

  • Genetic code
  • Cost/fitness score

方法:

  • Mate
  • Mutate
  • Calculate Fitness Score

咱們如今考慮怎麼讓基因在遺傳算法的最後一個謎團——種羣中交互.

 

種羣

種羣就是一組染色體,種羣一般會保持相同的尺寸,可是會隨着時間的推移,發展到一個成本更均勻的狀態。

你須要選擇種羣大小,我選擇20。你可作任意選擇,10,100或1000,如你所願。固然有優點也有劣勢,正如我幾回提到的,實驗並本身探索!

種羣離不開「代」,一個典型的代可能會包含:

  • 爲每一個染色體計算代價/適應性的分值
  • 以代價/適應性分值排序染色體
  • 淘汰必定數目的弱染色體
  • 讓必定數目的最強的染色體交配
  • 隨機突變某些成員
  • 某種完整性測試, 如:你怎麼知道該問題獲得瞭解決?

開始和結束

建立一個種羣很是簡單,只是讓隨機產生的染色體充滿整個種羣便可。在咱們的例子中,徹底隨機字符串的成本分數將會很恐怖,因此在個人代碼中以平均分30000的價值分數開始。數目龐大不是問題,這就是進化的目的,也是咱們在這裏的緣由。

知道如何中止種羣繁衍須要一點小技巧,當前的例子很簡單,當價值分數爲0時就中止。但這不老是那麼管用,有時你甚至不知道最小值是什麼,若是用適應性代替的話,你不知道可能的最大值是什麼。

在這些狀況下,你應該指定一個完整的標準,能夠是任何你想要的,可是這裏建議用下面的邏輯跳離算法

 

若是通過一千代的繁衍,最佳值也沒什麼變化,能夠說該值就是答案了,該中止計算了。

這個判斷標準可能意味着你永遠得不到全局最優解,可是不少狀況下你根本不須要獲得全局最優解,足夠接近就好了。

 

代碼

我仍是喜歡OOP方法,固然也喜歡粗曠簡單的代碼。 我會盡量採用簡單直接的策略,即便在某些地方還比較粗糙。

(注意:即便我在上文中把基因改爲了染色體,這裏代碼中仍是使用基因做爲術語,只是語義上有些區別罷了。)

 

var Gene = function(code) {  
        if (code)
                this.code = code;
        this.cost = 9999;
};
Gene.prototype.code = '';  
Gene.prototype.random = function(length) {  
        while (length--) {
                this.code += String.fromCharCode(Math.floor(Math.random()*255));
        }
};

很簡單,該類的構造函數接受一個字符串做爲參數,設定一個「價值」(cost),一個輔助函數用來生成新的隨機的染色體。

Gene.prototype.calcCost = function(compareTo) {  
        var total = 0;
        for(i = 0; i < this.code.length; i++) {
                total += (this.code.charCodeAt(i) - compareTo.charCodeAt(i)) * (this.code.charCodeAt(i) - compareTo.charCodeAt(i));
        }
        this.cost = total;
};

價值函數把「模型」——字符串做爲一個參數,和自身的字符串在ASCII編碼方面作差運算,而後取其平方值。

Gene.prototype.mate = function(gene) {  
        var pivot = Math.round(this.code.length / 2) - 1;

        var child1 = this.code.substr(0, pivot) + gene.code.substr(pivot);
        var child2 = gene.code.substr(0, pivot) + this.code.substr(pivot);

        return [new Gene(child1), new Gene(child2)];
};

交配函數以一個染色體爲參數,找到中間點,以數組的方式返回兩個新的片斷。

Gene.prototype.mutate = function(chance) {  
        if (Math.random() > chance)
                return;

        var index = Math.floor(Math.random()*this.code.length);
        var upOrDown = Math.random()

突變函數把一個浮點值做爲參數,表明染色體的突變概率。

var Population = function(goal, size) {  
  this.members = [];
  this.goal = goal;
  this.generationNumber = 0; while (size--) {   var gene = new Gene(); gene.random(this.goal.length); this.members.push(gene); } };

種羣類中的構造器以目標字符串和種羣大小做爲參數,而後用隨機生成的染色體創建種羣。

Population.prototype.sort = function() {  
  this.members.sort(function(a, b) {
    return a.cost - b.cost;
    });
}

定義一個 Population.prototype.sort 方法做爲一個輔助函數對種羣依據他們的價值(cost)分數排序。

Population.prototype.generation = function() {  
        for (var i = 0; i < this.members.length; i++) {
                this.members[i].calcCost(this.goal);    
        }

        this.sort();
        this.display();
        var children = this.members[0].mate(this.members[1]);
        this.members.splice(this.members.length - 2, 2, children[0], children[1]);

        for (var i = 0; i < this.members.length; i++) {
                this.members[i].mutate(0.5);
                this.members[i].calcCost(this.goal);
                if (this.members[i].code == this.goal) { 
                        this.sort();
                        this.display();
                        return true;
                }
        }
        this.generationNumber++;
        var scope = this;
        setTimeout(function() { scope.generation(); } , 20);
};

種羣的生產方法是最重的部分,其實也沒有什麼魔法。display()方法只是把結果渲染到頁面上,我設置了代際間隔時長,不至於讓事情爆炸般增加。

注意,在本例中我僅僅讓排在最頂端的兩個染色體交配,至於在你本身的實踐中怎麼處理,可多作各類不一樣的嘗試。

 

window.onload = function() {  
        var population  = new Population("Hello, world!", 20);
        population.generation();
};

仍是看實例吧:

http://jsfiddle.net/bkanber/BBxc6/?utm_source=website&utm_medium=embed&utm_campaign=BBxc6

相關文章
相關標籤/搜索