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


作者:Burak Kanber

翻譯:王維強

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


 

遺傳算法應該是我接觸到的機器學習算法中的最后一個,但是我喜歡把它作為這個系列文章的開始,因為這個算法非常適合介紹“價值函數”或稱“誤差函數”,還有就是局部和全局最優概念,二者都是機器學習中的重要概念。

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

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

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

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

 

問題

用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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM