遗传算法

要点 
如果对遗传算法有兴趣的朋友, 强烈推荐先看看我制作的动画短片 什么是遗传算法, 在动画里有了基础的了解, 在接下来的内容中, 你就如鱼得水啦. 如果让我用一句话概括遗传算法: “在程序里生宝宝, 杀死不乖的宝宝, 让乖宝宝继续生宝宝”.


在这一节中, 我们的 “乖宝宝” 就是图中更高的点, 用遗传算法, 我们就能轻松找到 “最乖的宝宝”.


 遗传算法


找一个好的fitness方程 
所有的遗传算法 (Genetic Algorithm), 后面都简称 GA, 我们都需要一个评估好坏的方程, 这个方程通常被称为 fitness. 在今天的问题中, 我们找到下面这个曲线当中的最高点. 那么这个 fitness 方程就很好定, 越高的点, fitness 越高.


 遗传算法


如果这个曲线上任一点的 y 值是 pred 的话, 我们的 fitness 就是下面这样:


def get_fitness(pred):
    return pred
DNA 编码 
在 GA 中有基因, 为了方便, 我们直接就称为 DNA 吧. GA 中第二重要的就是这 DNA 了, 如何编码和解码 DNA, 就是你使用 GA 首先要想到的问题. 传统的 GA 中, DNA 我们能用一串二进制来表示, 比如:


DNA1 = [1, 1, 0, 1, 0, 0, 1]
DNA2 = [1, 0, 1, 1, 0, 1, 1]
为什么会要用二进制编码, 我们之后在下面的内容中详细说明这样编码的好处. 但是长成这样的 DNA 并不好使用. 如果要将它解码, 我们可以将二进制转换成十进制, 比如二进制的 11 就是十进制的 3. 这种转换的步骤在程序中很好执行. 但是有时候我们会需要精确到小数, 其实也很简单, 只要再将十进制的数浓缩一下就好. 比如我有 1111 这么长的 DNA, 我们产生的十进制数范围是 [0, 15], 而我需要的范围是 [-1, 1], 我们就将 [0, 15] 缩放到 [-1, 1] 这个范围就好.


def translateDNA(pop):
    return pop.dot(2 ** np.arange(DNA_SIZE)[::-1]) / float(2**DNA_SIZE-1) * X_BOUND[1]
注意, 这里的 pop 是一个储存二进制 DNA 的矩阵, 他的 shape 是这样 (pop_size, DNA_size).




 
进化啦 
进化分三步:


适者生存 (selection)
DNA 交叉配对 (crossover)
DNA 变异 (mutation)
我们用 python 的三个功能, 一个循环表示:


# 种群 DNA
pop = np.random.randint(2, size=(POP_SIZE, DNA_SIZE))


F_values = F(translateDNA(pop))
fitness = get_fitness(F_values)
pop = select(pop, fitness)      # 按适应度选 pop
pop_copy = pop.copy()           # 备个份
for parent in pop:
    child = croseeover(parent, pop_copy)
    child = mutate(child)
    parent[:] = child           # 宝宝变大人
适者生存的 select() 很简单, 我们只要按照适应程度 fitness 来选 pop 中的 parent 就好. fitness 越大, 越有可能被选到.


def select(pop, fitness):
    idx = np.random.choice(np.arange(POP_SIZE), size=POP_SIZE, replace=True,
                           p=fitness/fitness.sum()) # p 就是选它的比例
    return pop[idx]
接下来进行交叉配对. 方式很简单. 比如这两个 DNA, Y 的点我们取 DNA1 中的元素, N 的点取 DNA2 中的. 生成的 DNA3 就有来自父母的基因了.


DNA1 = [1, 1, 0, 1, 0, 0, 1]
       [Y, N, Y, N, N, Y, N]
DNA2 = [1, 0, 1, 1, 0, 1, 1]


DNA3 = [1, 0, 0, 1, 0, 0, 1]
而 python 写出来也很方便, 从 pop_copy 中随便选一个当另一个父辈 和 parent 进行随机的 crossover:


def crossover(parent, pop):
    if np.random.rand() < CROSS_RATE:
        i_ = np.random.randint(0, POP_SIZE, size=1)  # select another individual from pop
        cross_points = np.random.randint(0, 2, DNA_SIZE).astype(np.bool)  # choose crossover points
        parent[cross_points] = pop[i_, cross_points]  # mating and produce one child
    return parent
mutation 就更好写了, 将某些 DNA 中的 0 变成 1, 1 变成 0.


def mutate(child):
    for point in range(DNA_SIZE):
        if np.random.rand() < MUTATION_RATE:
            child[point] = 1 if child[point] == 0 else 0
    return child
有了这些规则, select, crossover, mutate, 我们就能在程序里上演进化论啦. 赶紧运行一下我在github的这套全部代码.


接下来几节内容, 我们就来看看在不同的情况中如何根据不同的标准选择 fitness 和 DNA 编码.




----------------------------------


如果对遗传算法有兴趣的朋友, 强烈推荐先看看我制作的动画短片 什么是遗传算法, 在动画里有了基础的了解, 在接下来的内容中, 你就如鱼得水啦.


接着上节对遗传算法的基本应用, 在这一节中, 我们用通过不同的编码 DNA 方式, 不同的 fitness 定义方式来让程序生成出自己设定的句子来.


fitness 和 DNA 
上次我们提到过 GA 中最重要的就是怎么定义 fitness function, 怎么给 DNA 编码. 这次我们来句另一个例子. 比如我们有一个要生成的句子:


Target:    You get it!
Generate:  YhtBget i@!
可以想象, 我们能够用这个句子长度的 DNA 来生成这个句子. 每个 DNA 代表一个字母. 如果对上的字母越多, 我的 fitness 就越高. 因为用一个 class 来代表 GA 会比较方便, 我们之后都用 class 来写.


class GA:
    def get_fitness(self):             # count how many character matches
        match_count = (self.pop == TARGET_ASCII).sum(axis=1)
        return match_count
而 DNA 呢, 可以都用数字, 而且可以用 ASCII 编码. 将数字转化成字符, 或者字符转数字都可以, 我们为了统一, DNA 都用数字形式.


class GA:
    def translateDNA(self, DNA):    # convert to readable string
        return DNA.tostring().decode('ascii')
而字符转数字可以用 numpy 的这个功能:


>>> np.fromstring('dasd@', dtype=np.uint8)
# array([100,  97, 115, 100,  64], dtype=uint8)


 
进化啦 
如果 GA 用一个 class 代替, 那 select, mutate, crossover 都是 class 里的功能了.


class GA:
    def select(self):


    def crossover(self, parent, pop):


    def mutate(self, child):
上面这三个功能的算法和上节内容差不多. 所以不会再详细说明了. 你也可以去我的 github 看全部代码. 但是这个 class 中还有一个功能来将上面的三个功能联系起来. 其实这就是上节内容里面的 forloop 中的内容.


class GA:
    def evolve(self):
        pop = self.select()
        pop_copy = pop.copy()
        for parent in pop:  # for every parent
            child = self.crossover(parent, pop_copy)
            child = self.mutate(child)
            parent[:] = child
        self.pop = pop
有了上面定义的这些功能, 再将其他的小部分补全. 我们就能很容易的使用这个 GA class 了.


ga = GA(DNA_size=DNA_SIZE, DNA_bound=ASCII_BOUND, cross_rate=CROSS_RATE,
            mutation_rate=MUTATION_RATE, pop_size=POP_SIZE)


for generation in range(N_GENERATIONS):
    fitness = ga.get_fitness()
    best_DNA = ga.pop[np.argmax(fitness)]
    best_phrase = ga.translateDNA(best_DNA)
    print('Gen', generation, ': ', best_phrase)
    if best_phrase == TARGET_PHRASE:
        break
    ga.evolve()


"""
Gen 0 :  !hT'ge0[px$
Gen 1 :  !n#'ged[p&!
Gen 2 :  YHJA(er6QM!
Gen 3 :  8=K@ge  "tZ
Gen 4 :  ThTVKet X7!
Gen 5 :  'oJ@ge06iM!
...
Gen 179 :  Youqget it!
Gen 180 :  You'get it!
Gen 181 :  You get it!
"""
赶紧试试这套github的全部代码.


接下来几节内容, 我们就来看看在不同的情况中如何根据不同的标准选择 fitness 和 DNA 编码.
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值