遺傳演算法python實現
遺傳演算法python實現
一、問題引入
下面看一個求最優解的函式問題,這裡告訴我們x的範圍為(-1,2),需要我們找到使f函式取得最大值的x,拿到這個方程,直接解還是比較困難的,所以我們使用遺傳演算法,找到儘可能接近最優解的值。
二、遺傳演算法的步驟
1.初始化
設定進化代數計數器t=0,設定最大進化代數T,隨機選擇M個個體作為初始群體P(0)
2.個體評價
計算群體P(t)中各個個體的適應度
3.選擇運算
將選擇運算元作用於群體。選擇的目的是把優化的個體直接遺傳到下一代或通過配對交叉產生新的個體再遺傳到下一代。選擇操作是建立在群體中個體的適應度評估基礎上的。
4.交叉運算
將交叉運算元作用於群體。遺傳演算法中起核心作用的就是交叉運算元
5.變異運算
將變異運算元作用於群體。即是對群體中的個體串的某些基因作變動。群體P(t)經過選擇、交叉、變異運算之後得到下一代群體P(t+1)。
6.終止條件判斷
若t=T,則以進化過程中所得到的具有最大適應度個體作為最優解輸出,終止計算
三、實現思路
1.編碼的設計
我們所需要求的x值是十進位制的小數,我們要將其轉換為類似基因的編碼來作為遺傳演算法的個體,所以我的思路是將十進位制的小數轉換為二進位制數,便於對其進行選擇、交叉和變異。需要實現的是編碼函式和解碼函式。
2.適應度函式
適應度函式即問題中給出的方程,只需要將二進位制數解碼為十進位制小數帶入方程中即可得到該個體的適應度。
3.選擇函式
選擇函式是要選出群體中適應度高的,為了符合遺傳的規律也不都是適應度高的,只是適應度高的被選擇的概率大。為了實現選擇我使用了輪盤賭的方法,何為輪盤賭呢,就是將所有個體放到一個輪盤上,適應度高的所佔的輪盤面積大,然後轉動輪盤,指標指到哪個個體就將其選擇進入到下一輪交叉運算,直到選擇出種群規模M個(這裡要注意整個遺傳演算法自始至終種群規模是不變的都為M),這就導致選擇到下一輪的個體會有重複的,即適應度高的個體可能被選擇多次而適應性差的可能沒有被選擇。
4.交叉函式
交叉函式和變異函式都是為了保證種群個體的多樣性,因為初始種群是隨機選擇的,可能並沒有好的個體,如果沒有交叉和變異僅僅是選擇的話,最後也只是得到了初始種群的最優個體,所以需要交叉和變異產生更多樣的個體才更可能在演算法結束後得到優秀的個體。交叉是得到多樣性個體的關鍵步驟。
交叉函式主要實現思路是將選擇出的M個個體中的某些個體(並不是所有的 ,需要設定交叉的概率,符合自然規律)進行兩兩的交叉,因為前面是將個體編碼為二進位制碼,所以只需要隨機確定一個交叉點,將交叉點後的片段進行交換(這只是一種方法,還可以將交叉 點前的進行交換,還可以確定兩個交叉點,將交叉點中間的片段進行交換等),這裡要注意編碼是有編碼空間的,交叉之後可能會超出編碼空間,這裡需要一個判斷,如果超出了變數空間則重新進行交叉(這個可以視為自然界中基因進行交叉後個體死亡)。選擇後的種群進入變異操作。
5.變異函式
變異也是有一定變異概率的,這裡我的做法是隨機選擇變異個體的一位,然後將其取反,如果超出了編碼空間則重新進行變異直到落在編碼空間中。
6.迭代
將前五步不斷迭代到T輪後,再選擇該種群的最優個體,即可得到此最優問題的近似解
四、具體實現
下面是完整程式碼的連線,可以直接執行:
連結:https://pan.baidu.com/s/18c1Dw62I43Hrghhi0sZhzw
提取碼:8ki9
1.編碼解碼函式
為了程式碼的可移植性,我寫了一個獲得二進位制位數的程式碼,只需要輸入十進位制的左右端點和有效位數,即可得到所需二進位制編碼的位數,這樣這個程式碼就可以擴充套件到其他問題,而不僅僅侷限於該問題
# 得到所需二進位制的位數
def get_bit(start,end,decimal):
'''
:param start: 區間左端點值
:param end: 區間右端點值
:param decimal: 有效位數
:return: 所需二進位制的位數
'''
# 求所需要的二進位制數的位數
need = (end - start) * pow(10, decimal + 1)
# 對2取對數,向上取整得到位數
bit = int(math.log(need, 2)) + 1
return bit
將十進位制的小數轉換為二進位制數,因為python中存不下22位的數,所以使用字串進行表示
# 編碼函式
def encode(start,end,decimal,bit,num):
'''
:param start: 區間左端點值
:param end: 區間右端點值
:param decimal: 有效位數
:param bit: 所需二進位制的位數
:param num: 需要轉化的十進位制數
:return: 22位二進位制數
'''
# # 求所需要的二進位制數的位數
# need = (end - start) * pow(10,decimal + 1)
# # 對2取對數,向上取整得到位數
# bit = int(math.log(need,2)) + 1
# print(int(bit)+1)
# 將數轉化為二進位制
binary = bin(int((num + 1) * pow(10,decimal + 1)))
# 除去前面的0b
binary = str(binary)[2:]
# 將其補為22位
while len(binary) < 22:
binary = "0" + binary
return binary
將二進位制數解碼轉換為十進位制數
# 解碼函式
def decode(start,end,decimal,num):
'''
:param start: 區間左端點值
:param end: 區間右端點值
:param decimal: 有效位數
:param num: 需要解碼的二進位制數
:return: 原十進位制數
'''
num = "0b" + num
num = int(num,2)
num = num / pow(10,decimal + 1) -1
# print(num)
return num
2.適應度函式
我的做法是先將二進位制數解碼然後帶入函式中得到適應度
# 適應度函式
def fitness(start,end,decimal,num):
'''
:param start: 區間左端點值
:param end: 區間右端點值
:param decimal: 有效位數
:param num: 需要求適應度函式值的二進位制數
:return: 適應度函式值
'''
# 首先解碼
x = decode(start,end,decimal,num)
# 計算適應度函式值
f = x * math.sin(10 * math.pi * x) + 2.0
return f
3.選擇函式
這裡使用適應度函式來設定輪盤,整個輪盤大小為1,每個個體在輪盤的起始位置為前面所有個體適應度求和再除以所有個體適應度的和,終止位置為包括該個體在內的前面所有個體適應度求和再除以所有個體適應度的和,這樣該個體在輪盤所佔的大小與其適應度成正比,通過隨機生成(0,1)的小數來作為轉盤的指標,落在哪個個體的區域就選擇該個體。
# 選擇函式
def select(start,end,decimal,population):
"""
:param start: 區間左端點值
:param end: 區間右端點值
:param decimal: 有效位數
:param num: 需要求適應度函式值的二進位制數
:param population: 種群,規模為M
:return: 返回選擇後的種群
"""
# 按照population順序存放其適應度
all_fitness = []
for i in population:
all_fitness.append(fitness(start,end,decimal,i))
# print(fitness(start,end,decimal,i))
# 適應度函式的總和
sum_fitness = sum(all_fitness)
# 以第一個個體為0號,計算每個個體輪盤開始的位置,position的位置和population是對應的
all_position = []
for i in range(0,len(all_fitness)):
all_position.append(sum(all_fitness[:i+1])/sum_fitness)
# print(all_position)
# 輪盤賭進行選擇
# 經過選擇後的新種群
next_population = []
for i in range(0,len(population)):
# 生成0-1之間的隨機小數
ret = random.random()
for j in range(len(all_position)):
# 根據輪盤賭規則進行選擇
if all_position[j] > ret:
# print(ret)
# print(all_position[j])
next_population.append(population[j])
break
return next_population
4.交叉函式
這裡為了防止交叉和變異後的個體超出編碼空間,所以寫了一個判斷交叉變異後的個體是否超出範圍的函式
# 判斷是否超出範圍的函式
def whether_out(start,end,decimal,num):
if start <=decode(start,end,decimal,num) <= end:
return True
else:
return False
這裡根據交叉概率選擇需要交叉的個體數,因為選擇出來的種群順序本來就是隨機的,所以只需要從第一個開始交叉,直到達到交叉的數目即可。
# 交叉函式
def cross(M,Pc,bit,start,end,decimal,next_population1):
'''
:param M: 種群規模
:param Pc: 交叉概率
:param bit: 二進位制的位數
:param start: 區間左端點值
:param end: 區間右端點值
:param decimal: 有效位數
:param next_population1: 選擇後的種群
:return: 交叉後的種群
'''
num = M * Pc
# 計數器,判斷是否交換次數達到num次
count = 0
i = 0
# # 交叉後的種群
# next_population2 = []
# 由於選擇後的種群本來就是隨機的,所以讓相鄰兩組做交叉,從第一組開始直到達到交叉概率停止
while(i < M):
# while(count < num):
# 隨機產生交叉點
position = random.randrange(0,bit-1)
# print(position)
# print(position)
# 將兩個個體從交叉點斷開
tmp11 = next_population1[i][:position]
tmp12 = next_population1[i][position:]
tmp21 = next_population1[i+1][:position]
tmp22 = next_population1[i+1][position:]
# 重新組合成新的個體
# print(next_population1[i])
next_population1[i] = tmp11 + tmp22
# print(next_population1[i])
next_population1[i+1] = tmp21 + tmp12
# 判斷交叉後的個體是否超出範圍,如果每超出則count+1,否則i不加,count不加
if (whether_out(start,end,decimal,next_population1[i]) and whether_out(start,end,decimal,next_population1[i+1])):
i += 2
count += 1
else:
continue
if count > num:
break
# print(count)
return next_population1
5.變異函式
這個函式是用來將指定位置的字元取反,即變異操作
# 取反字串指定位置的數
def reverse(string,position):
string = list(string)
if string[position] == '0':
string[position] = "1"
else:
string[position] = "0"
return ''.join(string)
這裡判斷是否變異是每個個體都產生一個(0,1)的隨機小數,如果小於變異概率則對該個體進行變異
# 變異函式
def variation(M,Pm,start,end,decimal,bit,next_population2):
'''
:param M: 種群規模
:param Pm: 變異概率
:param start: 區間左端點值
:param end: 區間右端點值
:param decimal: 有效位數
:param bit: 二進位制的位數
:param next_population2: 交叉後的種群
:return: 變異後的種群
'''
# i = 0
for i in range(M):
ret = random.random()
# 生成0-1的隨機數,如果隨機數
if ret < Pm:
# 隨機產生變異點
position = random.randrange(0, bit)
next_population2[i] = reverse(next_population2[i],position)
# if (whether_out())
while(whether_out(start,end,decimal,next_population2[i]) == False):
# 如果超出範圍則重新隨機產生變異點,直到滿足範圍
position = random.randrange(0, bit)
next_population2[i] = reverse(next_population2[i], position)
else:
continue
return next_population2
6.選擇群體中最優個體
在迭代T代的個體中選擇適應度最高的個體
# 尋找群體中的最優個體
def search(start,end,decimal,population):
'''
:param start: 區間左端點值
:param end: 區間右端點值
:param decimal: 有效位數
:param population: 最終迭代後的群體
:return: 最優個體
'''
# 記錄函式值
fit = []
for i in population:
fit.append(fitness(start,end,decimal,i))
# 求出最大值所在的位置
position = fit.index(max(fit))
return decode(start,end,decimal,population[position])
7.主函式
首先從(-1,2)的個體空間中隨機選擇M個個體作為初始種群,然後進行T次選擇、交叉、變異得到最終的種群,然後選擇出該種群中最優的個體
# 主函式
def main(M,T,Pc,Pm,start,end,decimal):
"""
:param M: 種群規模
:param T: 遺傳運算的終止進化代數
:param Pc: 交叉概率
:param Pm: 變異概率
:param start: 區間左端點值
:param end: 區間右端點值
:param decimal: 有效位數
:return:
"""
bit = get_bit(start,end,decimal)
# 全集,包括所有編碼後的個體
all = []
for i in range(-1 * pow(10,6), 2 * pow(10,6) + 1):
all.append(encode(start,end,decimal,bit,i / pow(10,6)))
i += 1
# 第一次隨機選擇種群,規模為T
population = random.sample(all, M)
for i in range(T):
# 進行選擇操作
population = select(start,end,decimal,population)
# 進行交叉操作
population = cross(M,Pc,bit,start,end,decimal,population )
# 進行變異操作
population = variation(M,Pm,start,end,decimal,bit,population )
# 最優個體
final = search(start,end,decimal,population)
print('%.5f' % final)
入口函式,需要指定初始種群規模,遺傳運算的終止進化代數,交叉概率,變異概率,左端點值,右端點值和有效位數這些引數也很重要,如果指定的引數不好則得不到好的結果。
if __name__ == '__main__':
# test()
main(200,200,0.6,0.005,-1,2,5)
下面是執行的結果,大概在1.85左右,執行時間13秒,還能接受,說明整體複雜度還可以