基因遗传算法--demo和一些尝试(二)

超级血月当天晚上持续更新, 接着上一篇:
基因遗传算法–demo和一些尝试
这篇文章记录了我思考DNAr激活算法和整个群落进化设计的一些想法过程, 高手请轻拍指导交流, 不想看大段文字, 直接拖到最下方看结论。

我们尝试更换了DNA变异的算法, 简化了原来的算法, DNA的变化尺度, 不再与自己的大小有关系, 由DNAr直接影响。

def mutate_dna(dna, r):
    """
    r为DNA变化幅度的因子, 即DNAr
    """
    return dna + ( r / (2*np.e))**3

def mutate_self(x):
    """不受到其他基因控制的, 控制其他基因变化率的基因"""
    x = x + np.random.randn(len(x))
    return x

def mutate(df):
    df.r = mutate_self(df.r)
    df.d = mutate_dna(df.d, df.r)
    return df

为什么选择r/(2*np.e)作为DNAr的激活函数, 看函数曲线

《基因遗传算法--demo和一些尝试(二)》 DNAr的激活函数

选取这个激活函数的目标是想让DNAr控制DNA具有如下特点:

  1. 遇到较优解的时候, DNA要收敛, DNAr趋于0, 并且新的变异x + np.random.randn(len(x))不会使得DNA大幅震荡,DNAr在0附近的时候, DNA的变化是远小于1的, 并且有利于寻找较优解的具体值。
  2. 在发现更优的解的时候, 整个群落会更快的迁移过去。可以看到<-5, >5的时候, 函数都比较陡峭, 这个时候会让群落加速冲向最优解。

运行修改后的代码, 看效果
下面是target从-7到1888的时候, 群落DNA分布和DNAr的分布

(下面是30步数一个图, 如之前代码所示):

《基因遗传算法--demo和一些尝试(二)》 开始的时候, 群落位于-7附近

《基因遗传算法--demo和一些尝试(二)》 群落已经找到了最优解, 并且冲过头了

《基因遗传算法--demo和一些尝试(二)》 冲过头, 错过了最优解, 但是看到DNAr已经在减小

《基因遗传算法--demo和一些尝试(二)》 往回冲, DNAr又开始加速了

《基因遗传算法--demo和一些尝试(二)》 震荡

《基因遗传算法--demo和一些尝试(二)》 继续震荡, 但是DNAr震荡的幅度和DNA震荡的幅度都在减小

《基因遗传算法--demo和一些尝试(二)》 小幅震荡

《基因遗传算法--demo和一些尝试(二)》 小幅震荡

《基因遗传算法--demo和一些尝试(二)》 逐渐稳定了

《基因遗传算法--demo和一些尝试(二)》 稳定中小幅震荡

《基因遗传算法--demo和一些尝试(二)》 稳定中小幅震荡

最后群落收敛在最优解附近, 整个过程不难发现一个问题, 震荡过大了, 在DNA找到最优解后, 反复震荡好久, 群落才稳定下来。群落在冲向最优解的时候, 如果最优解足够远, 会让DNAr每一步都会增加一点, 好处是DNA加速冲向结果, 坏处是最后DNA最优的时候, DNAr却处于一个较大的值, (有点深度学习优化算法Momentum的感觉), 这个时候会刹不住车, 继续让DNA向着原有的方向冲下去。直观感受就是整个群落加速冲向最优解, 冲过最优解, 冲向更远处, 最后会像钟摆一样, 反复围着最优解震荡一会儿才收敛。

如何优化呢, 一个想法就是减小( r / (2*np.e))**3, 但是减小了DNAr对DNA的影响, 实际DNA冲过头的劲头会降低, 但是群落迁移速度也降低了, 我们要找一个种制约动量过头的机制, 把冲过头的动量给拉住。首先想到的就是, 能不能借鉴深度学习算法中已经存在的诸多算法呢?

由于总是记不住那些算法, 还专门又看了下大多数优化算法, 大致了解了其核心思想, 于是就有了《深度学习几种优化算法的笔记-一句话理解各种优化算法》此文。

  1. 仿照adam, 既有动量, 同时还存在一点点之前的制约, 我们已经有了动量, 制约DNAr一直变大的趋势, 简单的来说就是把最近的历史DNAr处理后, 作为参数去除DNAr, 这样, 在+, -空间震荡的时候, 有利于更快的减小DNAr的幅度, 但是当DNAr本身就很大的时候, 这个值必然会导致DNAr不能加速向前冲, 感觉有副作用, 因为我们的DNA实际没有归一化, 会导致DNAr的变化也不是归一化的, 加上上面的算法必然导致冲不动的感觉。

  2. 之前都被深度学习的优化算法给惯性思维了, 一直想到一个统一个规则来更新DNAr或者作为DNAr的激活函数, 别人是因为根本就只有一个方法可更新, 我们其实一个群落,随机产生多个更新方法, 按照一定比例, 在不同的情况下分配就好。
    顺着这个思路, 就想到了“召唤祖先”这个算法。 形象的来说, 就是群落的长老发现,一代不如一代的时候, 决定牺牲部分生存空间, 来召唤祖先, 把祖上最牛逼的那帮人给召唤出来, 重新为群落注入活力, 当然召唤出来的祖先DNAr被置为0。

中国人也经常尊古, 存在即合理。哈哈

那么, 其实我们就需要再维护一张表, 姑且成为名人堂吧。但是考虑到以后的需求, 我们先豪爽的记录所有的祖先, 太大就放到硬盘上, 再大再说吧。

  1. 前面的两个想法, 第一个忽略了, 相比深度学习优化算法, 我们没有计算梯度, 而所有的优化算法, 都是基于梯度的计算, 在有个前提的方向的前提下, 调整其梯度;第二个, 现在用出来, 为时过早, 因为现在我们只有一个基因, 多几个基因共同组成个体来适应环境呢?明显不合适。
    于是第三个方法应运而生, 也是早应该想到的, 目前的进化方式, 一旦DNA变化起来, 所有的DNA都在变化, 甚至还有加速变化的趋势, 但是当多个DNA存在的时候, 这种变化和震荡是的问题更加复杂, 那么其实应该想到, 传统基因算法里面的基因, 可没那么容易变化, 别人是在交换染色体后, 随机选择一点基因变化的。所以, 如果我们要所有的基因都变化, 其实应该有个锁, 有很大几率, 基因不会发生变化。这个锁基因的概率, 姑且作为一个超参数吧。

基于上面的第三点想法, 我们修改了代码, 并且支持多个DNA和对应的DNAr, 直接上代码:

import numpy as np
from matplotlib import pyplot as plt
import pandas as pd
_delta = 0.001

survive_rate = 0.1
parent_rate = 1 - 0.1 - 0.3 - survive_rate
global_target = [88, -7, 188, 150]
global_lock = 0.2

def sigmoid(x):
  return 1/(1+np.exp(-x))

def sigmoid_0_m1_1(x):
    """通过sigmoid获取一个分布在-1到1之间, 中心点为0的中心对称的分布, X={R}"""
    return 2/(1+np.exp(-x)) - 1

def sigmoid_1_0_2(x):
    """通过sigmoid实现的, 分布在0~2之间, 中心点在1的分布"""
    return 2/(1+np.exp(-x))

def get_adapt_point(x):
    """获取归一化的分数"""
    p = np.sum(-(df.d - np.array(global_target))**2, axis=1)
#     p = -(x - global_target) ** 2
    p -= p.min()
    p /= (p.sum() + _delta)
    return p

def get_rank_point(x):
    """获取归一化的排名分数, 越靠前, 分数越高"""
    #把资源向头部集中
    rank = x.rank(method="first")
    rank = (rank / len(rank))**2
    rank = rank / (sum(rank) + _delta) #归一化
    return rank

def spawn(show=False):
    iterables = [['d', 'r'], ["a", "b", "c", "d"]]
    df = pd.DataFrame(np.random.randn(100, 8), columns=pd.MultiIndex.from_product(iterables, names=['layer', 'type']))
    if show:
        print("--------------创世纪-----------------")
        df.hist()
        plt.show()
    return df
def mutate_dna(dna, r):
    """
    r为DNA变化幅度的因子, 由另外一个DNA决定
    """
    return dna + ( r / (2*np.e))**3

def mutate_self(x):
    """不受到其他基因控制的, 控制其他基因变化率的基因"""
    x = np.where(np.random.rand(len(x), x.shape[1]) < global_lock, 0, x + np.random.randn(len(x), x.shape[1]))
    return x

def mutate(df):
    df.r = mutate_self(df.r)
    df.d = mutate_dna(df.d, df.r)
    return df

def evolution(df):
    #首先判断其适应度
    p = get_adapt_point(df.d)
    p += get_rank_point(p) * 10#这个rank非常重要, 有利于大大加强优势的集中
#     p = p / sum(p) 不用在归一化了, 有利于对已经有优势的调整
    #TODO  家族加成
    #TODO  其他加成
    #TODO: 是否对整个进化过程中的个体, 按寿命死亡
    
    df['p'] = p
    
#     print(df)
    
    #遗传变异
    #生存策略, 先取survive_rate保留, 剩余的死亡
    survive = df.sample(frac=survive_rate, weights=df.p)
    
    #取前10%为必定会做parent的, 能够繁殖多次
#     elites = df.sample(frac=0.1, weights=df.p)
#     elites2 = df.sample(frac=0.1, weights=df.p)
#     elites2 = df.sample(frac=0.1, weights=df.p)
    
    parents = df.sample(frac=parent_rate, weights=df.p)
    
    children = mutate(parents.append([df.sample(frac=0.1, weights=df.p), df.sample(frac=0.3, weights=df.p)]))
    
    return survive.append(children), survive, children

df = spawn()

for i in range(0, 100):
    df, survive, children = evolution(df)
    if i % 20 == 0:
        print("="*100)
        print("the {}th. dna".format(i))
#         plt.figure(figsize=(18, 5))
#         plt.subplot(1, 2, 1)
#         plt.hist([survive.d, children.d], stacked=True, color=['green', 'yellow'], bins=20)
        df.d.hist()
        plt.show()
#         plt.subplot(1, 2, 2)
        df.r.hist(color='green')
        plt.show()
        
df.d.hist()
df.r.hist(color='green')
plt.show()
# plt.figure(figsize=(18, 5))
# plt.subplot(1, 2, 1)
# plt.hist([survive.d, children.d], stacked=True, color=['green', 'yellow'], bins=20)
# plt.subplot(1, 2, 2)
# plt.hist(df.r, bins=20)
# plt.show()

print("======================the end========================")

结果如下:

《基因遗传算法--demo和一些尝试(二)》 开始的时候DNA

《基因遗传算法--demo和一些尝试(二)》 开始的时候DNAr
《基因遗传算法--demo和一些尝试(二)》 100次之后DNA

《基因遗传算法--demo和一些尝试(二)》 100次之后DNAr

总结图片就不贴了, 有兴趣的同学可以自己跑跑代码, 改改参数, 如何修改超参数, 来适应较多的情况, 让他们快速收敛, 这个因目标不同而不同吧,我们应该尽可能的想法, 让参数具有自调节性, 而不是人来调整参数, 对于需要人来调整的参数, 估计也用多次模型的方法, 来找到最优解吧。

结论:
用一个随机数, 直接在一定几率下, 让DNAr直接归0,这样, 就有一部分守旧的DNA会为狂奔的DNA才踩下急刹车。

思考:

  1. 是否让DNA具有归一性, 都在一个大致的范围, 这样参数更好设置;

  2. 如何保持群体活性, 动态的让群体的聚集范围产生类似于呼吸的效果。对于一个群落, 我们用DNA的相似度, 来计算一个群落的单一性, 如果过于单一, 就是收敛到了一个很小的区间, 这个必然是我们不想要的结果, 我们就触发一种机制, 让整个网络突变, 发散开来, 这样, 整个群落的范围就好像呼吸一样, 注意一定要呼吸有度, 不然刚发散,有快速聚拢了。

那再扯远点点, 什么时候像上帝灭掉恐龙一样, 灭掉大多数群落, 来为人类腾出生存空间呢?
估计人工干预才是最好的。机器设定的规则到呼吸+扰动, 就够了。以我们的目标而言(优化深度学习网络), 用这些方法 已经够计算机运行很久了。远远不到需要用灭世的方法来寻找更优解。而且我极度怀疑, 没有人这样做的根本原因, 是因为实在计算机算不过来。

  1. 当群体发生分化的时候, 如何分家。

当然, 我也意思到一个问题, 上面的代码对DNA的异变, 更多的是连续性的变化, 而非跳变, 不能突然有+100变动到-100, 这个是当前的设计方法说决定的。

    原文作者:拎着激光炮的野人
    原文地址: https://www.jianshu.com/p/e45d7c0f5651
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞