用遺傳算法優化垃圾收集策略

做者|Andrew Kuo
編譯|VK
來源|Towards Data Sciencepython

遺傳算法是一個優化技術,在本質上相似於進化過程。這多是一個粗略的類比,但若是你眯着眼睛看,達爾文的天然選擇確實大體上相似於一個優化任務,其目的是製造出徹底適合在其環境中繁衍生息的有機體。git

在本文中,我將展現如何在Python中實現一個遺傳算法,在幾個小時內「進化」一個收集垃圾的機器人。github

背景

我所遇到的遺傳算法原理最好的教程來自Melanie Mitchell寫的一本關於複雜系統的好書《Complexity: A Guided Tour》。算法

在其中一個章節中,Mitchell介紹了一個名叫Robby的機器人,他在生活中的惟一目的是撿垃圾,並描述瞭如何使用GA優化Robby的控制策略。下面我將解釋我解決這個問題的方法,並展現如何在Python中實現該算法。有一些很好的包能夠用來構造這類算法(好比DEAP),可是在本教程中,我將只使用基本Python、Numpy和TQDM(可選)。app

雖然這只是一個玩具的例子,但GAs在許多實際應用中都有使用。做爲一個數據科學家,我常常用它們來進行超參數優化和模型選擇。雖然GAs的計算成本很高,但GAs容許咱們並行地探索搜索空間的多個區域,而且在計算梯度時是一個很好的選擇。dom

問題描述

一個名爲Robby的機器人生活在一個充滿垃圾的二維網格世界中,周圍有4堵牆(以下圖所示)。這個項目的目標是發展一個最佳的控制策略,使他可以有效地撿垃圾,而不是撞牆。機器學習

Robby只能看到他周圍上下左右四個方塊以及他所在的方塊,每一個方塊有3個選擇,空的,有垃圾,或者是一面牆。所以,Robby有3⁵=243種不一樣的狀況。Robby能夠執行7種不一樣的動做:上下左右的移動(4種)、隨機移動、撿拾垃圾或靜止不動。ide

所以,Robby的控制策略能夠編碼爲一個「DNA」字符串,由0到6之間的243位數字組成(對應於Robby在243種可能的狀況下應該採起的行動)。post

方法

任何GA的優化步驟以下:性能

  1. 生成問題初始隨機解的「種羣」

  2. 個體的「擬合度」是根據它解決問題的程度來評估的

  3. 最合適的解決方案進行「繁殖」並將「遺傳」物質傳遞給下一代的後代

  4. 重複第2步和第3步,直到咱們獲得一組優化的解決方案

在咱們的任務中,你建立了第一代Robbys初始化爲隨機DNA字符串(對應於隨機控制策略)。而後模擬讓這些機器人在隨機分配的網格世界中運行,並觀察它們的性能。

擬合度

機器人的擬合度取決於它在n次移動中撿到多少垃圾,以及它撞到牆上多少次。在咱們的例子中,機器人每撿到一塊垃圾就給它10分,每次它撞到牆上就減去5分。而後,這些機器人以它們的擬合度相關的機率進行「交配」(即,撿起大量垃圾的機器人更有可能繁衍後代),新一代機器人誕生了。

交配

有幾種不一樣的方法能夠實現「交配」。在Mitchell的版本中,她將父母的兩條DNA鏈隨機拼接,而後將它們鏈接在一塊兒,爲下一代創造一個孩子。在個人實現中,我從每個親本中隨機分配每一個基因(即,對於243個基因中的每個,我擲硬幣決定遺傳誰的基因)。

例如使用個人方法,在前10個基因裏,父母和孩子可能的基因以下:

Parent 1: 1440623161
Parent 2: 2430661132
Child:    2440621161

突變

咱們用這個算法複製的另外一個天然選擇的概念是「變異」。雖然一個孩子的絕大多數基因都是從父母那裏遺傳下來的,但我也創建了基因突變的小可能性(即隨機分配)。這種突變率使咱們可以探索新的可能。

Python實現

第一步是導入所需的包併爲此任務設置參數。我已經選擇了這些參數做爲起點,可是它們能夠調整,我鼓勵你能夠嘗試調整。

"""
導入包
"""
import numpy as np
from tqdm.notebook import tqdm

"""
設置參數
"""
# 仿真設置
pop_size = 200 # 每一代機器人的數量
num_breeders = 100 # 每一代可以交配的機器人數量
num_gen = 400 # 總代數
iter_per_sim = 100 # 每一個機器人垃圾收集模擬次數
moves_per_iter = 200 # 機器人每次模擬能夠作的移動數

# 網格設置
rubbish_prob = 0.5 # 每一個格子中垃圾的機率
grid_size = 10 # 0網格大小(牆除外)

# 進化設置
wall_penalty = -5 # 因撞到牆上而被扣除的擬合點
no_rub_penalty = -1 # 在空方塊撿垃圾被扣分
rubbish_score = 10 # 撿垃圾可得到積分
mutation_rate = 0.01 # 變異的機率

接下來,咱們爲網格世界環境定義一個類。咱們用標記「o」、「x」和「w」來表示每一個單元,分別對應一個空單元、一個帶有垃圾的單元和一個牆。

class Environment:
    """
    類,用於表示充滿垃圾的網格環境。每一個單元格能夠表示爲:
    'o': 空
    'x': 垃圾
    'w': 牆
    """
    def __init__(self, p=rubbish_prob, g_size=grid_size):
        self.p = p # 單元格是垃圾的機率
        self.g_size = g_size # 不包括牆

        # 初始化網格並隨機分配垃圾
        self.grid = np.random.choice(['o','x'], size=(self.g_size+2,self.g_size+2), p=(1 - self.p, self.p))
        
        # 設置外部正方形爲牆壁
        self.grid[:,[0,self.g_size+1]] = 'w'
        self.grid[[0,self.g_size+1], :] = 'w'

    def show_grid(self):
        # 以當前狀態打印網格
        print(self.grid)

    def remove_rubbish(self,i,j):
        # 從指定的單元格(i,j)清除垃圾
        if self.grid[i,j] == 'o': # 單元格已是空
            return False
        else:
            self.grid[i,j] = 'o'
            return True

    def get_pos_string(self,i,j):
        # 返回一個字符串,表示單元格(i,j)中機器人「可見」的單元格
        return self.grid[i-1,j] + self.grid[i,j+1] + self.grid[i+1,j] + self.grid[i,j-1] + self.grid[i,j]

接下來,咱們建立一個類來表示咱們的機器人。這個類包括執行動做、計算擬合度和從一對父機器人生成新DNA的方法。

class Robot:
    """
    用於表示垃圾收集機器人
    """
    def __init__(self, p1_dna=None, p2_dna=None, m_rate=mutation_rate, w_pen=wall_penalty, nr_pen=no_rub_penalty, r_score=rubbish_score):
        self.m_rate = m_rate # 突變率
        self.wall_penalty = w_pen # 因撞到牆上而受罰
        self.no_rub_penalty = nr_pen # 在空方塊撿垃圾的處罰
        self.rubbish_score = r_score # 撿垃圾的獎勵
        self.p1_dna = p1_dna # 父母2的DNA
        self.p2_dna = p2_dna # 父母2的DNA
        
        # 生成字典來從場景字符串中查找基因索引
        con = ['w','o','x'] # 牆,空,垃圾
        self.situ_dict = dict()
        count = 0
        for up in con:
            for right in con:
                for down in con:
                    for left in con:
                        for pos in con:
                            self.situ_dict[up+right+down+left+pos] = count
                            count += 1
        
        # 初始化DNA
        self.get_dna()

    def get_dna(self):
        # 初始化機器人的dna字符串
        if self.p1_dna is None:
            # 沒有父母的時候隨機生成DNA
            self.dna = ''.join([str(x) for x in np.random.randint(7,size=243)])
        else:
            self.dna = self.mix_dna()

    def mix_dna(self):
        # 從父母的DNA生成機器人的DNA
        mix_dna = ''.join([np.random.choice([self.p1_dna,self.p2_dna])[i] for i in range(243)])

        #添加變異
        for i in range(243):
            if np.random.rand() > 1 - self.m_rate:
                mix_dna = mix_dna[:i] + str(np.random.randint(7)) + mix_dna[i+1:]

        return mix_dna

    def simulate(self, n_iterations, n_moves, debug=False):
        # 仿真垃圾收集
        tot_score = 0
        for it in range(n_iterations):
            self.score = 0 # 擬合度分數
            self.envir = Environment()
            self.i, self.j = np.random.randint(1,self.envir.g_size+1, size=2) # 隨機分配初始位置
            if debug:
                print('before')
                print('start position:',self.i, self.j)
                self.envir.show_grid()
            for move in range(n_moves):
                self.act()
            tot_score += self.score
            if debug:
                print('after')
                print('end position:',self.i, self.j)
                self.envir.show_grid()
                print('score:',self.score)
        return tot_score / n_iterations # n次迭代的平均得分

    def act(self):
        # 根據DNA和機器人位置執行動做
        post_str = self.envir.get_pos_string(self.i, self.j) # 機器人當前位置
        gene_idx = self.situ_dict[post_str] # 當前位置DNA的相關索引
        act_key = self.dna[gene_idx] # 從DNA中讀取行動
        if act_key == '5':
            # 隨機移動
            act_key = np.random.choice(['0','1','2','3'])

        if act_key == '0':
            self.mv_up()
        elif act_key == '1':
            self.mv_right()
        elif act_key == '2':
            self.mv_down()
        elif act_key == '3':
            self.mv_left()
        elif act_key == '6':
            self.pickup()

    def mv_up(self):
        # 向上移動
        if self.i == 1:
            self.score += self.wall_penalty
        else:
            self.i -= 1

    def mv_right(self):
        # 向右移動
        if self.j == self.envir.g_size:
            self.score += self.wall_penalty
        else:
            self.j += 1

    def mv_down(self):
        # 向下移動
        if self.i == self.envir.g_size:
            self.score += self.wall_penalty
        else:
            self.i += 1

    def mv_left(self):
        # 向左移動
        if self.j == 1:
            self.score += self.wall_penalty
        else:
            self.j -= 1

    def pickup(self):
        # 撿垃圾
        success = self.envir.remove_rubbish(self.i, self.j)
        if success:
            # 成功撿到垃圾
            self.score += self.rubbish_score
        else:
            # 當前方塊沒有撿到垃圾
            self.score += self.no_rub_penalty

最後是運行遺傳算法的時候了。在下面的代碼中,咱們生成一個初始的機器人種羣,讓天然選擇來運行它的過程。我應該提到的是,固然有更快的方法來實現這個算法(例如利用並行化),可是爲了本教程的目的,我犧牲了速度來實現清晰。

# 初始種羣
pop = [Robot() for x in range(pop_size)]
results = []

# 執行進化
for i in tqdm(range(num_gen)):
    scores = np.zeros(pop_size)
    
    # 遍歷全部機器人
    for idx, rob in enumerate(pop):
        # 運行垃圾收集模擬並計算擬合度
        score = rob.simulate(iter_per_sim, moves_per_iter)
        scores[idx] = score

    results.append([scores.mean(),scores.max()]) # 保存每一代的平均值和最大值

    best_robot = pop[scores.argmax()] # 保存最好的機器人

    # 限制那些可以交配的機器人的數量
    inds = np.argpartition(scores, -num_breeders)[-num_breeders:] # 基於擬合度獲得頂級機器人的索引
    subpop = []
    for idx in inds:
        subpop.append(pop[idx])
    scores = scores[inds]

    # 平方並標準化
    norm_scores = (scores - scores.min()) ** 2 
    norm_scores = norm_scores / norm_scores.sum()

    # 創造下一代機器人
    new_pop = []
    for child in range(pop_size):
        # 選擇擬合度優秀的父母
        p1, p2 = np.random.choice(subpop, p=norm_scores, size=2, replace=False)
        new_pop.append(Robot(p1.dna, p2.dna))

    pop = new_pop

雖然最初大多數機器人不撿垃圾,老是撞到牆上,但幾代人以後,咱們開始看到一些簡單的策略(例如「若是與垃圾在一塊兒,就撿起來」和「若是挨着牆,就不要移到牆裏」)。通過幾百次的反覆,咱們只剩下一代難以想象的垃圾收集天才!

結果

下面的圖表代表,咱們可以在400代機器人種羣中「進化」出一種成功的垃圾收集策略。

爲了評估進化控制策略的質量,我手動建立了一個基準策略,其中包含一些直觀合理的規則:

  • 若是垃圾在當前方塊,撿起來

  • 若是在相鄰的方塊上能夠看到垃圾,移到那個方塊

  • 若是靠近牆,則向相反方向移動

  • 不然,隨意移動

平均而言,這一基準策略達到了426.9的擬合度,但咱們最終的「進化」機器人的平均擬合度爲475.9。

戰略分析

這種優化方法最酷的一點是,你能夠找到反直覺的解決方案。機器人不只可以學習人類可能設計的合理規則,並且還自發地想出了人類可能永遠不會考慮的策略。一種先進的技術出現了,就是使用「標記物」來克服近視和記憶不足。

例如,若是一個機器人如今在一個有垃圾的方塊上,而且能夠看到東西方方塊上的垃圾,那麼一個天真的方法就是當即撿起當前方塊上的垃圾,而後移動到那個有垃圾的方塊。這種策略的問題是,一旦機器人移動(好比向西),他就沒法記住東邊還有1個垃圾。爲了克服這個問題,咱們觀察了咱們的進化機器人執行如下步驟:

  1. 向西移動(在當前方塊留下垃圾做爲標記)

  2. 撿起垃圾往東走(它能夠看到垃圾做爲標記)

  3. 把垃圾撿起來,搬到東邊去

  4. 撿起最後一塊垃圾

從這種優化中產生的另外一個反直覺策略的例子以下所示。OpenAI使用強化學習(一種更復雜的優化方法)教代理玩捉迷藏。咱們看到,這些代理一開始學習「人類」策略,但最終學會了新的解決方案。

結論

遺傳算法以一種獨特的方式將生物學和計算機科學結合在一塊兒,雖然不必定是最快的算法,但在我看來,它們是最美麗的算法之一。

本文中介紹的全部代碼均可以在個人Github上找到,還有一個演示Notebook:https://github.com/andrewjkuo/robby-robot-genetic-algorithm。謝謝你的閱讀!

原文連接:https://towardsdatascience.com/optimising-a-rubbish-collection-strategy-with-genetic-algorithms-ccf1f4d56c4f

歡迎關注磐創AI博客站:
http://panchuang.net/

sklearn機器學習中文官方文檔:
http://sklearn123.com/

歡迎關注磐創博客資源彙總站:
http://docs.panchuang.net/

相關文章
相關標籤/搜索