From 3410de39049a6ccc606cedd4b5d248f150718782 Mon Sep 17 00:00:00 2001 From: yong Date: Fri, 29 Sep 2023 08:06:17 -0600 Subject: [PATCH] Update readme for history015_testinput3 --- README.md | 7 +- core/pom.xml | 4 +- .../com/gitee/drinkjava2/frog/Animal.java | 164 +++++++------- .../java/com/gitee/drinkjava2/frog/Env.java | 4 +- .../drinkjava2/frog/brain/BrainPicture.java | 4 +- .../gitee/drinkjava2/frog/brain/Consts.java | 89 ++++++++ .../gitee/drinkjava2/frog/brain/Genes.java | 50 ++--- .../com/gitee/drinkjava2/frog/egg/Egg.java | 3 +- .../drinkjava2/frog/egg/FrogEggTool.java | 12 +- .../drinkjava2/frog/objects/OneDotEye.java | 28 ++- .../drinkjava2/frog/temp/TestInput3.java | 201 ++++++++++++++++++ .../drinkjava2/frog/util/ColorUtils.java | 2 +- .../gitee/drinkjava2/frog/util/GeneUtils.java | 9 +- .../drinkjava2/frog/util/RandomUtils.java | 56 ++--- history/014_3cells/README.md | 27 ++- history/015_testinput3/TestInput3.java | 201 ++++++++++++++++++ result21_input3.png | Bin 0 -> 17379 bytes 17 files changed, 685 insertions(+), 176 deletions(-) create mode 100644 core/src/main/java/com/gitee/drinkjava2/frog/brain/Consts.java create mode 100644 core/src/main/java/com/gitee/drinkjava2/frog/temp/TestInput3.java create mode 100644 history/015_testinput3/TestInput3.java create mode 100644 result21_input3.png diff --git a/README.md b/README.md index c98db50..fc0affd 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,12 @@ 这些全局参数是跟随青蛙终身的,一旦青蛙孵化出来就不再动了,在程序里把所有常量放在一个数组里,用遗传算法来控制,基本规则是小变动有大概率发生,大变动有小概率发生。青蛙的参数分为两类,一类是与空间位置相关的,如脑细胞是否会出现在某个空间位置,一类是与位置无关的,如每个细胞激活后向其它细胞发送多少能量。前者要放到分裂算法里,用一串8/4/2叉基因树来控制空间分布,后者就没必要这么浪费了,直接用一组全局数字表示即可,并用遗传算法来随机变异和筛选它们。在给神经网络编程时,如果碰到可以用全局常量来控制的参数,尽量不要手工赋值,而要用遗传算法来控制,因为多变量的优化组合筛选靠人力是不可能做好的。就拿这个例子来说,我压根不知道这些参数将会是多大,是正还是负,但是我知道应该有这些参数,这就够了。人工生命项目编程不讲究精准,思维模式要从传统的精细化编程转变为以概率、笼统、可能为导向的思维,大方向人为确定,细节交给电脑去算,这和大自然用遗传算法来筛选出脑细胞的参数是一个道理。 从本次更新可以看到,青蛙是工作在一个连续的信息流下面,信息是以脉冲方式在细胞之间互相传递大小不等的能量,可以说是一个最简单的脉冲神经网络大脑了。这个实验不很实美,没有实现一个不漏地吃掉食物但是又不空咬这个目标,但是人生苦短,我不想继续和这三个细胞缠斗下去了,后面将转到多个视觉细胞和引入苦甜奖惩信号细胞来影响洞(权重)的大小,这个会更有趣、更智能,也更接近模式识别任务。我认为通用智能就是模式识别与行为输出结合起来的系统,如果奖罚细胞和行为输出细胞在一开始就做为这个模式识别系统的一个组成部分,并且由遗传算法来筛选参数,那通用人工智能就不远了。 - + +2023-08-25 关于XOR逻辑及多个输入的测试 +最近因为参数范围太大,很难跑出结果,想试试把忆细胞也简化掉,直接视细胞在咬细胞上挖洞,这样就成了单层神经网络,问题变成传统的单层神经网络实现模式识别,也就是说多个输入信号转化为更精简的输出信号。群里曾有人发过实现异或XOR逻辑需要一个中间层的图片,这有点复杂,我在网上搜了一下“实际脑细胞实现xor逻辑”,发现一篇文章“大脑只需单个神经元就可进行XOR异或运算”,按照它的原理,想了一下,在二个输入细胞、一个执行细胞,且每个输出都有正负两种权重的情况下是可以实现所有正常逻辑操作的,即AND、OR、XOR三个逻辑,这里面的小窍门是把累加高于1的饱和信号削除掉。“正常”指的是排除掉输入全为0输出为1这种看着就没什么用的逻辑。 +![result21](result21_input3.png) +本次更新在015_testinput3目录下,只有一个TestInput3.java文件,这是个临时测试类,用穷举法来测试一个如果有三个视觉输入细胞、一个执行细胞的情况下能不能实现所有正常模式识别,即输入有2的7次方=128种组合,输出有0/1两种信号。实测发现这128种情况下有31种输入是无法通过调整权重来实现输出逻辑的,也就是说31种情况下是无解的。类似地,如果有4个输入,在32768组合中有29987种情况是无解的。所以如果输入信号大于两个时,必须改进结构,增加层数或在水平方向平铺增加输入输出细胞。 +目前有两个问题:一是用甜激素增加所有最近活跃的正权重、苦激素增加所有最近活跃的负权重这种方式能不能完全代替穷举法找到所有解;二是有没有办法只使用浅层神经网络实现模式识别?(人脑的皮层占了很大比重,顾名思义,皮层可能就是浅层的,而不是象深度学习一样是极深层的神经网络),这个模式识别不需要很精准,能达到约15x15的分辨率就可以接近人脑的模式识别功能了,因为人脑还会结合动眼和变焦两个功能来缩小需要进行模式识别信号的像素。这两个问题目前深度学习可能能找到答案,但我不知道有没有人采用这种正负权重双通道输入的结构,这种双通道输入结构实际上才是实际生物的神经网络构成。 ## 运行方式 | Run diff --git a/core/pom.xml b/core/pom.xml index 9198112..0dfce8f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -2,9 +2,9 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.gitee.drinkjava2 - frog014 + frog015 jar - 14.0 + 15.0 frog 当前目标是用分裂算法来实现第一个具备模式识别功能的神经网络 diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/Animal.java b/core/src/main/java/com/gitee/drinkjava2/frog/Animal.java index 8fe8b75..9e9ab92 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/Animal.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/Animal.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import javax.imageio.ImageIO; +import com.gitee.drinkjava2.frog.brain.Consts; import com.gitee.drinkjava2.frog.brain.Genes; import com.gitee.drinkjava2.frog.egg.Egg; import com.gitee.drinkjava2.frog.objects.Material; @@ -49,8 +50,7 @@ public abstract class Animal {// 这个程序大量用到public变量而不是ge public ArrayList> genes = new ArrayList<>(); // 基因是多个数列,有点象多条染色体。每个数列都代表一个基因的分裂次序(8叉/4叉/2叉)。 - public static final int CONSTS_LENGTH = 8; - public int[] consts = new int[CONSTS_LENGTH]; //常量基因,用来存放不参与分裂算法的全局常量,这些常量也参与遗传算法筛选,规则是有大概率小变异,小概率大变异,见constGenesMutation方法 + public int[] consts = new int[Consts.CountsQTY]; //常量基因,用来存放不参与分裂算法的全局常量,这些常量也参与遗传算法筛选,规则是有大概率小变异,小概率大变异,见constGenesMutation方法 /** brain cells,每个细胞对应一个神经元。long是64位,所以目前一个细胞只能允许最多64个基因,64个基因有些是8叉分裂,有些是4叉分裂 * 如果今后要扩充到超过64个基因限制,可以定义多个三维数组,同一个细胞由多个三维数组相同坐标位置的基因共同表达 @@ -100,13 +100,28 @@ public abstract class Animal {// 这个程序大量用到public变量而不是ge public void initAnimal() { // 初始化animal,生成脑细胞是在这一步,这个方法是在当前屏animal生成之后调用,比方说有一千个青蛙分为500屏测试,每屏只生成2个青蛙的脑细胞,可以节约内存 GeneUtils.geneMutation(this); //有小概率基因突变 - GeneUtils.constGenesMutation(this); //常量基因突变 + Consts.constMutation(this);//常量基因突变 if (RandomUtils.percent(40)) for (ArrayList gene : genes) //基因多也要适当小扣点分,防止基因无限增长 fat -= gene.size(); GeneUtils.createCellsFromGene(this); //根据基因,分裂生成脑细胞 } + public boolean active(int step) {// 这个active方法在每一步循环都会被调用,是脑思考的最小帧,step是当前屏的帧数 + // 如果fat小于0、出界、与非食物的点重合则判死 + if (!alive) { + return false; + } + if (fat <= 0 || Env.outsideEnv(xPos, yPos) || Env.bricks[xPos][yPos] >= Material.KILL_ANIMAL) { + kill(); + return false; + } + + holesReduce(); //所有细胞上的洞都随时间消逝,即信息的遗忘,旧的不去新的不来 + Genes.active(this, step); //调用每个细胞的活动,重要! + return alive; + } + private static final int MIN_FAT_LIMIT = Integer.MIN_VALUE + 5000; private static final int MAX_FAT_LIMIT = Integer.MAX_VALUE - 5000; @@ -122,31 +137,16 @@ public abstract class Animal {// 这个程序大量用到public变量而不是ge //没定各个等级的奖罚值,目前是手工设定的常数 public void awardAAAA() { changeFat(2000);} public void awardAAA() { changeFat(1000);} - public void awardAA() { changeFat(60);} + public void awardAA() { changeFat(100);} public void awardA() { changeFat(10);} public void penaltyAAAA() { changeFat(-2000);} public void penaltyAAA() { changeFat(-1000);} - public void penaltyAA() { changeFat(-60);} + public void penaltyAA() { changeFat(-100);} public void penaltyA() { changeFat(-10);} public void kill() { this.alive = false; changeFat(-5000000); Env.clearMaterial(xPos, yPos, animalMaterial); } //kill是最大的惩罚 //@formatter:on - public boolean active(int step) {// 这个active方法在每一步循环都会被调用,是脑思考的最小帧,step是当前屏的帧数 - // 如果fat小于0、出界、与非食物的点重合则判死 - if (!alive) { - return false; - } - if (fat <= 0 || Env.outsideEnv(xPos, yPos) || Env.bricks[xPos][yPos] >= Material.KILL_ANIMAL) { - kill(); - return false; - } - - //holesReduce(); //TODO: 所有细胞上的洞都随时间消逝,即信息的遗忘,旧的不去新的不来 - Genes.active(this, step); //调用每个细胞的活动,重要! - return alive; - } - public void show(Graphics g) {// 显示当前动物 if (!alive) return; @@ -166,65 +166,79 @@ public abstract class Animal {// 这个程序大量用到public变量而不是ge return cells[x][y][z] > 0; } - public void open(int x, int y, int z) { //打开指定的xyz坐标对应的cell能量值为极大 - energys[x][y][z] = 99999f; + public void setEng1(int x, int y, int z) { //打开指定的xyz坐标对应的cell能量值为极大 + energys[x][y][z] = 1; } - public void open(int[] a) { //打开指定的a坐标对应的cell能量值为极大 - energys[a[0]][a[1]][a[2]] = 99999f; + public void setEng1(int[] a) { //打开指定的a坐标对应的cell能量值为极大 + energys[a[0]][a[1]][a[2]] =1; } - public void close(int x, int y, int z) { //关闭指定的xyz坐标对应的cell能量值为0 + public void setEng0(int x, int y, int z) { //关闭指定的xyz坐标对应的cell能量值为0 energys[x][y][z] = 0; } - public void close(int[] a) {//关闭指定的a坐标对应的cell能量值为0 + public void setEng0(int[] a) {//关闭指定的a坐标对应的cell能量值为0 energys[a[0]][a[1]][a[2]] = 0; } public void addEng(int[] a, float e) {//指定的a坐标对应的cell能量值加e - if (cells[a[0]][a[1]][a[2]] == 0) - return; - energys[a[0]][a[1]][a[2]] += e; - if (energys[a[0]][a[1]][a[2]] < 0) - energys[a[0]][a[1]][a[2]] = 0f; - if (energys[a[0]][a[1]][a[2]] > 10) - energys[a[0]][a[1]][a[2]] = 10f; - } + addEng(a[0], a[1], a[2], e); + } public void addEng(int x, int y, int z, float e) {//指定的a坐标对应的cell能量值加e if (cells[x][y][z] == 0) return; - energys[x][y][z] += e; - } + float eng = energys[x][y][z] + e; + if (eng > 1) //如果饱和,不再增加,通过这个方法可以实现异或逻辑或更复杂的模式识别,详见TestInput3测试 + eng = 1; + if (eng < 0) //回到传统方式,细胞不允许出现负能量。(但是权值,即树突的正负两个通道中的负通道上,可以出现负信号,这个与实际细胞的抑制信号相似) + eng = 0; + energys[x][y][z] = eng; + } - public float get(int x, int y, int z) {//返回指定的a坐标对应的cell能量值 + public float getEng(int x, int y, int z) {//返回指定的a坐标对应的cell能量值 return energys[x][y][z]; } static final int HOLE_MAX_SIZE = 1000 * 1000; - public void digHole(int sX, int sY, int sZ, int tX, int tY, int tZ, int holeSize) {//在t细胞上挖洞,将洞的方向链接到源s,如果洞已存在,扩大洞, 新洞大小为1,洞最大不超过100 + public void digHole(int[] srcPos, int[] targetPos, int holeSize, int fresh) { + digHole(srcPos[0], srcPos[1], srcPos[2], targetPos[0], targetPos[1], targetPos[2], holeSize, fresh); + } + + public void digHole(int sX, int sY, int sZ, int[] targetPos, int holeSize, int fresh) { + digHole(sX, sY, sZ, targetPos[0], targetPos[1], targetPos[2], holeSize, fresh); + } + + public static final int HOLE_ARR_SIZE = 5; //洞由几个参数构成 + + + + //TODO: =================以下这些方法太复杂,删除除或重新整理================= + public void digHole(int sX, int sY, int sZ, int tX, int tY, int tZ, int size, int fresh) {//在t细胞上挖洞,将洞的方向链接到源s,如果洞已存在,扩大洞, 新洞大小为1,洞最大不超过100 if (!hasGene(tX, tY, tZ)) return; if (!Env.insideBrain(sX, sY, sZ)) return; if (!Env.insideBrain(tX, tY, tZ)) return; - if (get(tX, tY, tZ) < 1) //要调整 + if (getEng(tX, tY, tZ) < 1) //要调整 addEng(tX, tY, tZ, 1); //要调整 int[] cellHoles = holes[tX][tY][tZ]; - if (cellHoles == null) { //洞不存在,新建一个, 洞参数是一个一维数组,分别为源坐标X,Y,Z, 洞的大小,洞的新鲜度(TODO:待加) - holes[tX][tY][tZ] = new int[]{sX, sY, sZ, holeSize}; + if (cellHoles == null) { //洞不存在,新建一个, 洞参数是一个一维数组,分别为源坐标X,Y,Z, 洞的大小,洞的新鲜度 + holes[tX][tY][tZ] = new int[]{sX, sY, sZ, size, fresh}; // return; } else { int emptyPos = -1; //找指定源坐标已存在的洞,如果不存在,如发现空洞也可以占用 - for (int i = 0; i < cellHoles.length / 4; i++) { - int n = i * 4; + for (int i = 0; i < cellHoles.length / HOLE_ARR_SIZE; i++) { + int n = i * HOLE_ARR_SIZE; if (cellHoles[n] == sX && cellHoles[n + 1] == sY && cellHoles[n + 2] == sZ) {//找到已有的洞了 if (cellHoles[n + 3] < 1000) //要改成由基因调整 - cellHoles[n + 3] += 100; + cellHoles[n + 3] += size; + if (cellHoles[n + 4] < 1000) //要改成由基因调整 + cellHoles[n + 4] += fresh; return; } if (emptyPos == -1 && cellHoles[n + 3] <= 1)//如发现空洞也可以,先记下它的位置 @@ -233,54 +247,62 @@ public abstract class Animal {// 这个程序大量用到public变量而不是ge if (emptyPos > -1) { //找到一个空洞 cellHoles[emptyPos] = sX; - cellHoles[emptyPos + 1] = sX; - cellHoles[emptyPos + 2] = sX; - cellHoles[emptyPos + 3] = holeSize; //要改成由基因调整 + cellHoles[emptyPos + 1] = sY; + cellHoles[emptyPos + 2] = sZ; + if (cellHoles[emptyPos + 3] < 1000) //要改成由基因调整 + cellHoles[emptyPos + 3] += size; + if (cellHoles[emptyPos + 4] < 1000) //要改成由基因调整 + cellHoles[emptyPos + 4] += fresh; return; } int length = cellHoles.length; //没找到已有的洞,也没找到空洞,新建一个并追加到原洞数组未尾 - int[] newHoles = new int[length + 4]; + int[] newHoles = new int[length + HOLE_ARR_SIZE]; System.arraycopy(cellHoles, 0, newHoles, 0, length); newHoles[length] = sX; newHoles[length + 1] = sY; newHoles[length + 2] = sZ; - newHoles[length + 3] = holeSize; //要改成由基因调整 + newHoles[length + 3] = size; //要改成由基因调整 + newHoles[length + 4] = fresh; //要改成由基因调整 holes[tX][tY][tZ] = newHoles; return; } } - public void holeSendEngery(int x, int y, int z, float le, float re) {//在当前细胞所有洞上反向发送能量(光子),le是向左边的细胞发, re是向右边的细胞发 + public void holeSendEngery(int[] pos, float e) {//在当前细胞所有洞上反向发送能量(光子),le是向左边的细胞发, re是向右边的细胞发 + holeSendEngery(pos[0], pos[1], pos[2], e); + } + + public void holeSendEngery(int x, int y, int z, float e) {//在当前细胞所有洞上反向发送能量(光子),le是向左边的细胞发, re是向右边的细胞发 int[] cellHoles = holes[x][y][z]; //cellHoles是单个细胞的所有洞(触突),4个一组,前三个是洞的坐标,后一个是洞的大小 if (cellHoles == null) //如洞不存在,不发送能量 return; - for (int i = 0; i < cellHoles.length / 4; i++) { - int n = i * 4; + for (int i = 0; i < cellHoles.length / HOLE_ARR_SIZE; i++) { + int n = i * HOLE_ARR_SIZE; float size = cellHoles[n + 3]; if (size > 1) { - int x2 = cellHoles[n]; - if (x2 < x) - addEng(x2, cellHoles[n + 1], cellHoles[n + 2], le); //向左边的细胞反向发送常量大小的能量 - else - addEng(x2, cellHoles[n + 1], cellHoles[n + 2], re); //向右边的细胞反向发送常量大小的能量 + addEng(cellHoles[n], cellHoles[n + 1], cellHoles[n + 2], e + cellHoles[n + 3] + cellHoles[n + 4]); //向源细胞反向发送常量大小的能量 } } } - // public void holesReduce() {//所有hole大小都会慢慢减小,模拟触突连接随时间消失,即细胞的遗忘机制,这保证了系统不会被信息撑爆 - // for (int x = 0; x < Env.BRAIN_SIZE - 1; x++) - // for (int y = 0; y < Env.BRAIN_SIZE - 1; y++) - // for (int z = 0; z < Env.BRAIN_SIZE - 1; z++) { - // int[] cellHoles = holes[x][y][z]; - // if (cellHoles != null) - // for (int i = 0; i < cellHoles.length / 4; i++) { - // int n = i * 4; - // int size = cellHoles[n + 3]; - // if (size > 0) - // cellHoles[n + 3] = (int) (size * 0.9);//要改成由基因调整 - // } - // } - // } + public void holesReduce() {//所有hole大小都会慢慢减小,模拟触突连接随时间消失,即细胞的遗忘机制,这保证了系统不会被信息撑爆 + for (int x = 0; x < Env.BRAIN_SIZE - 1; x++) + for (int y = 0; y < Env.BRAIN_SIZE - 1; y++) + for (int z = 0; z < Env.BRAIN_SIZE - 1; z++) { + int[] cellHoles = holes[x][y][z]; + if (cellHoles != null) + for (int i = 0; i < cellHoles.length / HOLE_ARR_SIZE; i++) { + int n = i * HOLE_ARR_SIZE; + int size = cellHoles[n + 3]; + if (size > 0) + cellHoles[n + 3] = (int) (size * 0.9);//要改成由基因调整 + int fresh = cellHoles[n + 4]; + if (fresh > 0) + cellHoles[n + 4] -= Consts.HOLE_REDUCE;//要改成由基因调整 + + } + } + } } diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/Env.java b/core/src/main/java/com/gitee/drinkjava2/frog/Env.java index 6f10a4b..3de24af 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/Env.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/Env.java @@ -32,11 +32,11 @@ public class Env extends JPanel { /** Speed of test */ public static int SHOW_SPEED = 1000; // 测试速度,-1000~1000,可调, 数值越小,速度越慢 - public static final int FROG_EGG_QTY = 200; // 每轮下n个青蛙蛋,可调,只有最优秀的前n个青蛙们才允许下蛋 + public static final int FROG_EGG_QTY = 300; // 每轮下n个青蛙蛋,可调,只有最优秀的前n个青蛙们才允许下蛋 public static final int FROG_PER_EGG = 4; // 每个青蛙蛋可以孵出几个青蛙 - public static final int SCREEN = 1; // 分几屏测完 + public static final int SCREEN = 4; // 分几屏测完 /** Delete eggs at beginning of each run */ public static final boolean DELETE_FROG_EGGS = true;// 每次运行是否先删除以前保存的青蛙蛋文件,如果为false将加载旧蛋文件继续运行 diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/brain/BrainPicture.java b/core/src/main/java/com/gitee/drinkjava2/frog/brain/BrainPicture.java index e14cfb3..316a1b0 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/brain/BrainPicture.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/brain/BrainPicture.java @@ -358,8 +358,8 @@ public class BrainPicture extends JPanel { int[] holes = a.holes[x][y][z]; if (holes != null) { setPicColor(Color.GRAY); - for (int i = 0; i < holes.length / 4; i++) { - int n = i * 4; + for (int i = 0; i < holes.length / Animal.HOLE_ARR_SIZE; i++) {//这里画出hole连线 + int n = i * Animal.HOLE_ARR_SIZE; drawCentLine(x, y, z, holes[n], holes[n + 1], holes[n + 2]); } } diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/brain/Consts.java b/core/src/main/java/com/gitee/drinkjava2/frog/brain/Consts.java new file mode 100644 index 0000000..9a9647b --- /dev/null +++ b/core/src/main/java/com/gitee/drinkjava2/frog/brain/Consts.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018 the original author or authors. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by + * applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + * OF ANY KIND, either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ +package com.gitee.drinkjava2.frog.brain; + +import static com.gitee.drinkjava2.frog.util.RandomUtils.percent; + +import java.lang.reflect.Field; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import com.gitee.drinkjava2.frog.Animal; +import com.gitee.drinkjava2.frog.util.Logger; +import com.gitee.drinkjava2.frog.util.RandomUtils; + +/** + * Here store counts + * + * 这个类存放脑常量定义、变异、日志打印相关的方法。 + * 神经网络中有一些全局常量,与结构生成无关,这时可以把所有常量定义到animal和egg的constGenes常量数组中,用统一的随机方法来变异这些常量 + * + * @author Yong Zhu + * @since 15.0 + */ +@SuppressWarnings("all") +public class Consts { + public static int CountsQTY; //总常量数量 + + private static int index_ = 0; + + private static int index() { + return index_++; + } + + public static final int ADD_EYE = index(); //用index()这种编程技巧而不是直接给定数值是为了增删常量定义时比较方便,不会影响其它行 + public static final int ADD_BITE = index(); + public static final int REDUCE_BITE = index(); + public static final int HOLE_FRESH = index(); + public static final int HOLE_REDUCE = index(); + + private static Map values = new LinkedHashMap(); + + static { + try { + Class c = Consts.class; + Field[] fs = c.getDeclaredFields(); + for (Field f : fs) { // 用反射来获取常量的名称并保存下来,将来在printLog中要按顺序打印出所有常量名 + if (int.class.equals(f.getType()) && !"LENGTH".equals(f.getName()) && !"index_".equals(f.getName())) { + values.put(f.getName(), f.getInt(null)); + } + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(-1); + } + CountsQTY = values.size(); + } + + public static boolean[] exist = new boolean[CountsQTY]; //不是每个常量组数都用到,只有被用字母代表的才会用到并在这里标记,这种方法比MAP要快 + + static StringBuilder sb = new StringBuilder(); + + public static void printLog(Animal a) { + sb.setLength(0); + int i = 0; + for (Entry e : values.entrySet()) { + sb.append(e.getKey()).append("=").append(a.consts[e.getValue()]).append("\t\t"); + if (i++ % 6 == 5) + sb.append("\n"); + } + Logger.debug(sb.toString()); + } + + public static void constMutation(Animal a) { //全局参数变异, 这一个方法变异动物的所有常量 + for (int i = 0; i < CountsQTY; i++) { + if (percent(20)) + a.consts[i] = RandomUtils.vary(a.consts[i]); + } + } + +} diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/brain/Genes.java b/core/src/main/java/com/gitee/drinkjava2/frog/brain/Genes.java index 8e05d87..1f9bc7b 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/brain/Genes.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/brain/Genes.java @@ -10,11 +10,12 @@ */ package com.gitee.drinkjava2.frog.brain; +import static com.gitee.drinkjava2.frog.brain.Consts.ADD_BITE; +import static com.gitee.drinkjava2.frog.brain.Consts.REDUCE_BITE; + import com.gitee.drinkjava2.frog.Animal; import com.gitee.drinkjava2.frog.Env; -import com.gitee.drinkjava2.frog.objects.Eye; import com.gitee.drinkjava2.frog.objects.OneDotEye; -import com.gitee.drinkjava2.frog.util.Logger; import com.gitee.drinkjava2.frog.util.RandomUtils; /** @@ -81,7 +82,7 @@ public class Genes { //Genes登记所有的基因, 指定每个基因允许分 return register(1, true, false, pos[0], pos[1], pos[2]); } - public static long registerFill(int... pos) {//登记并手工指定基因分布的位置 + public static long registerFill(int... pos) {//登记并手工指定基因填满的位置 return register(1, true, true, pos[0], pos[1], pos[2]); } @@ -93,22 +94,12 @@ public class Genes { //Genes登记所有的基因, 指定每个基因允许分 private static final int CS4 = Env.BRAIN_SIZE / 4; //============开始登记有名字的基因========== - public static long EYE = registerFill(0, 0, 0); //视网膜细胞,这个版本暂时只允许视网膜分布在x=0,y=0的z轴上,即只能看到一条线状图形 - - public static long MEM = registerFill(1, 0, 0); //记忆细胞,暂时只允许它分布在x=1,y=0的z轴上 + public static int[] EYE_POS = new int[]{0, 0, 0}; + public static long EYE = registerFill(EYE_POS); //视网膜细胞,这个版本暂时只允许视网膜分布在x=0,y=0的z轴上,即只能看到一条线状图形 public static int[] BITE_POS = new int[]{2, 0, 0}; public static long BITE = registerFill(BITE_POS); //咬动作细胞定义在一个点上, 这个细胞如激活,就咬食物 - // public static int[] NOT_BITE_POS = new int[]{2, 0, CS4}; - // public static long NOT_BITE = registerFill(NOT_BITE_POS); //不咬动作细胞定义在一个点上, 这个细胞如激活,就不咬食物 - - // public static int[] SWEET_POS = new int[]{2, 0, CS4 * 2}; - // public static long SWEET = registerFill(SWEET_POS); //甜味感觉细胞定义在一个点上, 当咬下后且食物为甜,这个细胞激活 - - // public static int[] BITTER_POS = new int[]{2, 0, CS4 * 3}; - // public static long BITTER = registerFill(BITTER_POS); //苦味感觉细胞定义在一个点上, 当咬下后且食物为苦,这个细胞激活 - //========开始登记无名字的基因 ========= static { } @@ -119,34 +110,21 @@ public class Genes { //Genes登记所有的基因, 指定每个基因允许分 for (int z = Env.BRAIN_SIZE - 1; z >= 0; z--) for (int x = Env.BRAIN_SIZE - 1; x >= 0; x--) { int y = 0; + int[] src = new int[]{x, y, z}; long cell = a.cells[x][y][z]; - if (a.consts[7] == 0) - a.consts[7] = 1; - if (hasGene(cell, BITE) && ((OneDotEye.code % 100) == 0)) {//如果没有输入,咬细胞也是有可能定时或随机激活的,模拟婴儿期随机运动碰巧咬下了 - a.addEng(x, y, z, a.consts[1] / 10); - } - float energy = a.energys[x][y][z]; - if (energy >= 1f) { //如果细胞激活了 - a.energys[x][y][z] = a.energys[x][y][z] - Math.abs(a.consts[2]) * 0.2f;//所有细胞能量都会自发衰减 - if (hasGene(cell, BITE)) { //如果是咬细胞 - if ((OneDotEye.code % 20) == 0) { //从上帝视角知道被20整除正好是OneDotEye看到食物出现的时刻 - a.awardAAAA(); //所以必然咬中,奖励 + if (energy >= 1f) { //如果细胞激活了 + a.setEng1(x, y, z); //细胞强度不允许超过1,见TestInput3 + if (hasGene(cell, BITE)) { //TODO:如果是咬细胞 + if (OneDotEye.foodSweet(step)) { //如食物是甜的 + a.awardAAAA(); //奖励 a.ateFood++; } else { - a.penaltyAA(); //其它时间是咬错了,罚。 可以改成penaltyAAAA或去除本行试试 + a.penaltyAA(); //其它时间是咬错了,罚 a.ateWrong++; } - a.digHole(x, y, z, x - 1, y, z, a.consts[3]);//咬细胞在记忆细胞上挖洞 - } - - if (hasGene(cell, EYE)) {//视网膜细胞在记忆细胞上挖洞 - a.digHole(x, y, z, x + 1, y, z, a.consts[4]); - } - - if (hasGene(cell, MEM)) {//记忆细胞,在当前细胞所有洞上反向发送能量 - a.holeSendEngery(x, y, z, a.consts[5], a.consts[6]); + a.setEng0(x, y, z); //咬完了后细胞能量归0 } } diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/egg/Egg.java b/core/src/main/java/com/gitee/drinkjava2/frog/egg/Egg.java index 92c1b0e..223c0f8 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/egg/Egg.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/egg/Egg.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import com.gitee.drinkjava2.frog.Animal; import com.gitee.drinkjava2.frog.Env; +import com.gitee.drinkjava2.frog.brain.Consts; import com.gitee.drinkjava2.frog.util.RandomUtils; /** @@ -36,7 +37,7 @@ public class Egg implements Serializable { // gene record the 8-tree structure of brain cells // 基因是随机生成的8叉树数据结构,和实际生物每个细胞都要保存一份基因不同,程序中每个脑细胞并不需要保存基因的副本,这样可以极大地减少内存占用 public ArrayList> genes = new ArrayList<>(); - public int[] constGenes = new int[Animal.CONSTS_LENGTH]; //animal中的全局常量基因全放在这里,用随机数来生成,用遗传算法筛选 + public int[] constGenes = new int[Consts.CountsQTY]; //animal中的全局常量基因全放在这里,用随机数来生成,用遗传算法筛选 public Egg() {// 无中生有,创建一个蛋,先有蛋,后有蛙 x = RandomUtils.nextInt(Env.ENV_WIDTH); diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/egg/FrogEggTool.java b/core/src/main/java/com/gitee/drinkjava2/frog/egg/FrogEggTool.java index 5d4ce65..f1b77f3 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/egg/FrogEggTool.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/egg/FrogEggTool.java @@ -23,6 +23,7 @@ import com.gitee.drinkjava2.frog.Animal; import com.gitee.drinkjava2.frog.Application; import com.gitee.drinkjava2.frog.Env; import com.gitee.drinkjava2.frog.Frog; +import com.gitee.drinkjava2.frog.brain.Consts; import com.gitee.drinkjava2.frog.util.LocalFileUtils; import com.gitee.drinkjava2.frog.util.Logger; @@ -59,19 +60,12 @@ public class FrogEggTool { if (holes == null) s.append("0,"); else - s.append(first.holes[x][0][z].length/4).append(","); + s.append(first.holes[x][0][z].length / 4).append(","); } Logger.debug("x=" + x + ", holes:" + s);//打印出每个细胞的洞数量 } - // Logger.debug(Arrays.toString(frogs.get(current_screen).constGenes)); //debug; - StringBuilder s = new StringBuilder(); - for (int i = 0; i < Animal.CONSTS_LENGTH; i++) { - if (i != 0) - s.append(", "); - s.append("\t" + i).append("=").append(first.consts[i]); - } - Logger.debug("consts: " + s); + Consts.printLog(first);//debug; 打印出每个细胞的常量 if (Env.SAVE_EGGS_FILE) { FileOutputStream fo = new FileOutputStream(Application.CLASSPATH + "frog_eggs.ser"); diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/objects/OneDotEye.java b/core/src/main/java/com/gitee/drinkjava2/frog/objects/OneDotEye.java index 3c1e700..451631b 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/objects/OneDotEye.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/objects/OneDotEye.java @@ -2,23 +2,39 @@ package com.gitee.drinkjava2.frog.objects; import com.gitee.drinkjava2.frog.Env; import com.gitee.drinkjava2.frog.Frog; +import com.gitee.drinkjava2.frog.brain.Consts; import com.gitee.drinkjava2.frog.objects.EnvObject.DefaultEnvObject; +import com.gitee.drinkjava2.frog.util.RandomUtils; /** - * DotEye的作用只有一个,就是定期在视网膜细胞上激活一个点,告知食物存在 + * + * OneDotEye的作用只有一个,就是定期在视网膜细胞上激活一个点,告知食物存在 + * 当前版本中Eye还没用到,目前只测试模式识别最简单的情况,即只有一个感光细胞的情况下,如何形成看到食物时咬下这个条件反射 + * */ public class OneDotEye extends DefaultEnvObject { - public static int code = 0; + private static int[] food = new int[Env.STEPS_PER_ROUND]; + static { + //食物只会出现在15为周期但不固定的时间点上,以防止细胞进化出周期进入鞍点, 食物用数字表示,0为不存在,1为甜,2为苦 + for (int i = 15; i < Env.STEPS_PER_ROUND - 15; i += 15) + food[i + RandomUtils.nextNegOrPosInt(5)] = 1 + RandomUtils.nextInt(2); + } + + public static boolean foodExist(int step) { + return food[step] > 0; + } + + public static boolean foodSweet(int step) { + return food[step] == 1; + } @Override public void active(int screen, int step) { - code++; - if (code % 20 == 0) { //每隔20步在所有青蛙的视网膜上画一个图案 ,单个点调试时设为每20步激活时就是食物 + if (foodExist(step)) { //如食物存在, 激活所有青蛙的视网膜 for (int i = screen; i < screen + Env.FROG_PER_SCREEN; i++) { Frog f = Env.frogs.get(i); - f.energys[0][0][0] = f.consts[0]; + f.energys[0][0][0] = f.consts[Consts.ADD_EYE]; } } } - } diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/temp/TestInput3.java b/core/src/main/java/com/gitee/drinkjava2/frog/temp/TestInput3.java new file mode 100644 index 0000000..e0c2539 --- /dev/null +++ b/core/src/main/java/com/gitee/drinkjava2/frog/temp/TestInput3.java @@ -0,0 +1,201 @@ +/* + * Copyright 2018 the original author or authors. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by + * applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + * OF ANY KIND, either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ +package com.gitee.drinkjava2.frog.temp; + +/** + * + * 临时类,待删,测试一个细胞有三个输入,并且每个输入是双通道(奖罚)情况下是否可以实现所有的模式识别 + * 处理逻辑为 (信号x正权重)取1为饱和值 - 信号x负权重,当结果大于0.5时输出1。 这个逻辑是模拟网上找到的“大脑只需单个神经元就可进行XOR异或运算”一文 + * 原理。只不过这里扩展到三个(或以上)输入的情况。 + * + * 三个输入有8种排列组合,如一组参数能实现任意8种组合,则有2的8次方=256种排列组合,去除全为0的输入必须输出0,只计算有1信号的输入,则有2的7次方=128种组合) + * (经实测,有31种组合条件不,不能找到符合要求的正负权重) + * + * (4个通道有16种排列组合,如一组参数能实现任意16种组合,则有2的16次方=65536种排列组合,去除全为0的输入,则有2的15次方32768种组合) + * (经实测,32768组合中有29987种不能找到符合要求的正负权重,如只测前1024个组合,则有603种情况不能找到解,如只测前128组合,则有31种情况下找不到解) + * + * + * + */ +@SuppressWarnings("all") +public class TestInput3 { + +// public static void main(String[] args) { +// testInput3(); +// //testInput4(); +// } + + public static void testInput3() { //这里测试一个细胞有三个树突输入,每个输入有正负两种信号,且信号以1为饱和,测试结果发现有31种情况无法找到解 + long notFoundCont = 0; + boolean pass = false; + float p = 0.1f; + for (int i = 0; i < 128; i++) { + found: // + for (float a = 0; a < 1.001; a += p) { + for (float b = 0; b < 1.001; b += p) { + for (float c = 0; c < 1.001; c += p) { + + for (float d = 0; d < 1.001; d += p) { + for (float e = 0; e < 1.001; e += p) { + for (float f = 0; f < 1.001; f += p) { + + pass = true; + int bt = 1; + int x, y, z; + for (int n = 1; n <= 7; n++) { + x = ((n & 4) > 0) ? 1 : 0; + y = ((n & 2) > 0) ? 1 : 0; + z = ((n & 1) > 0) ? 1 : 0; + int shouldbe = (i & bt) > 0 ? 1 : 0; + + float v = x * a + y * b + z * c; //正信号累加 + if (v > 1) //如饱和取1 + v = 1f; + float v1 = x * d + y * e + z * f; //负信号累加 + if (v1 > 1) + v1 = 1f; + v = v - v1; + + int result = v >= 0.5 ? 1 : 0; + + if (result != shouldbe) { + pass = false; + break; + } + bt = bt << 1; + } + + if (pass) { + System.out.print("i=" + i + " found, i=" + bin(i)); + System.out.println(" " + r(a) + ", " + r(b) + ", " + r(c) + " " + r(d) + ", " + r(e) + ", " + r(f)); + + bt = 1; + for (int n = 1; n <= 7 && false; n++) { + x = ((n & 4) > 0) ? 1 : 0; + y = ((n & 2) > 0) ? 1 : 0; + z = ((n & 1) > 0) ? 1 : 0; + int shouldbe = (i & bt) > 0 ? 1 : 0; + System.out.println(" " + x + "*" + r(a) + " + " + y + "*" + r(b) + " + " + z + "*" + r(c) + " - " + x + "*" + r(d) + " - " + y + "*" + r(e) + " - " + z + + "*" + r(f) + " = " + shouldbe); + + bt = bt << 1; + } + + break found; + } + + } + } + } + + } + } + } + if (!pass) { + System.out.println("i=" + i + " not found, i=" + bin(i)); + notFoundCont++; + } + } + System.out.println("notFoundCont=" + notFoundCont); + } + + public static void testInput4() {//这里测试一个细胞有4个树突输入,每个输入有正负两种信号,且信号以1为饱和,测试结果发现有603种情况无法找到解 + long notFoundCont = 0; + boolean pass = false; + float p = 0.2f; + for (int i = 0; i < 1024; i++) { + found: // + for (float a = 0; a < 1.001; a += p) { + for (float b = 0; b < 1.001; b += p) { + for (float c = 0; c < 1.001; c += p) { + for (float d = 0; d < 1.001; d += p) { + + for (float e = 0; e < 1.001; e += p) { + for (float f = 0; f < 1.001; f += p) { + for (float g = 0; g < 1.001; g += p) { + for (float h = 0; h < 1.001; h += p) { + + pass = true; + int bt = 1; + int x, y, z, m; + for (int n = 1; n <= 15; n++) { + x = ((n & 8) > 0) ? 1 : 0; + y = ((n & 4) > 0) ? 1 : 0; + z = ((n & 2) > 0) ? 1 : 0; + m = ((n & 1) > 0) ? 1 : 0; + int shouldbe = (i & bt) > 0 ? 1 : 0; + + float v = x * a + y * b + z * c + m * d; //正信号累加 + if (v > 1) //如饱和取1 + v = 1f; + float v1 = x * e + y * f + z * g + m * h; //负信号累加 + if (v1 > 1) + v1 = 1f; + v = v - v1; + + int result = v >= 0.5 ? 1 : 0; + + if (result != shouldbe) { + pass = false; + break; + } + bt = bt << 1; + } + + if (pass) { + System.out.print("i=" + i + " found, i=" + bin(i)); + System.out.println(" " + r(a) + ", " + r(b) + ", " + r(c) + ", " + r(d) + " " + r(e) + ", " + r(f) + ", " + r(g) + ", " + r(h)); + + bt = 1; + for (int n = 1; n <= 15; n++) { + x = ((n & 8) > 0) ? 1 : 0; + y = ((n & 4) > 0) ? 1 : 0; + z = ((n & 2) > 0) ? 1 : 0; + m = ((n & 1) > 0) ? 1 : 0; + int shouldbe = (i & bt) > 0 ? 1 : 0; + System.out.println(" " + x + "*" + r(a) + " + " + y + "*" + r(b) + " + " + z + "*" + r(c) + " + " + m + "*" + r(d) // + + " - " + x + "*" + r(e) + " - " + y + "*" + r(f) + " - " + z + "*" + r(g) + " - " + m + "*" + r(h) + " = " + shouldbe); + + bt = bt << 1; + } + + break found; + } + + } + } + } + + } + } + } + } + } + if (!pass) { + System.out.println("i=" + i + " not found, i=" + bin(i)); + notFoundCont++; + } + } + System.out.println("notFoundCont=" + notFoundCont); + } + + static float r(float f) { //取小数后2位 + return Math.round(f * 100) * 1.0f / 100; + } + + static String bin(int i) { //转二进制 + String ibin = Integer.toBinaryString(i); + while (ibin.length() < 7) + ibin = "0" + ibin; + return ibin; + } + +} diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/util/ColorUtils.java b/core/src/main/java/com/gitee/drinkjava2/frog/util/ColorUtils.java index 87525ab..bd4150e 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/util/ColorUtils.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/util/ColorUtils.java @@ -31,7 +31,7 @@ public class ColorUtils { { { if (!(r == b && r == g)) { - rainbow[i] = new Color(((r + 2) % 3) * 122, g * 122, ((b + 1) % 3) * 122); + rainbow[i] = new Color(((r + 2) % 3) * 122, g * 100, ((b + 1) % 3) * 88); i++; } } diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/util/GeneUtils.java b/core/src/main/java/com/gitee/drinkjava2/frog/util/GeneUtils.java index c197857..5bc43fd 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/util/GeneUtils.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/util/GeneUtils.java @@ -94,14 +94,7 @@ public class GeneUtils { } } } - - public static void constGenesMutation(Animal a) { //全局参数变异, 这一个方法变异动物的所有常量 - for (int i = 0; i < a.consts.length; i++) { - if (percent(20)) - a.consts[i] = RandomUtils.vary(a.consts[i]); - } - } - + public static void geneMutation(Animal a) { //基因变异,注意这一个方法同时变异所有条基因 for (int g = 0; g < GENE_NUMBERS; g++) if (percent(50)) { diff --git a/core/src/main/java/com/gitee/drinkjava2/frog/util/RandomUtils.java b/core/src/main/java/com/gitee/drinkjava2/frog/util/RandomUtils.java index c112ca6..16f393e 100644 --- a/core/src/main/java/com/gitee/drinkjava2/frog/util/RandomUtils.java +++ b/core/src/main/java/com/gitee/drinkjava2/frog/util/RandomUtils.java @@ -52,34 +52,38 @@ public class RandomUtils { return v; } - public static int vary(int v) {// 随机有大概率小变异,小概率大变异,极小概率极大变异 - if (percent(50)) - v += (nextInt(3) - 0.5); - else if (percent(50)) - v += 2 * (nextInt(2) - 0.5); - else if (percent(50)) - v += 4 * (nextInt(2) - 0.5); - else if (percent(50)) - v += 8 * (nextInt(2) - 0.5); - else if (percent(50)) - v += 16 * (nextInt(2) - 0.5); - else if (percent(50)) - v += 32 * (nextInt(2) - 0.5); - else if (percent(50)) - v += 64 * (nextInt(2) - 0.5); - else if (percent(50)) - v += 100 * (nextInt(2) - 0.5); - return v; + // public static int vary(int v) {// 随机有大概率小变异,小概率大变异,极小概率极大变异 + // if (percent(50)) + // v += (nextInt(3) - 0.5); + // else if (percent(50)) + // v += 2 * (nextInt(2) - 0.5); + // else if (percent(50)) + // v += 4 * (nextInt(2) - 0.5); + // else if (percent(50)) + // v += 8 * (nextInt(2) - 0.5); + // else if (percent(50)) + // v += 16 * (nextInt(2) - 0.5); + // else if (percent(50)) + // v += 32 * (nextInt(2) - 0.5); + // else if (percent(50)) + // v += 64 * (nextInt(2) - 0.5); + // else if (percent(50)) + // v += 100 * (nextInt(2) - 0.5); + // return v; + // } + + public static int vary(int v) {// 随机有大概率小变异,小概率大变异,极小概率极大变异 ,这里改进算法,用正切函数曲线来实现这个概率 + int n = nextNegOrPosInt(900); + return (int) (v + Math.round(2 * Math.tan(0.1f * n * 3.14159 / 180))); } -// public static void main(String[] args) { -// int n=0; -// for (int i = 0; i < 100; i++) { -// n=vary(n); -// if(n<0)n=0-n; -// System.out.println(vary(0)); -// } -// } + // public static void main(String[] args) { + // int n = 0; + // for (int i = 0; i < 100; i++) { + // n = vary(n); + // System.out.println(n); + // } + // } public static float vary(float v) {// 随机有大概率小变异,小概率大变异,极小概率极大变异 if (percent(40)) diff --git a/history/014_3cells/README.md b/history/014_3cells/README.md index b40b94e..2bc90d6 100644 --- a/history/014_3cells/README.md +++ b/history/014_3cells/README.md @@ -1,12 +1,17 @@ -## core目录简介 -core目录是当前工作目录,如果跑出什么结果就会拷贝一份放到history目录里存档。 - -当前目标是大方向是由遗传算法来自动排列脑细胞和触突参数,以实现模式识别功能,并与上下左右运动细胞、进食奖罚感觉细胞结合起来,实现吃掉无毒蘑菇,避开有毒蘑菇这个任务。(未完成) -当前小目标: -1)是要利用阴阳8叉树分裂算法进化出第一个可以工作(向食物运动)的神经网络。(已完成, 见011_yinyan_eatfood) -2)利用阴阳8叉树或4叉树分裂算法(见012_tree4)来进化出具备模式识别功能的神经网络。即实现图像到声音的关联,比如对应1,2,3,4 数字的图像会反同激活训练时对应的声音细胞(未完成) -3)简单时序信号的模式识别。比如AB后是C, AD后是E, ABC后是F,多次重复后即可形成时序信号的预测关联。功能类似RNN,但用分裂算法来实现参数自动生成。 - 以上2和3识别原理类似,都建立在细胞连接的遗忘曲线基础上,但是一个强调细胞的空间位置关系,另一个强调信号留存的时间 +2023-08-25 三个细胞一台戏 +本次更新在目录history\014_3cells下。在上次更新里已经说过了,为了实现模式识别,可以先从最简单的几个细胞的场景开始做起。于是就一路简化,最终简化到只剩下三个细胞,分别为视细胞、咬细胞、忆细胞。 实验目的是要达到这样一个效果,当食物出现时,视细胞激活,然后视细胞在忆细胞上打个洞,咬细胞则随机激活,然后也在忆细胞上也打个洞,最终实现的效果将会是视细胞激活忆细胞,然后忆细胞在洞上反向发送能量给咬细胞,这样就实现了视细胞到咬细胞的短路,形成一个最简单的条件反射。忆细胞的作用是隔离视细胞和咬细胞,防止形成视细胞直接驱动咬细胞这种简单连接。任务看起来很简单,但做起来就不太美好了,快两个月了才有点进展,先更新上来再说。 +![pic1](result20_3cells1.gif) ![pic2](result20_3cells2.gif) ![pic3](result20_3cells3.gif) +上面从左到右三个图,分别是对应三种场景下青蛙的行为:1奖惩值都很大;2奖励值远比惩罚值大;3只有奖励,没有惩罚。 +奖励是当咬下时正好有食物;惩罚是当咬下时食物不存在,咬了个空。测试时请修改Genes.java源码第138行,进行不同惩罚值的调整。可以看到,根据奖罚值的不同,三个细胞进化出的神经网络参数是不同的,奖惩值都很大时,细胞就会躺平,多咬多错,还不如不咬,以避免惩罚;奖励值远比惩罚值大时,细胞就会比较活跃,没有食物时也经常空咬;当完全没有惩罚时,细胞就会放飞自我,直接在咬细胞和忆细胞之间进化出信号循环回路锁定,全程都在咬,而且最过分的是干脆忽略掉视觉信号,把视细胞和忆细胞之间的连线(洞)直接用一个负值(蓝色)参数来掐断。 -当前小小目标: -进化出具备简单模式识别功能的神经网络,即实现图像到声音的关联, \ No newline at end of file +这次更新的主角不是分裂算法(因为就三个细胞,谈不上结构了),而是全局常量。本次程序中控制细胞特性的全局参数有7个,分别是: +视细胞激活后产生多大强度的能量? +咬细胞激活后产生多大强度的能量? +每个细胞激活后能量随时间流逝,每一步会遗失多少能量? +咬细胞激活后向记忆细胞传送多少能量? +视细胞激活后向记忆细胞传送多少能量 +忆细胞激活后反向向视细胞传送多少能量? +忆细胞激活后反向向咬细胞传送多少能量? +这些全局参数是跟随青蛙终身的,一旦青蛙孵化出来就不再动了,在程序里把所有常量放在一个数组里,用遗传算法来控制,基本规则是小变动有大概率发生,大变动有小概率发生。青蛙的参数分为两类,一类是与空间位置相关的,如脑细胞是否会出现在某个空间位置,一类是与位置无关的,如每个细胞激活后向其它细胞发送多少能量。前者要放到分裂算法里,用一串8/4/2叉基因树来控制空间分布,后者就没必要这么浪费了,直接用一组全局数字表示即可,并用遗传算法来随机变异和筛选它们。在给神经网络编程时,如果碰到可以用全局常量来控制的参数,尽量不要手工赋值,而要用遗传算法来控制,因为多变量的优化组合筛选靠人力是不可能做好的。就拿这个例子来说,我压根不知道这些参数将会是多大,是正还是负,但是我知道应该有这些参数,这就够了。人工生命项目编程不讲究精准,思维模式要从传统的精细化编程转变为以概率、笼统、可能为导向的思维,大方向人为确定,细节交给电脑去算,这和大自然用遗传算法来筛选出脑细胞的参数是一个道理。 + +从本次更新可以看到,青蛙是工作在一个连续的信息流下面,信息是以脉冲方式在细胞之间互相传递大小不等的能量,可以说是一个最简单的脉冲神经网络大脑了。这个实验不很实美,没有实现一个不漏地吃掉食物但是又不空咬这个目标,但是人生苦短,我不想继续和这三个细胞缠斗下去了,后面将转到多个视觉细胞和引入苦甜奖惩信号细胞来影响洞(权重)的大小,这个会更有趣、更智能,也更接近模式识别任务。我认为通用智能就是模式识别与行为输出结合起来的系统,如果奖罚细胞和行为输出细胞在一开始就做为这个模式识别系统的一个组成部分,并且由遗传算法来筛选参数,那通用人工智能就不远了。 \ No newline at end of file diff --git a/history/015_testinput3/TestInput3.java b/history/015_testinput3/TestInput3.java new file mode 100644 index 0000000..e0c2539 --- /dev/null +++ b/history/015_testinput3/TestInput3.java @@ -0,0 +1,201 @@ +/* + * Copyright 2018 the original author or authors. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by + * applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + * OF ANY KIND, either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ +package com.gitee.drinkjava2.frog.temp; + +/** + * + * 临时类,待删,测试一个细胞有三个输入,并且每个输入是双通道(奖罚)情况下是否可以实现所有的模式识别 + * 处理逻辑为 (信号x正权重)取1为饱和值 - 信号x负权重,当结果大于0.5时输出1。 这个逻辑是模拟网上找到的“大脑只需单个神经元就可进行XOR异或运算”一文 + * 原理。只不过这里扩展到三个(或以上)输入的情况。 + * + * 三个输入有8种排列组合,如一组参数能实现任意8种组合,则有2的8次方=256种排列组合,去除全为0的输入必须输出0,只计算有1信号的输入,则有2的7次方=128种组合) + * (经实测,有31种组合条件不,不能找到符合要求的正负权重) + * + * (4个通道有16种排列组合,如一组参数能实现任意16种组合,则有2的16次方=65536种排列组合,去除全为0的输入,则有2的15次方32768种组合) + * (经实测,32768组合中有29987种不能找到符合要求的正负权重,如只测前1024个组合,则有603种情况不能找到解,如只测前128组合,则有31种情况下找不到解) + * + * + * + */ +@SuppressWarnings("all") +public class TestInput3 { + +// public static void main(String[] args) { +// testInput3(); +// //testInput4(); +// } + + public static void testInput3() { //这里测试一个细胞有三个树突输入,每个输入有正负两种信号,且信号以1为饱和,测试结果发现有31种情况无法找到解 + long notFoundCont = 0; + boolean pass = false; + float p = 0.1f; + for (int i = 0; i < 128; i++) { + found: // + for (float a = 0; a < 1.001; a += p) { + for (float b = 0; b < 1.001; b += p) { + for (float c = 0; c < 1.001; c += p) { + + for (float d = 0; d < 1.001; d += p) { + for (float e = 0; e < 1.001; e += p) { + for (float f = 0; f < 1.001; f += p) { + + pass = true; + int bt = 1; + int x, y, z; + for (int n = 1; n <= 7; n++) { + x = ((n & 4) > 0) ? 1 : 0; + y = ((n & 2) > 0) ? 1 : 0; + z = ((n & 1) > 0) ? 1 : 0; + int shouldbe = (i & bt) > 0 ? 1 : 0; + + float v = x * a + y * b + z * c; //正信号累加 + if (v > 1) //如饱和取1 + v = 1f; + float v1 = x * d + y * e + z * f; //负信号累加 + if (v1 > 1) + v1 = 1f; + v = v - v1; + + int result = v >= 0.5 ? 1 : 0; + + if (result != shouldbe) { + pass = false; + break; + } + bt = bt << 1; + } + + if (pass) { + System.out.print("i=" + i + " found, i=" + bin(i)); + System.out.println(" " + r(a) + ", " + r(b) + ", " + r(c) + " " + r(d) + ", " + r(e) + ", " + r(f)); + + bt = 1; + for (int n = 1; n <= 7 && false; n++) { + x = ((n & 4) > 0) ? 1 : 0; + y = ((n & 2) > 0) ? 1 : 0; + z = ((n & 1) > 0) ? 1 : 0; + int shouldbe = (i & bt) > 0 ? 1 : 0; + System.out.println(" " + x + "*" + r(a) + " + " + y + "*" + r(b) + " + " + z + "*" + r(c) + " - " + x + "*" + r(d) + " - " + y + "*" + r(e) + " - " + z + + "*" + r(f) + " = " + shouldbe); + + bt = bt << 1; + } + + break found; + } + + } + } + } + + } + } + } + if (!pass) { + System.out.println("i=" + i + " not found, i=" + bin(i)); + notFoundCont++; + } + } + System.out.println("notFoundCont=" + notFoundCont); + } + + public static void testInput4() {//这里测试一个细胞有4个树突输入,每个输入有正负两种信号,且信号以1为饱和,测试结果发现有603种情况无法找到解 + long notFoundCont = 0; + boolean pass = false; + float p = 0.2f; + for (int i = 0; i < 1024; i++) { + found: // + for (float a = 0; a < 1.001; a += p) { + for (float b = 0; b < 1.001; b += p) { + for (float c = 0; c < 1.001; c += p) { + for (float d = 0; d < 1.001; d += p) { + + for (float e = 0; e < 1.001; e += p) { + for (float f = 0; f < 1.001; f += p) { + for (float g = 0; g < 1.001; g += p) { + for (float h = 0; h < 1.001; h += p) { + + pass = true; + int bt = 1; + int x, y, z, m; + for (int n = 1; n <= 15; n++) { + x = ((n & 8) > 0) ? 1 : 0; + y = ((n & 4) > 0) ? 1 : 0; + z = ((n & 2) > 0) ? 1 : 0; + m = ((n & 1) > 0) ? 1 : 0; + int shouldbe = (i & bt) > 0 ? 1 : 0; + + float v = x * a + y * b + z * c + m * d; //正信号累加 + if (v > 1) //如饱和取1 + v = 1f; + float v1 = x * e + y * f + z * g + m * h; //负信号累加 + if (v1 > 1) + v1 = 1f; + v = v - v1; + + int result = v >= 0.5 ? 1 : 0; + + if (result != shouldbe) { + pass = false; + break; + } + bt = bt << 1; + } + + if (pass) { + System.out.print("i=" + i + " found, i=" + bin(i)); + System.out.println(" " + r(a) + ", " + r(b) + ", " + r(c) + ", " + r(d) + " " + r(e) + ", " + r(f) + ", " + r(g) + ", " + r(h)); + + bt = 1; + for (int n = 1; n <= 15; n++) { + x = ((n & 8) > 0) ? 1 : 0; + y = ((n & 4) > 0) ? 1 : 0; + z = ((n & 2) > 0) ? 1 : 0; + m = ((n & 1) > 0) ? 1 : 0; + int shouldbe = (i & bt) > 0 ? 1 : 0; + System.out.println(" " + x + "*" + r(a) + " + " + y + "*" + r(b) + " + " + z + "*" + r(c) + " + " + m + "*" + r(d) // + + " - " + x + "*" + r(e) + " - " + y + "*" + r(f) + " - " + z + "*" + r(g) + " - " + m + "*" + r(h) + " = " + shouldbe); + + bt = bt << 1; + } + + break found; + } + + } + } + } + + } + } + } + } + } + if (!pass) { + System.out.println("i=" + i + " not found, i=" + bin(i)); + notFoundCont++; + } + } + System.out.println("notFoundCont=" + notFoundCont); + } + + static float r(float f) { //取小数后2位 + return Math.round(f * 100) * 1.0f / 100; + } + + static String bin(int i) { //转二进制 + String ibin = Integer.toBinaryString(i); + while (ibin.length() < 7) + ibin = "0" + ibin; + return ibin; + } + +} diff --git a/result21_input3.png b/result21_input3.png new file mode 100644 index 0000000000000000000000000000000000000000..bccab9e5b26547b65499fe87753200a3fd9fe181 GIT binary patch literal 17379 zcmbWfc{tSl`#1bS<%)1+EktGC%PxaTObC&otVOaflWhhW@U(WM&zRuTMeC?s@%=7X&eX zq5h+3^2oanL7FI?YgbHA)(hW>S?}}8uJvpmWR6B?{VOeA^zAF_XFrIV97i<6QpQ*6 z4rn0gu5#c{@Z-djI2Z&4(=5l+L(qk@>MNk}V+1%K1l0#`Hq$}SslWNr;CFN+0Txi{ z^8crjz~&Oq0L`YH@^6TuzVEpcvvlRUC2p?S6JY+dAo~1ZI zbLua89;k|3^udsCC&Xi|#SRLuk3SY{u+=u7&}2YL4O%%BoIH7f?y}!*>3WMD7ln?N z1wzRg$UHDz| zo++0L%9vhyS5Mcq3==k>!P8H3LVV<|yvROg)K#`z$ojD+BV@Qj|Bc{e2>aB2GFWMx zKwF=%yPl{r7dM@(LffqtD*?g2j_IdhtL1KrU%utL()anz$Q{6y|Jl)MV3 z9szek%8}r=zY~U;C;B==TWZB7o1)PqGlc0X#l+0^nT?ifq zhTG{WOfzo2PrD5!7byUp8NWmSv%gk|lYTYh=YTwax z$Dx6hX%^7;Vq?gWV3)1c2+;Qi>Hnwx23>O-b?ngptA6q-SNH3&%ho(LsLKw@36Q4> z<_I|H|=lLEI)*#(j=(AvnTZIpw&{aScA&r!B5zZLP|Y;H%R+!VDqq< z=>baPag&lX_4;c^1!70rC0cmXe?|q+V*bwk3tGf>B@_0PR&}K<+H#cW_h~y>pkX+_ zG-)OsAKu=h;EpksrS9b&y_5MD zo;&v_wD>lT1Gy%iv8|baq4d;Odm2FFG~32H!x)yXo!OL)3-qtO(*A}}!L>$&9v;-! zk@m{`7N};z6{?q83ppRVO|E4vk;jzH}0PX7yS?x^tHo9p7g(6f82^ zCy1;w%b3z5&l)&Lzp^NwZ=4)F4N(O5zR1LGPi{R*ofvpbbWK^v;mj<&BeZzXUJiDW zhDZypDket`K0+yxh3A)ZM_eq?s zv<(vtt9a%*-4^_mi3X~nXnbSKEnl4!F^Ib~CG1dXD{Hg#?lC8q{i>i*MY;@KFL@c5 z&qtKH{uC_F)@Qa{4b=^IjqOLW%PU2G5?I`gZ~C~h3Vzf$j~N}??<0_R(}AO5nsqx% zscly)!GcR@U*A}$I|At>_ znapcYVJWX<{%epE>Q*@586{US1zt&=cVGMFHSOCW$l3IpAqkP#)MHtF_J1`g^BU7! zPC3e!J65rMPObflVyEDXMo=)aDjFRvG4yAQD7J%yAilA3y1n-4`5U%ZT+8#`dS1G7 zLV)AFm)PGQq@eLfyDU2%fkmBba^*=Wgg1lkMj_&CVI)y6`CCkg#yR1|!!qu#L+h_Tg>r_y+zh>&rt%`xtt>+Q#ZS6pUP_Gz=VJ z?z+asY2ZM5Ccd%KxVg$V$}k!?z5Iv<5}2DtVD1Xz#sWAz2kk7RGGk>dxexaZ(*D+! zUz?c%d9k?apNUjqS^62BTtN$6$O^Jv-)lee#C;_3(y@5@Ki&$udude|Fk;qEH`X+MS2Czf^#-ky<+t(tJMkA)!2WiyZcLgCPmbbLz*veC!^R@r_M z8uyr)n;PrCe<540Jo$IN@?4Tb%PehhU2gsc#w_9UT?69np|_+wj|>SoBeb7gxPN?{ zU%+xA%_;|bK6bQTj_gQNZ`0pZM+?^Di}IF0*WLyNuVMyKBGy+~u4u@8h71OEhT=&x zYA_E2c4!y9x9imn=I87X^t19nF+;8CI8>j@cWh;Tv4@i#?lJkp>lp+k1npbRI0&QC z`_xy4F(O4Fw~j!6ZExki$xIp73L3gNQ`%S2- zX0wC2t@<6H9jFYW2vmeZ_B6R0+fG85fZq3m|2VEj7`Jl|2erMr=3K6dZ)Af{K zQ;MK{mP;S$N_+@CqIhxUW6)Y_(X3G6ZC1u_?nW`nua+s! zoE*dEKjKpJ9;n*8X#yS*#) zzX|q_DR#Ga=zQdk-SUt!9n_t$^1#$CY)+!T+h3`-`%-eyrbEf)msWUOybc?* z+n~FwL~^O@H^8}Pzai|UC5Go&ybJ$cZL?5NX95lthwnkv5F5!+(wb+752|9f>2Ve* z&iBoRiQjXT?47xk?C$#u7wxTZI&FUZ`;B4#c(dlW^uhat4KG&_gc-Z;th6F00y{rq z6;%1#+e-hK!;igaXMWIkv&8*sy2}{Sj!IbqPJ$svFz4Ky^utE+WWJT1GX~%K?^CKr z>T-N9llGtBuPR=G)FDSR7{9EjBXHzfZijwa^oj!a#OBvN$2w8uu+?bL zYU@s=d{kq$Jnqf4d!+s6&Ly~_Dve9P=+8L_E4ywO=1IM*&^TZ8Y>x@rTV|~Ktaayx ztwrKBzQ14}LS&S`%qqd&gVXy`KqDh)ov>Goa&vWoW?qhc>#H70zY&?}u8{4{N{rNw zZoJPY96cdp&QfIqmfsp4q_{VFxcr0NP&a?wYY_txgJkRRi*XBA-zb}Zy#3)+2B}JQ z2^_i)Bk}w<>bwt+Ng2S&gd%ce-)WXE23=)-agzkXZ|SP8S?QWBX>b56c(>lI@qpPi zx&$1|Nt-u9nA1=Zmdl*9k86~*%BUEll#XAqAYAiL3Bco zAln9JoJT*67IGix3aH3Tc|FzKg&k8sVkE7J;r(ussy8;qI+OJ|zc?0Oo-ivs2EpEB znjyiIhClObYdDkkMe#Vb`5}wCw%tx5KbJ&5IP@kX_pKA!*`WFk&atjIaMPYJ?+b3L zux-F65DPW~#re#G`>&1Pg*#q1Z_%zj22t{YMq9va9#WdRSyHI?ac-t6sL~g^4tMkp zo9Es*^wkioAamd$==pQz6%DX=pyRRc>pN$%=G+l}T8PTg82OUU94I(rZ8d7qK^XJn zGyi3c3}t_fqUmgobRUTkkw~l3oH8`of~95u-E>xNM#>B;2}&!bd|1B5gyi7 zd5dWgA_k$XU%#G@DfAM9UVH4}OLkIP#+LgsEi`VE_EXV9Tt!p$0ysU~;`#pTv!*c^ zAEule->aeE{q@y3>WDuZVjTBO4&h(5Y4Q9W1hU^M`|<75JXwq9&E7v0gT~di(IH^4 z#e3T#-m2ekUQO9gt6SaN}6$l z?L?3d)_V|F`U?m>gPcMnM;!(8elshmo@aek7WlA9m5DA~rU>!?6opUM$%Z)R&6$!M zU8A2j(}mD0QjS8w19Q{%opq9C$`dR&er;+HG}6@0BFd)Bq07_e zGFxzOg%p~!)!c7I&npi2GW-nXHvYxDEcNkuUc9JG+5?22zz97=@wKYyw@8?E2=_pC zj_K7!DErfORkW!DJkV(G7;uPvK*}2mm^Fv$p(_94E)hAy4~r!lFZS1{uFPfQlNwCN z%I6pOoSAg+V|HpRR`#?4LyT{O*DK|P`jlJCBw2XPiez=B!*byduq1`~7(e-xs3!O6 zCJ%C3hUdzsn%C`z9ux!!uTp)Kg`k?A=<_Sk2OANK>wPa*DeJrV%q?F(l>@XjoSes# zVM?A!wQAg$@zjO*vi%x-YflXRT299vFR7T}4*L2oaDLuuMNsCv*EbqNN0N6cU9x4t z7crWOh}Z+MK=|Fz+}}?#sylOXR|<%K;%BQ_oCE4;3KotQXUkRHd$^z_pF?;AO|CfA z1@l9$ly4b5US<`GCu%1};`Zv5wtp78Sfcz4F{Cx$puq7E5OcK5Zl`es-Wt= zrT$0Yh9MuF;>^7+N9M4;j=($PA<fZ$IGX}@&wJ+6$ z^??Fxx%GM})Y5koSgR=ISB*f49g`z`X; z-PEA6ju$Bao^Jnbc;0L66a9;y9FoXJzS@V867Czn`nafD=XD7ePro?j0O*6%8rx~d z=V5)hXvGvPKdel=2mpA)w4lj3__|y|7T6L|@2iZtfJu;r*Yl})@O2b;4$*gsd3k^P z!nk_Ab8^QQ`{j<4ZsZPhY1~c)xh>!IfW$tC{nCLIi^T4ug6cXxjMeRE*M9VPWKas$ z1oMp9ZlZ)GqmMAFS0WtPcy%Q+Ldp4-8HQh9_%L8%Sqzn%{hnm;jR&Pq7{U#CeOhj%DL;yx zE>4k2O}rVSg?kG7K;Rf8f7W=#Cq)h2_|l~;R!{HuTq52Ft9R<9f#pPrS%y71PmVav zDba78{g@-#-a`7?$ISt~!X3J~ND`IQ(DZsBqyCE`mx9-~g!M=# zZv#zS{Btf{-{qvu2m8~nL>zk3**3<$td44UDcW+y?xB+BJay}@dlL@B$rP-6V0sdR zxn+rerg)KfzLBdL@UQILYGF$+Rs56eKL-s_V)|l8{`%8_g|hIa3AEn@uP(b+ zI<7@G?~wL?HBezrx?XYS!Q)KWZ=Nv;uQkPtz0R#qXR5u%&G1(;Khcg}&wb~)4Wit> zgd*~XQK>||8@8%O<;F~n?yi%k_L!z&yL4kSQySeNLLUTFea6fG)Y6*1&RY}XaIm(U zuac)aJrUWbqXUeK7q`{KC0>$-ZD{oG)%6st-wOBd_m9u~D6P?$Rs%PS!f;9b&>+Lj zjPzg6mE}pkiom6QDY7Q7~zGz(`FU!ayBni{N;-7ubL0WxB2d!wD;Jfp?~00nk2Tah!{s+ zk|E7>;Ga)_H&&~m8ur0v0fTET=gmqgnKf4VOFnDyY(6#$;+A=^`ViFLH5;x=7~V=* z6pjUu;}d6Y_5~#W=4zmzEZl>fbU$_xp;q z*ImZVgH8DZ#-P2>PMLR-hw=ECk`?+!v3FLYzd}R+uUFrQpEx%3(7H=g0>@|NBY~7J z(~49E28k(<#&;#tW1VFtDzZ;;AZ8tl(jjs-`dW_Hjj@_ZiXC+>$*b_(Zm~;fylEPq zktHVeurX$Ld@oSVlW(b@ol2gt6y*7P_v_oTM%ZS6H8PVg36rVk+b4FQHWQj;;c|SP zyiA|)!rS~AWrkUXRvd>>U-v# z{WHE{A@C0H1_6%3Qqony{FIn{Iu_u2J)e{tksaBs`(x%;R)15H*m6^_fb)!53Uk{v zBYrVctZ*doH!u=E$oo#vAN(+_l=u2Jp5+SKHJ_+*W43%Ktv@bdmPt^uB1NSKEm03)yqpTU9caRVgw=%*37DO@#Db z%n~3`MV9sHSfEt&{D~?wUNQDq(70Mjij0fb$P;2OApI0Pg%2Vz?*a2q2~xoXI0mdd_q){;;KDhHZE#NQlM^fjqIZP@`^GuwF&YJ zeJxn1v(~rCc_DIlqA?1k34;h#W9giHZQebzD#1gR0^_|n5|q2eZcvBx84o%10+&OT zTDvV8C4MTuW*Lp0biXdMM5I-DR=Q~x4OW7)HOG7Ukh}1EF0r^(9rMZf&cxcejBDh- zZj-EJ*VLC8R2XMM%O@*%dfRr&E;HYk+V8goK>+}7qKSfwN)>q>KDkwkodZ2-jXiy^ z)b^7XCZ%N8gh9Q({#~z)S8C$&cT%P#HSQ2{iiz6MBRL}8yOZ{R0K8o(k3fE-I*B8J zEBmMXBT!*!v}>P<$YG}0Df`$Lo@ZUA4Bu*NDQKOrQ#DdrxjY`?7T9rgH&}vyS=F$( zO~?O1iojV4`ILVEDvX!rztv3MlHuC-U37?5Z#-9;fJ+q>!`&|yzH`EGz<+yEzL>BF zTuzF>T~hkRwNVPgkAwd{;9aFQUj4KOv5%kU$BS~o*>dHgu?%b*GJs1e>keey2$;I_ zAU3$2Xgd=#!SQjchu)(Jsg#|ZtYY2}vWwYwc|>F@;GQDQ0Aw&3V6{YUDs|+J+T`rj~QEhZ8Fv8#e9wBur1ZypR=-BuT-t%`P?FXPnCu=P~Br6fREgB^56R}{kL*1_zNr4j= zLNJZq(E-pN9XRI4XJq)OdxP=!iu)-)4HP;>VBBdmUSLt@@N9+<)RAVuHo?M{Hiy2< z{)pRr_?c#uX>yTXM#k%iE4jeyH+fkfKAdoP4Cx{T@RctrNJ_N>sVuo%7f3rt1JDvr zt54(c?I%B^ly^Rjh+DDJ{;$%Z_&QKpbZlx$q|fEl^4{YbJcleCgw6mhJQ%$+R^cya zqe#kFYfzMop^FT~H*#BgKk-=JPnflTx*7G^dXyp5{a8^K;S2XGHd_Z4Sud*M~7GG7omtHx5GP3q15=gcsu)^10oulLF-Z zvwx_S&g2QDeHY;H=xm;;_=^?^{4y|Ord1M} zk=kqVv@;X9I8Ex*iq{OtM1KI2$QK&Wfth2pAJZ8^vN&iYGbAX zzvAe9e<|s0kb!I9Z>Hk2-{-5w_f%)vdR!*^<06%=k!KLFb#-BWhb&*#YG-&Wmh}a# z0@$V!cz25Oi==(1j4k8w(`zDP?u{N=0+myokWLe7M-j{Y2HZl?dn@_YCx(Nr<+(A ziN-!Qwa&W>VBzOonYV(-1KOa=8+C~eTVG(*vHC4{duSJV&M4T*EJd~(HNkq?jrd84 zhrMPi2rkOJ)6VzzVcY2WzO6Yz8R)|{8ew2{V-K3O!GPlce%0{M|=J`b&iz%m&yyF z)AENkz2uQq@(VAUeBa#UmAWH&`@}(o`+@MQm&*Q6L@pw?d3?`Rjq9t;xPTqaiBt55 zkEBL7Y`Gw#Vk$z8x}$$w&(7#H!cJ!7XWaaO;hWQ(e^RL#AyQAIQ7$ao25Up8$w?lM z#P%L9SX^YEI?5o`L80i|ROC)Yee{0AE9am8L~srJ+FbZ*`bQJ~Zo6KOneb-921`>8 z^n6emdbX^mM1aMzo@MiA@v5hH&ruJ`l>@2eq0MAgBC7fbUqu+d3X;LKud7T&kxYq7;1y)ykX7?~=2DkV8jThYCBAmvjNRQJ)`Fz7t{%e<#LHdW- zo~#xU8WX#D`NY~%%s_$)Z4g;%U2cqdaQw3x7GC5vOJ)4~m_3_&PG-in_ke8Ru1i!K zD#C5jC0NaW5cWzlI{Lzw%wy%-{$~--Dh?i4>yZDdc&Ri@5c9mQQGH$R&QAu(iK5W9 zt+NJciWQ<>Yv$I+md-*Ref`yX2`Q@=hBkR7Sk)3-xbmfO8Y9Ql2NFyGBYUbwb zOJ!4ngts_%YurD6bg|Ku*lz7t-6FO3BanM>pz7oPL;WQnlV^wO=Xkmn~2iTDm zY{1qJw?frcKlDV%1|DR=yHg5BjjZ$^2=3~a_i_#c`Y^4~oz!S#wVsp@PEr?Y6Hx5` zRlaJUg5|C1f4%*WwQpgPPf%gW{Os4xyWZiIu6#6D-Fw}6 z&RG^qTG~li2rx;~>6fMk0>AvHkkn{1*#jgHlN`V?gbY_L-lc9BgT{FAE)u`{^^@-X8nH8rvRsa^b^Z34?HZrKH-i?eNbk-nGjIC`h+$#|~sDfP+& z=tWAQ7Cu)~6AcPyHjF(guduPRAnRmZ5dU9kK*MZRkww3HwmHB!On>|F9@@R~rvg90 z22)<)0)ls1Bzq!WUiUD=NJYSC&JHQ$fm4Ql5k?xqT=S608YQnfEc5*0+;FGH(pqTV zj#5Uz-iaVK;M1cE=U-(j`#*Mgra~J`T>s5E=cbO~ts}T2>8bg{f2RdEsH8bdN13NT zmxd2WOfB{kCGyI*&Q5F$N-a);^q>BO_s3J(;*F#2MvWNrV#p0ay2j}!8|_e`?Jon zw+UUJEQY@q*W|GSX3$XAEan>eClxv?RY!SX?+7g}O-RtOq|U>K>^MVggz1>1y!T@h(n^y2E*O0;$7kV|d}cFU#3rX4AAnn(s8g56 z0&SB!T_t2{V+%cdsEFPgWF2glZ@Fc;)kGJiDR-cqt}!tFgPt9}!X1wIkxm z25E&4#e(5mmlr1_M1)N2;>876009M3^aNkf2+wk;Th1P6q#Xe9pp`+8G;Og($#vWd zn+IG{#%I4H-Ng&J3Z}T4>qn)c6{06DRv}3HeO0ec_)a)fM>6Y>g1!tqj#+z8*duN& zgQTr)P%mFCYrWq4Ov{)~r|SyFD_2{jYge9)EqNA>l0$1Ym$g?nQ@$N3qZ~cVZqwku zZ{PXu7#;l)ZA=KXHuYh7IktMIrh~qT!WN6olzW%*-t!E^Yh0s)bB}Ii1t-8qY((Zi%RHT_-Q5uzvEPYm?r4Eko)8**cKNjoKpJ4~Bq zoV`CfXHME!|Ig8c4U?-pd^yHmmv4<*?v}{(Erg^Q-q0bbLuuLmF~F$_<^3AHTHtMl&QUau1vV0dn0Ds87Dk7dCi3~3GSgR_W6b;ABEGy zabvYIBYe%&GI8A&j$AgU33&1Dn-ktT`LB~SvhrKT#eM}_|1+pM?INsI;1t1$EJ~>d z{w8AW0=@U+M-)7mTsa$EFx{Gg-mu4$qw|eh56;vG2Bfm^4XnF&2Nj@x?cp=_O@BHq zzlDOsN3EZORY{+5kK-GkAHx!z{8TSOGuJ$pTEH18gw@HFTrr$b^?gr7`~JrVX0=KU z@L{^z?=7*?=RZEUt9{Zdxnf~i(wgH|IDgq=Rjexg6y%*%zH#4U^~&_(sKn4~;CsnL z;9=Qdq5B5d+(s8#Xvjan@V6pAzTuIoHjn&D{h_O_ZWCY`$s$J5T-NXpKu|w7pV>M5 z49H|dzUp2#mZALr{M*G!i@1N16%qe~Dr7f`ZLv7Q#U9(3a0F zX9rL|VSv&t&KD>T;q^RZ+DFNB*EHPp-Q1A?jC`$OtUQQ>D8@B47r^^$Z`j+CZpmY$hu2GVhH7s1n9%(w_p24 zI}>`IM$mY-(67U4Jq-O*Uy>6lSn!0;QuBKmD%_8gz2~xgM?9@zhGg9s(1CUF?Xsz> zkXhfd{pA8|w1x)e~~K-i&Fc+8V2v!tCGPqrHozkVdTro{p8`Uz)|_YjCROg}~! z9r-<2gHj{2U;e zM{o*_zM8Ugg~QaoU(~aHQ7zl*P?hCiVZUMu#ISo`I zo>IzM-@g*C0QVR_?GRgqbTwUmj-vlV`tpf=b7UnZS?-7Zdjv?o3_v{62HyfUO~s$3 zOVtaApr)R-5OxUv{ctJ&Y(BQ~oyU6>?Zztw4NxS6si~kxqN|SFA-&k(~uwsTCFjMU8-(B%8ZnE??TSKAtH%J^7IN!5$r_4XQh_9KNi(Xx_%u zB_4MZd-J*8zKzbqmZrSt4IttAXwrH7nE+3-g-3VCbSbg~O6!nGZtnpafE29DFy+bh zI6a(u)-~O7Zet~&N)VW`*rdH%Cs5tA<4#DQ4x`2lfFw7rj)@XFZQ?btStA7nj2V_&zeE6Y<6y;gbD5^ddZx5xldb~I}CIy&Q$2xu) z`VTV$a=@(yS+mG+Lk_ELVR87BFI=>nbDKTAE$nca=|)Nw4Yk3|W=3X9+Ple_%r9dB zF$~rQkEBk5OMT^|RU^c_Gy8J(brg1BMHaOLQ8y{H_8z{BDW7=z*f@I~pJMtTVGpG1 zQ0q`dxXne(rofU5O>p0y|4(oC#vaC+NBfZfTP2{x^#mp_>#~D^a)SDBk=kg6T9F9# zJds%fe+tDy-sA0YctTG;-j57fUy(^6>`hR~X6TIa^yO{OItnUa6#o*k{OXbNW=zid z+>-HY5fKciNEOW$92wUQ>Y)G$vUTp6>zPaKMq@BDYaK|M?8ihEI{T1A)=qZuuD5>r z`RK^Ff6&NbQ4#*@nJa%}2PYxR77t#kK6-waZZUm!?A%biELL zuY=kjMFBf#I+^*f>Hd&4WEZbm>#Yil=CMR(La-}<=*5K_JXTYW|G?QUt0rdH+y0UP{zjvo)ZO0Bo zs-rDSAB%y=kiPpgvj{ZYl&SU4jfO6LnV>52+Z+>tu#jdgV)ZywzS3kwrV3&}fBT;5 zR7Jl!kl#n;c5?A`D*;76tlmaPX4hZ^ztuoj)g*Q_n0`}JjA|V(+Mw!rJa5vBpWe4@ zrScISK&F0*o&(~WlzB^~9*t@#!O7#`SwX_?$hW=Z1cw$bJ_;YN8H9SSkuD40Ti)Gl zptBBRV}w{%=}rD9O8%*P zucw2oW51k$tX7i;{t=Q~gfz#m)73L>O1LKJC0As1_&~=RJ|7EKT895X2iibiCQx+s zJDYS1O0`_USCGv#<0HE*WkAhKq4yl8xhuN=DpmO?Wy5nHJUjV6dSdc_^)BUqRw;eF zu00(v6PjILtYEwBKp0a*l6>)?C|+MuM_Hq@jV_UJ#aU z${QavkOS%N(6Uow>&C6Kws_*vP)fzB?JHyMW4Bef&I8TWgD9KwgCJ?#9l@VGfg z4r#KWOOr{-m>iO> zOCBOq(%Lv|j@p}U*4N?Io6nbuE&qV^;-x9Z5lCKkqS-SH6$?M{dGt zpwVWFb>?E2CTrqka34zS)IH!A%`D7YtZPx0lSLnLcI%x55!OSIRXX5>N8Znu4=ddj z%(V6FtPo|RF{a_qlZ=>zoSSZfR}t5TS&|dA`;SrB(+NJ(>B??D7w_w4&$(ysv;(FJ zys%(jMOSB?L$a4Vyu+{0$;i%WK*tg&j(%@skthhLo2o3?$d(trS-)XVN3wOn`;Nu) z9C;5>82fauZ#yqZY0Kx?KqHVbSQ@LO@KBO$*Tt2Ibl13?<>-8Z*61tvC>4?jY(Uab0}6keAdp zU{o%Qr~z`{b2_-iEEK|(T}UiOx4r+x%XVVceMTEfiyf2n_CLq}PLj#UHJ!E>udX&W z!X&Mj5M)hV)(r>PZi^i;1HqkDs0?V47Ot)gQG`-UDRiuO&oW;Y*U;NBRl4;IOyVrL zIK5;gTNmfv(`s+;eKB*ODZ4~ZQ@326ywH1FonC8F`NY7EcfD5oZJPd_Z>c& zrs39Z^+ImhHYT=NzMvE5O>^vH$BT)wi86a1UP{~;&U~jmP#qsm0E!Zle126U{$YG; z7VFpWA!i6xjVj@K_xkpUoS(QzPA?GCU1_&db)E_Xrc=I;yI`ePD(1X!jup4@5JKII z;hQK~=XufQg)Tc*qOngx<12A7<`ZIEaO^2JHR9GrT2RlRG*Hhf{ldND_9_-OKcNLe z>rC7m+k}tVH3}OxrB(MrFV?OlRRFOs*(8=Kl5E|1Y!~9nuppsJ1 z5|fVUec^natfO_5urrjUpc(}Y#YfYN9VMKOhOW~TN7mE z{ak1Ik6HF=OVs6spJ$PGeJ7@ibTXlG1P0%%CaT(vQs;pNtH0Z}7{q&q1#cR6U7pK4 zG-jg8S|?~~gMa|*yeTke&Hm+D_N)nDH1a0OR>mBO?jZ1h7g)-R$I8Mp%{AxI6Y>%| zPVG*jNDazm>h>N^MnA2raN)ry`H%s5*#9nN+t?(QCObR3}P!AlhC4}?rrWQ zQ@(fFypOE&eskiu;BJMC=3CHL%YIIapl>mCRqja@bvTnw9@(WTpOE#`>BwIJRnj`c zHvQrjm9Q})z+!Tl(;YG~#&?E% zYoh5>W5N3i$U30-Gk83IHRq7*c|_=E%LRAmdxuP#NMr-h3uB(!92ux%*nOE(ev7o< zPRav2rTJZEV2`Vk^z8eYM6fWDoHO8;?L8i!8bkLNWpb_Z7kH9-qE@sD9;20up6WOy zf33RBNdzoZu`3@r`bFjWR1s*OyM~^1BeanMjnt8fnPjpWABzw70+Quv1dk;<>MoGM zq(ta)R@#14BSi0x@OvPa#M$8?^g#4-N9Wt7u34#x0;2q8hRKKuM>7xrAb9F2-ao<1 ztxs*~L9bZE#L|ux;6;x*zwZ+Q@5KNK<`s>NzGuYrP8nI(;lq{--djMWBL)0cIl)3!iFNn@AO%t9Pu_R}7|t!eBh&HOIi1a0iI z%z)XT&;OlYWh^wO&DYF(bbbpws93$(0sl<527#mK&-~5;^cnQTDhPpY0v=sQvIA&@typbC?mJs@e zREIEWG26V~=Q-FfK8?Am(`X08JyfVIC3X~Hr&YVizY!`PXH~k)38v8(WM#sUCvh=y z!SSM*=$z?2Asdra7BOH}j5$oK-Zdoony|*uZZeYI#Q{$Zk5*uy%7y~ftEl2Zs!|(V zRI0}VLf_0TgavNVvJ!X>-7EdqS2l;vkZ!t)6!Q>K(SJ5&LHy)KvUKE#E$E8+a)5xH5rU0Q@MjTtW6J~vWq4G@eEPu z+s}J%zj*~jr`*nB3OOemuN{rH{ftisg53Y=u4BU`DG$h>CMSSuE5{<7dc`>|npEF> zNhnKvESoI|fTjfU`a+&KL>ZW20#ef!m}|e_q5~CrNXP(a%C&kEBs)!?_ddf(AgFI< z900iDx=39_tJ<|maxR@%)_bC2LgJVSmVaKT-=e(Hh+_$NmX!K*ogohHuR_&M5+k%Rg+?PP-S z1&{0fF<=SJOvAeiG`2%Klw2ghVmQT=t=vZf)GgQwGQfQM_?rhY@`;4Pp)hdc-|ONE zC4mum@}1|d8hFzxpNMk(cuJVsO|ezGrcmTgKERbaH@K?)orFCR7*>db?!5bkJ$??^LiR)+grOi3)+-MeTyZ-3WlX1jNyE#XHV)I zAL4ow0=r5A=yVA`Q!n(86lU?Zs`@IUzb6#p|G;Q#tBVN7EVb~MhL&)n2! Up6&yGCj-*aGPqWF_3q>U2U?MlrT_o{ literal 0 HcmV?d00001