標籤:想法 smd function pivot 最優 神經網路 his 結束 ice
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)
在本例中,該方法簡單而且人工痕迹明顯,很顯然我們的目標是使代價為零,一旦為零,程式就可以停下來了。有時情況並不如此,比如當你在尋找最低代價時,需要用不同的方法結束計算,反之,如果尋找的是適應性最高分值時,可能需要用到其他的條件來停止計算。
價值函數是遺傳演算法中非常重要的內容,因為如果你足夠聰明就能使用它來調和完全不相干的參數。 在本例中,我們只關注字元。但是如果你是在建立一套駕駛導航應用,需要權衡過路費,距離,速度,交通燈,糟糕的鄰車還有橋樑等等情況,把這些完全不相干的參數封裝進統一,優美,整潔的價值函數中來處理,最終依據參數不同的權重獲得路徑資訊。
交配和死亡
交配是生活中的一個常態,我們會在遺傳演算法中大量使用它。交配絕對是一個魔幻時刻,兩段染色體為分享彼此的資訊墜入愛河。從技術層面描述交配就是“交叉”,但是我還是願意稱呼其為“交配”,因為能使所描繪的圖景更加具有直覺性。
到目前為止我們還沒有談到遺傳演算法中的“種群”概念,但是我敢說只要你運行一個遺傳演算法,你某個時刻看到的可不僅僅是一個染色體這麼簡單。你可能會同時擁有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% 。My Code中是讓字元在ASCII碼上移動1,你可以有自己更激進的設定。實驗,測試,學習,這就是唯一的途徑。
染色體:總結
染色體代表你要解決問題的備選方案,他們由表達本身組成(在我們的例子中,是一個長度為13的字串),一個價值或適應性分數以及其函數,交配及突變的能力。
我喜歡把這些東西用OOP的觀念考慮進去,染色體的類可以像下面這樣定義:
屬性:
- Genetic code
- Cost/fitness score
方法:
- Mate
- Mutate
- Calculate Fitness Score
我們現在考慮怎麼讓基因在遺傳演算法的最後一個謎團——種群中互動.
種群
種群就是一組染色體,種群通常會保持相同的尺寸,但是會隨著時間的推移,發展到一個成本更均勻的狀態。
你需要選擇種群大小,我選擇20。你可做任意選擇,10,100或1000,如你所願。當然有優勢也有劣勢,正如我幾次提到的,實驗並自己探索!
種群離不開“代”,一個典型的代可能會包含:
- 為每個染色體計算代價/適應性的分值
- 以代價/適應性分值排序染色體
- 淘汰一定數目的弱染色體
- 讓一定數目的最強的染色體交配
- 隨機突變某些成員
- 某種完整性測試, 如:你怎麼知道該問題得到了結局?
開始和結束
建立一個種群非常簡單,只是讓隨機產生的染色體充滿整個種群即可。在我們的例子中,完全隨機字串的成本分數將會很恐怖,所以在My Code中以平均分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)); }};
很簡單,該類的建構函式接受一個字串作為參數,設定一個價值,一個輔助函數用來產生新的隨機的染色體。
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 方法作為一個輔助函數對種群依據他們的成本分數排序。
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
機器學習之Javascript篇::遺傳演算法介紹