超级血月当天晚上持续更新, 接着上一篇:
基因遗传算法–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的激活函数, 看函数曲线
DNAr的激活函数
选取这个激活函数的目标是想让DNAr控制DNA具有如下特点:
- 遇到较优解的时候, DNA要收敛, DNAr趋于0, 并且新的变异
x + np.random.randn(len(x))
不会使得DNA大幅震荡,DNAr在0附近的时候, DNA的变化是远小于1的, 并且有利于寻找较优解的具体值。 - 在发现更优的解的时候, 整个群落会更快的迁移过去。可以看到<-5, >5的时候, 函数都比较陡峭, 这个时候会让群落加速冲向最优解。
运行修改后的代码, 看效果
下面是target从-7到1888的时候, 群落DNA分布和DNAr的分布
(下面是30步数一个图, 如之前代码所示):
开始的时候, 群落位于-7附近
群落已经找到了最优解, 并且冲过头了
冲过头, 错过了最优解, 但是看到DNAr已经在减小
往回冲, DNAr又开始加速了
震荡
继续震荡, 但是DNAr震荡的幅度和DNA震荡的幅度都在减小
小幅震荡
小幅震荡
逐渐稳定了
稳定中小幅震荡
稳定中小幅震荡
最后群落收敛在最优解附近, 整个过程不难发现一个问题, 震荡过大了, 在DNA找到最优解后, 反复震荡好久, 群落才稳定下来。群落在冲向最优解的时候, 如果最优解足够远, 会让DNAr每一步都会增加一点, 好处是DNA加速冲向结果, 坏处是最后DNA最优的时候, DNAr却处于一个较大的值, (有点深度学习优化算法Momentum的感觉), 这个时候会刹不住车, 继续让DNA向着原有的方向冲下去。直观感受就是整个群落加速冲向最优解, 冲过最优解, 冲向更远处, 最后会像钟摆一样, 反复围着最优解震荡一会儿才收敛。
如何优化呢, 一个想法就是减小( r / (2*np.e))**3
, 但是减小了DNAr对DNA的影响, 实际DNA冲过头的劲头会降低, 但是群落迁移速度也降低了, 我们要找一个种制约动量过头的机制, 把冲过头的动量给拉住。首先想到的就是, 能不能借鉴深度学习算法中已经存在的诸多算法呢?
由于总是记不住那些算法, 还专门又看了下大多数优化算法, 大致了解了其核心思想, 于是就有了《深度学习几种优化算法的笔记-一句话理解各种优化算法》此文。
仿照adam, 既有动量, 同时还存在一点点之前的制约, 我们已经有了动量, 制约DNAr一直变大的趋势, 简单的来说就是把最近的历史DNAr处理后, 作为参数去除DNAr, 这样, 在+, -空间震荡的时候, 有利于更快的减小DNAr的幅度, 但是当DNAr本身就很大的时候, 这个值必然会导致DNAr不能加速向前冲, 感觉有副作用, 因为我们的DNA实际没有归一化, 会导致DNAr的变化也不是归一化的, 加上上面的算法必然导致冲不动的感觉。
之前都被深度学习的优化算法给惯性思维了, 一直想到一个统一个规则来更新DNAr或者作为DNAr的激活函数, 别人是因为根本就只有一个方法可更新, 我们其实一个群落,随机产生多个更新方法, 按照一定比例, 在不同的情况下分配就好。
顺着这个思路, 就想到了“召唤祖先”这个算法。 形象的来说, 就是群落的长老发现,一代不如一代的时候, 决定牺牲部分生存空间, 来召唤祖先, 把祖上最牛逼的那帮人给召唤出来, 重新为群落注入活力, 当然召唤出来的祖先DNAr被置为0。
中国人也经常尊古, 存在即合理。哈哈
那么, 其实我们就需要再维护一张表, 姑且成为名人堂吧。但是考虑到以后的需求, 我们先豪爽的记录所有的祖先, 太大就放到硬盘上, 再大再说吧。
- 前面的两个想法, 第一个忽略了, 相比深度学习优化算法, 我们没有计算梯度, 而所有的优化算法, 都是基于梯度的计算, 在有个前提的方向的前提下, 调整其梯度;第二个, 现在用出来, 为时过早, 因为现在我们只有一个基因, 多几个基因共同组成个体来适应环境呢?明显不合适。
于是第三个方法应运而生, 也是早应该想到的, 目前的进化方式, 一旦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========================")
结果如下:
开始的时候DNA
开始的时候DNAr
100次之后DNA
100次之后DNAr
总结图片就不贴了, 有兴趣的同学可以自己跑跑代码, 改改参数, 如何修改超参数, 来适应较多的情况, 让他们快速收敛, 这个因目标不同而不同吧,我们应该尽可能的想法, 让参数具有自调节性, 而不是人来调整参数, 对于需要人来调整的参数, 估计也用多次模型的方法, 来找到最优解吧。
结论:
用一个随机数, 直接在一定几率下, 让DNAr直接归0,这样, 就有一部分守旧的DNA会为狂奔的DNA才踩下急刹车。
思考:
是否让DNA具有归一性, 都在一个大致的范围, 这样参数更好设置;
如何保持群体活性, 动态的让群体的聚集范围产生类似于呼吸的效果。对于一个群落, 我们用DNA的相似度, 来计算一个群落的单一性, 如果过于单一, 就是收敛到了一个很小的区间, 这个必然是我们不想要的结果, 我们就触发一种机制, 让整个网络突变, 发散开来, 这样, 整个群落的范围就好像呼吸一样, 注意一定要呼吸有度, 不然刚发散,有快速聚拢了。
那再扯远点点, 什么时候像上帝灭掉恐龙一样, 灭掉大多数群落, 来为人类腾出生存空间呢?
估计人工干预才是最好的。机器设定的规则到呼吸+扰动, 就够了。以我们的目标而言(优化深度学习网络), 用这些方法 已经够计算机运行很久了。远远不到需要用灭世的方法来寻找更优解。而且我极度怀疑, 没有人这样做的根本原因, 是因为实在计算机算不过来。
- 当群体发生分化的时候, 如何分家。
当然, 我也意思到一个问题, 上面的代码对DNA的异变, 更多的是连续性的变化, 而非跳变, 不能突然有+100变动到-100, 这个是当前的设计方法说决定的。