Python遺傳算法:火狐進(jìn)化

今天是教師節(jié),老師們,教師節(jié)快樂!


這是一張由遺傳算法程序生成的圖片。

遺傳算法初嘗,火狐像素進(jìn)化

前言

作為本科生物出生的我,對遺傳算法好奇已久。再后來看到科學(xué)松鼠會的一篇文章遺傳算法:內(nèi)存中的進(jìn)化,對其使用三角形進(jìn)化成火狐,感到十分有趣。于是我準(zhǔn)備也寫一個(gè)程序來生成一個(gè)火狐。

這里我不使用三角形來進(jìn)化,而是直接使用像素。多個(gè)三角形拼成一個(gè)火狐,實(shí)際也是是三角形內(nèi)的像素顏色組合成火狐的樣子。所以,我決定直接使用像素生成一個(gè)火狐。

背景知識

簡略說一下遺傳算法以及我寫的程序邏輯。
遺傳算法:初始化種群后,種群間的個(gè)體之間進(jìn)行交叉(可能發(fā)生變異)繁殖出下一代,然后優(yōu)勝劣汰,最差勁的部分個(gè)體被淘汰,存活的個(gè)體之間繼續(xù)進(jìn)行交叉(變異)繁殖。直到種群中個(gè)體符合某個(gè)條件終止。

  1. 遺傳算法組成:

    • 編碼:為便于計(jì)算機(jī)編程,個(gè)體基因需要以合適編碼方式進(jìn)行編碼。(程序中直接使用圖片像素色值進(jìn)行編碼)
    • 群體:多個(gè)個(gè)體組成群體。(程序中,就是多個(gè)圖片組成群體)
    • 適應(yīng)度函數(shù):用于判定個(gè)體的適應(yīng)度,是否適合存活。(程序中,就是圖片與火狐圖片進(jìn)行像素色值的比較)
    • 選擇:淘汰適應(yīng)度差的個(gè)體之后,選擇剩下的部分個(gè)體進(jìn)行交叉繁殖(程序直接隨機(jī)選擇n對個(gè)體)
    • 交叉:即父母各提供部分基因繁殖組成出下一代。(程序中就是兩圖片各自提供一半像素組成新圖片)
    • 變異:為了種群基因多樣性,繁殖個(gè)體可能發(fā)生變異。(程序中表現(xiàn)為部分像素色值發(fā)生變化)
  2. 程序邏輯:
    根據(jù)火狐圖片大小,生成P張相同大小,但像素色值隨機(jī)的圖片種群,然后用適應(yīng)度函數(shù)給每個(gè)圖片個(gè)體打分。然后隨機(jī)選擇n對父母,父母雙方各提供一半像素組成新的后代圖片。后代進(jìn)行適應(yīng)度打分后,根據(jù)適應(yīng)度分?jǐn)?shù)將種群中適應(yīng)度分?jǐn)?shù)最不好的個(gè)體淘汰以維持種群大小。然后進(jìn)行新一輪的交叉繁殖變異淘汰,直到合適的圖片個(gè)體產(chǎn)生或進(jìn)化代數(shù)結(jié)束。

圖片
這里使用圖片是一張RGBA四通道的圖片,R表示紅色,G表示綠色,B表示藍(lán)色,A表示不透明度。R,G,B三個(gè)色值的不同大小組合疊加,幾乎涵蓋人眼感知的所有顏色。R,G,B,A四個(gè)取值都在0-255之間(在matplotlib中使用的話,取值范圍被縮放到0-1之間了)
而一張圖片由許多像素組成,不同像素有著各自的色彩數(shù)值,對于RGBA圖來說,一個(gè)像素點(diǎn)顏色就由這四個(gè)數(shù)值(r,g,b,a)確定。比如,用matplotlib讀取一張500*500像素大小的圖片

>>> img = mpimg.imread('./ff.png')
>>> img.shape ## 500*500的像素大小,每個(gè)像素由(r,g,b,a)4個(gè)色值確定顏色
(500, 500, 4)
>>> img
array([[[1., 1., 1., 0.],
        [1., 1., 1., 0.],
        [1., 1., 1., 0.],
        ...,
        [1., 1., 1., 0.],
        [1., 1., 1., 0.],
        [1., 1., 1., 0.]],

       [[1., 1., 1., 0.],
        [1., 1., 1., 0.],
        [1., 1., 1., 0.],
        ...,
        [1., 1., 1., 0.],
        [1., 1., 1., 0.],
        [1., 1., 1., 0.]]], dtype=float32)
>>> plt.imshow(img)
image.png

若長寬各取250個(gè)像素點(diǎn)則:

>>> plt.imshow(img[list(range(250)),:][:,list(range(250))])
image.png

所以我們的目標(biāo)就是由一些像素rgba數(shù)值隨機(jī)生成的圖片進(jìn)化成一個(gè)火狐形狀的圖片。


image.png

結(jié)果

最后效果我感覺還行,雖然跟原圖比的話,還是差了一些。程序運(yùn)行后,其實(shí)很快就能看到大致雛形,但是細(xì)節(jié)上就不行了。


image.png

image.png

上面圖片中,g后面的數(shù)字表示代數(shù),最后一個(gè)"_"后面的額數(shù)字就是相似度分?jǐn)?shù)了,可以看到,一開始分值是幾百上千的下降,到后面就是個(gè)位數(shù)的下降了。
我在B站上上傳了個(gè)像素進(jìn)化過程的動(dòng)畫,感興趣的可以去看看小視頻

代碼組成講解

程序運(yùn)行環(huán)境:

  • Win10
  • Python3 (需要安裝兩個(gè)庫:matplotlib、numpy)

目標(biāo)圖片

火狐

編碼

我們直接使用像素色值作為基因。那么,初始化一個(gè)個(gè)體的所有基因,則可以第一個(gè)函數(shù):

def init_genes(id, x, y, z):
    """初始化個(gè)體基因
    :param id:  隨機(jī)數(shù)種子,與其他初始化相區(qū)別
    :param x: 圖片高
    :param y: 圖片長
    :param z: 4表示rgba, 3表示rgb
    """
    np.random.seed(id)
    pixel = np.random.random((x, y, z))
    return pixel

通過隨機(jī)生成一個(gè)維數(shù)(x, y, z)大小的的ndarray,其中數(shù)值在[0,1)之間。比如生成一個(gè)500*500像素大小的rgba圖,則:

>>> img = init_genes(1, 500, 500, 4)
>>> img.shape
(500, 500, 4)
>>> img
array([[[4.17022005e-01, 7.20324493e-01, 1.14374817e-04, 3.02332573e-01],
        [1.46755891e-01, 9.23385948e-02, 1.86260211e-01, 3.45560727e-01],
        [3.96767474e-01, 5.38816734e-01, 4.19194514e-01, 6.85219500e-01],
        ...,
        [2.54232355e-01, 8.40863542e-02, 8.64144095e-01, 4.47898916e-01],
        [5.61786261e-01, 7.36710958e-01, 7.96488867e-01, 4.47508139e-01],
        [1.84127556e-01, 8.28732852e-01, 3.09979598e-02, 9.46728270e-01]],
       ...,

        [[5.68421936e-01, 3.39263061e-01, 4.29541410e-02, 4.75883106e-01],
        [3.49561943e-02, 6.97079682e-01, 2.07790075e-01, 2.31854801e-01],
        [3.56860024e-01, 7.54193790e-01, 5.53116793e-01, 5.94979902e-01],
        ...,
        [4.51152445e-01, 1.17319322e-01, 6.25857591e-01, 6.13930562e-01],
        [1.19037827e-01, 1.30051715e-01, 6.84848503e-01, 4.96384839e-02],
        [4.26193435e-01, 4.93233752e-01, 1.08809709e-01, 3.70251829e-01]]])

適應(yīng)度函數(shù)

由于我們的目的簡單,就是讓進(jìn)化的個(gè)體圖片與目標(biāo)圖片盡量相似。那么適應(yīng)度函數(shù)就直接用進(jìn)化個(gè)體和目標(biāo)進(jìn)行比較,計(jì)算得分。

def comp_similarity(indv, target):
    """計(jì)算相似度,計(jì)算兩個(gè)array的差值平方和,即越小,相似度越大。
    :param indv: 圖片像素
    :param target: 目標(biāo)圖片像素
    """
    score = np.sum(np.square((indv - target)))
    return score

初始化群體

根據(jù)目標(biāo)圖片的大小,初始化群體個(gè)體的大小,并根據(jù)目標(biāo)圖片像素,計(jì)算群體個(gè)體與目標(biāo)圖片的相似度。

def init_indv(id, x, y, z, target):
    """初始化個(gè)體數(shù)據(jù),初始話像素基因,與目標(biāo)相似度分?jǐn)?shù)
    :param id: 一個(gè)數(shù)字,與其他初始化相區(qū)別
    :param x: 圖片高
    :param y: 圖片長
    :param z: 4表示rgba, 3表示rgb
    :param target: 目標(biāo)像素
    """
    pixel = init_genes(id, x, y, z)
    score = comp_similarity(pixel, target)
    indv = {}
    indv['score'] = score
    indv["gene"] = pixel
    return indv

def init_pop(target, p=15, jobs=5):
    """初始化群體數(shù)據(jù)
    :param target: 目標(biāo)像素
    :param p: 群體大小
    :param jobs: 進(jìn)程數(shù)
    """
    x, y, z = target.shape
    f_init = partial(init_indv, x=x, y=y, z=z,target=target)
    with Pool(jobs) as pl:
        for i,v in enumerate(pl.map(f_init, list(range(p)))):
            data_pool[i] = v

繁衍,變異下一代

這里變異的話,我認(rèn)為是比較trick的一個(gè)地方。一開始,我是直接一個(gè)一個(gè)的像素點(diǎn)的色值數(shù)據(jù)突變,不過我發(fā)現(xiàn)進(jìn)化太慢。
后來我想到,正常圖片的話,一個(gè)像素點(diǎn)附近的像素,也基本上是相同的像素。于是呢,突變時(shí),我就隨機(jī)選擇一些像素點(diǎn),以它為中心,周圍n個(gè)像素點(diǎn)的色值都設(shè)置成該像素的色值,這樣,我發(fā)現(xiàn)像素可能能快速成型,但是,后面也是沒法進(jìn)化了,因?yàn)閷⒅車O(shè)置一樣的色值,缺少多樣性,導(dǎo)致后期很難進(jìn)化。
然后,我就想設(shè)成一樣的缺少多樣性,那就設(shè)成不一樣的,以當(dāng)前像素色值為均值,標(biāo)準(zhǔn)差設(shè)置0.05(也可以設(shè)成其他)隨機(jī)生成正太分布的數(shù)值了。這樣突變多樣性就就有了。

def breed(p1, p2, mutation, width=5):
    """初始化群體數(shù)據(jù)
    :param p1: 個(gè)體1
    :param p2: 個(gè)體2
    :param mutation: 突變率
    :param width: 變異寬度
    """
    x, y, z = p1.shape
    new_p = p1.copy()
    x1_idx = random.sample(range(x), int(x/2))
    y1_idx = random.sample(range(y), int(y/2))
    x2_idx = list(set(range(x)) - set(x1_idx))
    y2_idx = list(set(range(y)) - set(y1_idx))

    temp1 = p2[x1_idx]
    temp1[:,y1_idx] = p1[x1_idx][:, y1_idx]
    new_p[x1_idx] = temp1

    temp2 = p2[x2_idx]
    temp2[:,y2_idx] = p1[x2_idx][:, y2_idx]
    new_p[x2_idx] = temp2

    m_x = random.sample(range(x), int(x*mutation))
    m_y = random.sample(range(y), int(x*mutation))

    indv_m = new_p.copy()
    for i,j in zip(m_x, m_y):
        channel = random.randint(0,z-1)
        center_p = new_p[i,j][channel]
        sx = list(range(max(0, i-width), min(i+width, x-1)))
        sy = list(range(max(0, j-width), min(j+width, y-1)))
        mtemp = indv_m[sx]
        normal_rgba = np.random.normal(center_p,.01, size=mtemp[:,sy].shape[:2])
        normal_rgba[normal_rgba>1] = 1
        normal_rgba[normal_rgba<0] = 0
        mtemp[:,sy, channel] = normal_rgba
        indv_m[sx] = mtemp
    return new_p, indv_m

選擇,交叉

每一代,隨機(jī)選擇n對個(gè)體進(jìn)行交叉繁殖,繁殖下的個(gè)體后,對整個(gè)種群個(gè)體的適應(yīng)度進(jìn)行比較,選擇出最差的部分個(gè)體淘汰,以維持種群大小一致。
這里我處理的簡單,就不放代碼了。

全部代碼:

代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容