啓發式算法之遺傳算法

剛開學便被拉去參加了研究生數模比賽,賽題是一個航班排班的優化問題,因此第一反映即是遺傳算法,比賽期間三個問題都使用單目標遺傳算法,趁着還比較熟悉,特此記錄,以便後續複習。本篇文章使用Python進行實現。python

啓發式算法

啓發式算法是一種技術,這種技術使得在可接受的計算成本內去搜尋最好的解,但不必定能保證所得的可行解和最優解,甚至在多數狀況下,沒法闡述所得解同最優解的近似程度。算法

就是說這種算法的全局最優解只是理論上可行,大多數狀況下都是一個局部最優解。啓發式算法用的比較多的有模擬退火算法(SA)、遺傳算法(GA)、列表搜索算法(ST)、進化規劃(EP)、進化策略(ES)、蟻羣算法(ACA)、人工神經網絡(ANN)。這裏重點介紹一下遺傳算法(GA)。網絡

遺傳算法準備

遺傳算法(Genetic Algorithm, GA)起源於對生物系統所進行的計算機模擬研究。它是模仿天然界生物進化機制發展起來的隨機全局搜索和優化方法,借鑑了達爾文的進化論和孟德爾的遺傳學說。其本質是一種高效、並行、全局搜索的方法,能在搜索過程當中自動獲取和積累有關搜索空間的知識,並自適應地控制搜索過程以求得最佳解。dom

具體來講,在寫算法以前,有四個很重要的步驟:函數

  1. 肯定編碼方式
  2. 如何設計編碼
  3. 肯定約束條件
  4. 如何實現約束

肯定編碼方式

對於編碼方式來講,影響到交叉算子、變異算子等遺傳算子的運算方法,大很大程度上決定了遺傳進化的效率。 總的來講,編碼方式能夠分爲三大類:二進制編碼法、浮點編碼法、符號編碼法。優化

  1. 二進制編碼法:將求解的數從十進制轉換成二進制表示,這樣便於編碼與解碼,方便交叉和變異算子的實現,但對於一些連續函數的優化問題,因爲其隨機性使得其局部搜索能力較差,如對於一些高精度的問題,當迭代接近最優解後,因爲其變異後表現型變化很大,不連續,因此會遠離最優解,達不到穩定。而格雷碼能有效地防止這類現象。編碼

  2. 格雷碼編碼法:咱們先來複習一下格雷碼:spa

    十進制 格雷碼   二進制
    
    0       0000    0000
    1       0001    0001
    2       0011    0010
    3       0010    0011
    4       0110    0100
    5       0111    0101
    6       0101    0110
    7       0100    0111
    8       1100    1000
    9       1101    1001
    10      1111    1010

    維基百科對格雷碼解決誤碼的說明:.net

    傳統的二進位系統例如數字3的表示法為011,要切換為鄰近的數字4,也就是100時,裝置中的三個位元都得要轉換,所以於未徹底轉換的過程時裝置會經歷短暫的,010,001,101,110,111等其中數種狀態,也就是表明著二、一、五、六、7,所以此種數字編碼方法於鄰近數字轉換時有比較大的誤差可能範圍。葛雷碼的發明便是用來將誤差之可能性縮減至最小,編碼的方式定義為每個鄰近數字都只相差一個位元,所以也稱為最小差異碼,可使裝置作數字步進時只更動最少的位元數以提升穩定性。設計

    連續兩個整數所對應的編碼值之間僅僅只有一個碼位是不一樣的,當迭代次數不少接近最優值時,相鄰兩個值之間變化是最大的,當一個染色體變異後,它原來的表現現和如今的表現型是連續的,這樣能夠將偏差的可能性縮減至最小。

    格雷碼的轉換爲:

    G爲格雷碼,B爲二進制碼,n爲當前計算位,\(\oplus\)爲異或

    就有\(G(n) = B(n+1) \oplus B(n)\)

    \(G(n) = B(n+1) + B(n)\)

  3. 浮點編碼法

    對於一些多維、高精度要求的連續函數優化問題,使用二進制編碼來表示個體時將會有一些不利之處。

    二進制編碼存在着連續函數離散化時的映射偏差。個體長度較知時,可能達不到精度要求,而個體編碼長度較長時,雖然能提升精度,但卻使遺傳算法的搜索空間急劇擴大。

    所謂浮點法,是指個體的每一個基因值用某一範圍內的一個浮點數來表示。在浮點數編碼方法中,必須保證基因值在給定的區間限制範圍內,遺傳算法中所使用的交叉、變異等遺傳算子也必須保證其運算結果所產生的新個體的基因值也在這個區間限制範圍內。

  4. 符號編碼法

    這種編碼法其實在解決實際問題時問題時是用的最多的,例如此次的航班優化問題我就使用的這種方法,這種方法要結合實際問題來進行編碼,而後交叉和變異也要結合實際進行編寫,這裏我就舉個具體例子來說解一下符號編碼法。

如何設計編碼

對於通常的數學函數優化問題都是將求解的值轉換成二進制或者格雷碼進行迭代而後計算適應度(fitness)便可,這裏我主要講解一下若是不是一個傳入值直接求解方程的問題解決。

舉個栗子

假如我有10架航班(a,b,c,...),和5個登機口(1,2,3,4,5),我如今要讓排班更加緊湊,使得使用的登機口數儘可能少。每架飛機有個一個出發時間(departure_time),在出發時間以前45分鐘飛機必須到達登機口,因而咱們設計的一個最小化函數即爲

\[ F = \sum_{i=0}^5\sum_{j=0}^{10}S_{ij}\]

這裏i即爲登機口數,j爲航班數,這樣\(S_ij\)就是對應登機口的航班與上一架航班的時間之差,使得時間和最小咱們可使得安排更加緊湊。

那我就能夠這樣進行編碼

ga1.jpg

10個染色體就是對應的10架航班,裏面的內容就是其對應的登機口號,這樣咱們就能夠計算同一登機口的相鄰飛機的起飛間隔時間差了。

例如2號登機口對應的就是飛機a,飛機i,那麼咱們使用$ i_{arrival_time} - a_{arrival_time}$就能夠獲得對應的時間差了,統計計算1號,3號,4號和5號,加在一塊兒就是咱們的目標函數的值了。

肯定約束條件

對於不一樣的問題求解會有不一樣的約束方式,常見的約束方式有

  • 範圍限制
  • 條件限制

範圍限制,例如咱們的求值只可在(0,0.3]與[0.5,0.8)之間,那麼咱們在求解的時候要麼加入懲罰項,即計算到非對應範圍的時候乘上一個很大的值來限制,要不就在對應交叉和變異的時候先判斷在交叉變異進行限制。

條件限制,例如上題,咱們假如約束條件,即爲飛機有寬與窄之分,咱們的登機口也有對應的寬窄之分,對應的飛機只能停到對應的登機口處,因此這裏咱們編碼時也要在知足約束進行編碼,編寫交叉變異時也針對此進行編寫。

如何實現約束

實現約束即在三個地方實現,分別爲

  1. 基因可行解初始化(inital)
  2. 基因交叉(crossover)
  3. 基因變異(mutate)

這裏簡單講解一下可行解初始化的約束如何實現,咱們能夠寫一個簡單的while循環來找到可行解:

POP_SIZE = 100 # 可行解數目
DNA_SIZE = 10 #dna長度,這裏爲航班數量,即爲10
all_airport_id = [1,2,3,4,5] # 登機口id
aircraft_to_airport_dict = {
    1:[1,2,3],
    2:[2,3,4],
    ...
}# 這裏是每架飛機對應的登機口id,這裏我就簡寫了
def inital(self):
    pop = [] # 可行解list
    while len(pop) != POP_SIZE:
        dna = [0 for i in range(DNA_SIZE)]
        all_aircraft_id = list(range(1, 11)) # 全部飛機id
        temp_airport_id_set = set() # 遍歷過的存儲飛機對應的可行的飛機場
        for aircraft_id in all_aircraft_id:
            workable_airport = aircraft_to_airport_dict[aircraft_id] #可行的登機口id
            while 1: #直到要找到合適的機場停機
                airport_id = random.choice(list(workable_airport)) # 隨機生成一個登機口id
                temp_airport_id_set.add(airport_id)
                if len(temp_airport_id_set) == len(workable_airport):
                    dna[aircraft_id-1] = 0 #若是全都不合適,就跳出
                    break
                if constraint(aircraft_id, airport_id): #若是知足約束
                    dna[aircraft_id-1] = airport_id

這裏的constraint約束函數能夠是不少約束,好比時間約束,區域約束等等。

遺傳算法實現

我的認爲遺傳算法整體分爲5個部分:

  1. 初始化可行解
  2. 目標函數,即計算適應度(fitness)
  3. 適者生存
  4. 交叉進化
  5. 變異

具體流程爲:

生成n組可行解dna
迭代m次:
    計算每一個dna的fitness
    適者生存
    循環n個dna:
        交叉進化
        變異

接下來主要講一下適者生存、交叉進化和變異

適者生存

物競天擇,適者生存

適者生存的意思就是經過計算fitness,而後做爲隨機選擇的機率,這樣機率越大的就被選擇到機率越大。具體實現以下,我對fitness作了個倒數,這裏是由於我計算的是最小化,因此越小對於我來講是最優的,因此取倒數再計算機率才符合邏輯。

def select(self, pop, fitness):    
        fitness = 1 / fitness
        idx = np.random.choice(np.arange(POP_SIZE), size=POP_SIZE, replace=True,
                           p=fitness/fitness.sum())
        return pop[idx]

交叉進化

交叉進化有不少種方式,具體能夠看這篇文章遺傳算法中幾種交叉算子小結,講的已經很全了。

這裏我主要講一下單點交叉,即隨機在雙親(parent)dna上選擇一個點p,而後與孩子(child)dna上p點後的基因進行交換,實現起來也十分簡單。以下圖,o點爲2,而後就將將藍色部分與黃色部分互相交換。

ga2.jpg

CROSS_RATE = 0.8 #交叉機率
def crossover(self, parent, pop):  
    if np.random.rand() < CROSS_RATE:
        i_ = random.randint(0, POP_SIZE - 1) # 孩子基因,child
        p = np.random.randint(0, DNA_SIZE)  # 單點交叉
        parent[p:DNA_SIZE] = pop[i_][p:DNA_SIZE]
        return parent
    return parent

變異

人間總有那麼多出其不意的突變,很難說咱們怎樣纔算是到了窮途末路,人只要一息尚存,對什麼均可抱有但願。

——蒙田《蒙田隨筆全集》

許多突變都是中性的,但有些可能會對後代形成損傷,而有些則多是有益的。控制基因的突變能夠對生物體形成極大的影響。有益的基因突變能夠幫助生物更好地適應環境,並極可能遺傳給後代。

變異就是在dna上隨機選擇一個點進行基因變異,這樣作的緣由是防止局部最優,由於若是這個世界有人突變,那麼就不排除會比咱們如今更加適應這個世界的可能。c點就是變異點。

ga3.jpg

MUTATION_RATE = 0.01 #突變機率
def mutate(self, child):
    for c in range(DNA_SIZE):
        if np.random.rand() < MUTATION_RATE:
            workable_airport = aircraft_to_airport_dict[aircraft_id] #可行的登機口id
            random_airport_id = random.choice(workable_airport) #隨機一個機場
            child[c] = random_airport_id
相關文章
相關標籤/搜索