面試過程是一個很好的查漏補缺的機會,要好好梳理,覆盤面試過程。
比如最近面試就發現,我已經把一年前課堂上學的遺傳算法和優化方法,全數還給老師了,渣都不剩O_O。。。
這對得起辛辛苦苦上的課和嘔心瀝血寫的大作業嗎?
於是抓緊複習,真的多虧當年嘔心瀝血寫的大作業,很快勾起了我的回憶~
遺傳算法思路很簡單,關鍵在於在一定概率下選擇比較好的樣本留下,而結果改進的關鍵在於交叉和變異。
當年做的單目標優化問題主要用於找到高維函數的極值,代碼中用的是20維。簡單來說就是求一組自變量(100個),找到類似於
這樣的函數的極值(最小值或者最大值)。
當然實際中函數的樣子更加複雜,代碼中有當時的6道題目,可以感受一下。
遺傳算法解題步驟歸結下來分爲幾步:
- 初始化種羣樣本(比如100個個體構成種羣,每個個體有20個自變量的初始值);
- 計算適應度(把自變量代入要求解的函數中,有些函數要求求最小值,那麼適應度要計算爲函數值的倒數,保證越接近正確的答案適應度越大);
- 輪盤賭選擇樣本;
- 交叉:以一定概率將兩個個體交換一部分自變量;
- 變異:以一定概率將個體中某些值變異成其他值;
- 重複迭代2~5步,直到得到最優個體和最優解。
嚴肅一點就是
其中,選擇算子輪盤賭選擇樣本的理論如下:
首先來一個餅圖,不對是輪盤
三個顏色代表三個個體,餅的大小代表每個個體的適應度大小。
理論上來說,餅越大,被飛刀戳中的概率越大,這就是通過適應度選擇較好個體的過程。
具體寫成代碼怎麼做呢?這裏用一個累積概率的方法,每個個體排排站,將它們的概率也拼在一起。
想象一條直線,兩端代表0和1,中間每個個體憑實力(概率)佔據一段,實力越強佔據的線段越長。這樣我們隨機生成一個0~1之間的數,落在長線段區間的機會就比較大。把直線圈成一個圓形,就是輪盤賭的原理。
具體代碼如下,做一個參考,建議先從主函數開始看:
六個題目分別爲:
咦,怎麼才5個?不知道爲啥代碼裏面有6個,代碼中對應的是第5個題目。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 18-11-1 上午10:06
# @Author : Xier
# @File : high_d.py
# 高維單目標優化問題
import numpy as np
import copy
import math
import random
import matplotlib.pyplot as plt
from pylab import mpl
mpl.rcParams['font.sans-serif'] = 'NSimSun, Times New Roman'
mpl.rcParams['axes.unicode_minus']=False
def init(range_ab,val_num,popusize):
init_gen = np.random.uniform(range_ab[0], range_ab[1], (popusize,val_num))
return init_gen
def cal_fitness(dec_gen):
f = np.zeros((dec_gen.shape[0],1))
f1 = np.zeros((dec_gen.shape[0], 1))
f2 = np.zeros((dec_gen.shape[0], 1))
# 題目一
# for i in range(dec_gen.shape[0]):
# for j in range(dec_gen.shape[1]):
# if j == 0:
# f[i] = dec_gen[i][j]*dec_gen[i][j]
# else:
# f[i] += dec_gen[i][j]*dec_gen[i][j]
# 題目二
# for i in range(dec_gen.shape[0]):
# for j in range(dec_gen.shape[1]):
# if j == 0:
# f[i] = math.ceil(dec_gen[i][j]+0.5)**2
# else:
# f[i] += math.ceil(dec_gen[i][j]+0.5)**2
# # 題目三
# for i in range(dec_gen.shape[0]):
# for j in range(dec_gen.shape[1]):
# for p in range(j+1):
# if p == 0:
# x = dec_gen[i][p]
# else:
# x += dec_gen[i][p]
# if j == 0:
# f[i] = x**2
# else:
# f[i] += x**2
# 題目四
# for i in range(dec_gen.shape[0]):
# for j in range(dec_gen.shape[1]):
# if j == 0:
# f1[i] = abs(dec_gen[i][j])
# else:
# f1[i] += abs(dec_gen[i][j])
# for j in range(dec_gen.shape[1]):
# if j == 0:
# f2[i] = abs(dec_gen[i][j])
# else:
# f2[i] *= abs(dec_gen[i][j])
#
# f[i]=f1[i]+f2[i]
# 題目五
# for i in range(dec_gen.shape[0]):
# for j in range(dec_gen.shape[1]-1):
# if j == 0:
# f[i] = 100*(dec_gen[i][j+1]-dec_gen[i][j]**2)**2+(1-dec_gen[i][j])**2
# else:
# f[i] += 100*(dec_gen[i][j+1]-dec_gen[i][j]**2)**2+(1-dec_gen[i][j])**2
# 題目六
A = 1
for i in range(dec_gen.shape[0]):
for j in range(dec_gen.shape[1]):
if j == 0:
f[i] = dec_gen[i][j]**2-A*math.cos(2*math.pi*dec_gen[i][j])
else:
f[i] += dec_gen[i][j]**2-A*math.cos(2*math.pi*dec_gen[i][j])
f[i] = dec_gen.shape[1]*A + f[i]
return f
def mate_selection(y, gen, val_num):
fitness = 1/(y+0.1) # 找使函數值最小的個體,函數值越小適應度越大
P = fitness.copy()
Q = fitness.copy()
sum_fitness = sum(fitness)
for i in range(len(P)):
P[i] = fitness[i]/sum_fitness # 爲每個個體賦一個選中的概率
Q[i] = sum(P[:i+1]) # 記錄至今爲止的概率
mate_np = np.zeros((1, gen.shape[1]))
ran_select = np.random.random(len(fitness))#-1 # 隨機生成0~1的數
# 輪盤賭,理論上來說蛋糕面積越大,隨機戳到的機率越大
for r in range(len(ran_select)):
for q in range(len(Q)):
if ran_select[r] <= Q[q]:
mate_np = np.vstack((mate_np, gen[q, :]))
break
return mate_np[1:,:]
def cross_able(gen,pc):
# cross_np = gen[0].copy()
# no_cross_np = gen[0].copy()
ran_select = np.random.random(gen.shape[0])
cross_np = gen[np.where(ran_select<pc),:]
no_cross_np = gen[np.where(ran_select>=pc),:]
return cross_np[0,:,:], no_cross_np[0,:,:]
def cross(gen):
for i in range(gen.shape[0]//2):
idex = random.randint(0, gen.shape[1]-1)
gen[[i*2,i*2+1],idex:] = gen[[i*2+1,i*2],idex:]
return gen
def mutation(gen,pm):
for i in range(gen.shape[0]):
ran_select = np.random.random(gen.shape[1])
gen[i,np.where(ran_select<pm)] = np.random.uniform(range_ab[0], range_ab[1],(1, len(np.where(ran_select<pm))))
return gen
if __name__ == '__main__':
range_ab = [-5.12, 5.12]
popusize = 100 # 種羣個數
val_num = 20 # 自變量個數
epochs = 10000 # 迭代優化次數
# 初始化浮點數種羣
gen = init(range_ab, val_num, popusize)
Y = [] # 記錄當代最優結果
min_y = []
record = []
for i in range(epochs):
# 解碼後的樣本
# dec_gen = decoding(range_ab, bin_gen, bit_n,val_num)
# 計算適應度
y = cal_fitness(gen)
y_min = np.min(y)
Y.append(y_min)
if i == 0:
record_min = y_min
record = [i + 1, gen[np.argmin(y)],record_min] # 記錄第幾個epoch,最優個體,對應的最優結果
if y_min < record_min:
record_min = y_min
# a = i+1
record = [i+1, gen[np.argmin(y)],record_min] # 更新最優結果
min_y.append(record_min)
# 選擇樣本,輪盤賭
mate_np = mate_selection(y, gen, val_num)
# 判斷能否交配
cross_np, no_cross_np = cross_able(mate_np, 0.6)
# 交配
crossed_gen = cross(cross_np)
# 變異
cross_gen = np.vstack((crossed_gen, no_cross_np))
gen = mutation(cross_gen, 0.01)
print('[迭代次數,最優個體,最優值] = ', record)
plt.title('Rastrigin函數')
# plt.ylim(-100, 3000)
# yticks= np.linspace(0, 3000, 5)
# plt.yticks(yticks)
plt.plot(range(epochs), Y, label='當前代最優解')
plt.plot(range(epochs), min_y, label='歷史最優解')
plt.legend()
# 添加最優解在圖中的位置
plt.scatter([record[0]], [record[2]], color='red')
# 添加座標點信息
plt.text(record[0]-1000, record[2], (record[0], record[2]))
plt.show()
運行20次的結果如下:
從表可以看出,對於某些複雜高維問題,遺傳算法還是過於簡單,效能有效。
遺傳算法是比較初級的算法,後續廣大學者從很多方面改進了遺傳算法,如給適應度函數加一些約束,使最優樣本的選擇更加合理,函數更容易收斂。或者改變變異和交叉機制,使算法優化過程能從局部極小值點跳出來等等。
附上以上5個函數的收斂曲線如下: