久远的记忆,发上来和有缘人分享一下,格式有些乱掉了。。。
内部交流系列一
人工神经网络中的一种——反向传播神经网络(BP ANN)
·写在前面的话
一个人的理解难免有偏颇,所以以下的言论不可全信,要抱着怀疑的态度审视。本着传达思想又不吓到人的目的,相关的名字可能不会解释太多,有兴趣的话可以查资料深入了解一下。
(注:人工神经网络这是一个大的学科分支,以下主要针对BP ANN)
1、引子
多人对战中的电脑玩家,如何应对才会让人觉得像在和真人对战,不至于太简单或太难而失去兴趣?
写一个状态机能解决这个问题:
Switch 电脑玩家类型
Case 肉搏力量型
If( 友军人数很多 || 敌人攻击力很弱 || 自己生命值饱满 || 敌人距离太近)
冲上去PK再说
Else if
。。。 。。。
Case 法术远战型
。。。 。。。
。。。 。。。
状态机很常用也确实可以解决问题,但是看起来要想做到惟妙惟肖,会有太多种情况等着我们。而且,这个AI是死的,它的行为一出生就注定了,应该尝试造个活的!
如果你已经想到了办法,先别急着往下看,花点时间去尝试实现它,别让你的PC世界太孤单。
2、还是引子
(1)假设已知圆的定义,下面哪个更像圆?
(椭圆、园、正方形的三个图形)
(2)假设已知1999的平方 = 3,996,001 3999的平方 = 15,992,001
那么2999的平方 = ? (如果再已知3000的平方 = 9000,000 ,2999的平方 = ?)
我们比较哪个更圆的时候,会将图形和自己认知的圆去比较,凭经验或者说直觉,估计每个图和圆的误差,选出误差最小的。
硬伤:倘若你选了第2个,但是有人告诉你第3个才是他们所规定的正圆,那么下次再让你选,也许你会选第3个,因为有人这样教过你。
猜测2999的平方的时候,根据以前的学习经验和尝试,估计其值大概在(3,996,001 + 15,992,001)/2左右,知道3000的平方以后,猜测会更准,如果再知道2998的平方根,甚至有可能直接猜对。
硬伤:但是如果只告诉你2998、2999、3000的平方值,去猜测1999、3999的平方,误差显然会很大,直接猜对的几率更是微乎其微。
上面描述的处理过程,可能有所偏差,不过大体上BP神经网络就是从类似这样的角度,模仿人脑的处理过程,首先学习样本集合(1999的平方 = 3,996,001 同时3999的平方 = 15,992,001 同时 3000的平方 = 9000,000)、不断调整自己的权值以拟合样本集合的结果,注意是拟合,这个小小的神经网络,不知道什么是平方(像我们小时候),告诉它输入A要给出结果B,他就不断内部调整,努力实现由A得B,再告诉它要由C得D,它又会努力拟合,这时候问它由A得出什么?,可能回答又不是刚才的B了,于是,继续不断的训练,直到误差小于阀值,或者很不幸的死循环下去了(一般是训练样本不好造成不收敛)
3、简单的定义
说了很多,是为了先指出神经网络的硬伤,以免大家像刚接触这些思想时的自己一样,误认为什么问题都可以设计一个ANN来处理,神经网络,其实就是一个根据训练样本,通过大量反复的计算,构建出来的解决和样本数据有关的问题的一个公式。
神经网络 ≈ 一个奇妙的公式
言规正传,人工神经网络是信息学和生物学的交叉学科分支,主要的思想是从生理角度模拟人的大脑(神经系统),在模式识别领域使用比较广泛,如手写字识别、天气预报、水域水质鉴定、环境质量评价等。
神经网络有很多种并且还在不断的快速演化,所以理解其主要思想是最关键的。使用广泛同时也是比较容易理解的是反向传播神经网络(Backpropagation ANN,简称BP)。
补充一点,传统意义上的人工智能,大多从心理角度模拟人,比如棋类等人机博弈,通过启发式算法,会选择从当前形势能转化出的最好形势(或局部最好形势),深度优先、广度优先或者减枝算法等,都可以找到这样一个解。
如果用神经网络来解决这样的问题,不要指望它会很强,除非有合适的样本和方法,不断的训练它。这正像从小到大,多位老师指导下,我们自己从无知逐步积累的过程。
4、一个很重要的概念——活化函数(activation function)
以下引自《游戏开发中的人工智能》:
成人大脑约有1011个神经元,而每个神经元可经由突触,接收约104个其他神经元的输入电位。如果所有输入电位的结合效果足够强,神经元就会活化,将其活化电位传给其他神经元。对于单个神经元,它的活化函数把总输入值以非线性方式对应到相应的输出值。
5、理想的AI对手或同伴
啰嗦了很多,代码中看个究竟吧。
神经网络由神经元组成,BP中常用的是三层网络,输入层、隐匿层、输出层,每层都是由神经元构成,“输入层--隐匿层”以及“隐匿层--输出层”之间的神经元彼此互相连接,每个连接又有不同的权重(训练的过程这些权重会被不断调整),详见下图。
(1)通用层----NeuralNetworkLayer
BP网络的设计,可以把每一层看成一个基本的单元,抽象出一个层的通用类,属性包括以下这些:
private int numberOfNodes; // 该层神经元节点的数量
private int numberOfChildNodes; // 下层神经元节点的数量
private int numberOfParentNodes; // 上层神经元节点的数量
private double[][] weights; // 连接上下层节点的二维权重数组
private double[][] weightChanges; // 对权重做调整的校正值数组(实现动量法)
private double[] neuronValues; // 该层内神经元计算所得值(活化值)
private double[] desiredValues; // 该层内神经元所要的值(目标值)
private double[] errors; // 该层中每个神经元相关的误差
private double[] biasWeights; // 该层中和每个神经元相关的偏差权重值
private double[] biasValues; // 该层中和每个神经元相关的偏差值(通常为1或-1)
private double learningRate; // 学习率,用于计算权重校正值
private boolean linearOutput; // 该层神经元是否使用线性活化函数(输出层用)
private boolean useMomentum; // 调整权重值时是否使用动量
private double momentumFactor; // 动量系数
private NeuralNetworkLayer parentLayer; // 上层实体
private NeuralNetworkLayer childLayer; // 下层实体
重要的方法也不多:
// 随机初始化权重(有初始值才能开始训练)
public void randomizeWeights(){。。。 。。。}
// 利用神经元输入值和活化函数,计算该层内每个神经元的活化值或内含值
public void calculateNeuronValues(){。。。 。。。}
// 计算每个神经元的误差
public void calculateErrors(){。。。 。。。}
// 调整权重
public void adjustWeights(){。。。 。。。}
(2)整体网络----NeuralNetwork
属性自然是网络中包含的层:
private NeuralNetworkLayer inputLayer;
private NeuralNetworkLayer hiddenLayer;
private NeuralNetworkLayer outputLayer;
方法:
// 设定输入值,用在训练时设定输入数据和网络实际运用时设定输入值
public void setInput(int i, double value){
// 根据某组输入值,指定需要的输出值,自然是训练时用了
public void setDesiredOutput(int i, double value){
// 根据一组输入值,产生输出值
public void feedForward(){
// 计算某组输出值的误差
public double calculateError(){
// 利用倒传递技巧,调整连接的权重值
public void backPropagate(){
outputLayer.calculateErrors();
hiddenLayer.calculateErrors();
hiddenLayer.adjustWeights();
inputLayer.adjustWeights();
}
(3)网络训练员----Trainer
首先需要构建一个合理的训练样本,这个对一个网络是至关重要的,例如:
private double[][] trainingSet = {
//友军 允许被击中次数 敌人是否交战 距离 追逐 群聚 闪躲
{0, 1, 0, 0.2, 0.9, 0.1, 0.1},
{1, 1, 1, 0.2, 0.9, 0.1, 0.1},
{0, 1, 0, 0.8, 0.9, 0.1, 0.1},
{0.1, 0.5, 0, 0.2, 0.9, 0.1, 0.1},
{0, 0.25, 1, 0.8, 0.1, 0.9, 0.1},
{0, 0.2, 1, 0.2, 0.1, 0.1, 0.9}
};
(注意这里把每个值,都转化到了[0,1]区间内了,很常用的做法)
有了训练样本,还需要写个训练方法:
public void training(){
int c = 0;
double error = 1.0;
// 误差大于阀值或者训练次数小于阀值,继续训练
while((error > 0.01) && (c<50000)){
error = 0;
c++;
for(i = 0; i < trainingSet.length; i++){
// 设定输入
nn.setInput(0, trainingSet[i][0]);
nn.setInput(1, trainingSet[i][1]);
nn.setInput(2, trainingSet[i][2]);
nn.setInput(3, trainingSet[i][3]);
// 设定预期输出
nn.setDesiredOutput(0, trainingSet[i][4]);
nn.setDesiredOutput(1, trainingSet[i][5]);
nn.setDesiredOutput(2, trainingSet[i][6]);
// 根据输入产生输出
nn.feedForward();
// 计算某组输出值的误差,并累加起来
error += nn.calculateError();
// 利用倒传递技巧,调整连接的权重值
nn.backPropagate();
}
error = error / (float)i; // 最后看的误差是均值
}
}
示例:
**** 训练 14784 次,最终误差是 0.009999912609213308 ****
训练结束后,可以保存所有的权重,以便下次直接使用;也可以每次都训练它,由于初始化的权重是随机的,特定的一次训练找到的权重,可能仅仅是局部最优的。还记得开始提到的硬伤么----如果只告诉你2998、2999、3000的平方值,去猜测1999、3999的平方。
使用的方法,也简单,设定输入,计算输出,搞定
public void using(float[] input){
nn.setInput(0, input[0]);
nn.setInput(1, input[1]);
nn.setInput(2, input[2]);
nn.setInput(3, input[3]);
nn.feedForward();
}
训练出来的比较胆小的家伙:
友军数 生命值 敌人战斗力 敌人距离
0 9 0 2
输出层最终结果,取最大值为最终结果:
应该 " 和 敌 人 战 斗 " 的概率: = 0.762632540158345
应该 " 向 友 军 靠 拢 " 的概率: = 0.0034357177373021347
应该 " 赶 紧 闪 人 " 的概率: = 0.37737883506167563
友军数 生命值 敌人战斗力 敌人距离
0 9 1 2
输出层最终结果,取最大值为最终结果:
应该 " 和 敌 人 战 斗 " 的概率: = 0.21038989157498075
应该 " 向 友 军 靠 拢 " 的概率: = 0.0015496997584573229
应该 " 赶 紧 闪 人 " 的概率: = 0.9544101889559983
友军数 生命值 敌人战斗力 敌人距离
2 5 1 3
输出层最终结果,取最大值为最终结果:
应该 " 和 敌 人 战 斗 " 的概率: = 0.8208737696486574
应该 " 向 友 军 靠 拢 " 的概率: = 0.023020775229503063
应该 " 赶 紧 闪 人 " 的概率: = 0.05766601778934931
友军数 生命值 敌人战斗力 敌人距离
2 5 2 3
输出层最终结果,取最大值为最终结果:
应该 " 和 敌 人 战 斗 " 的概率: = 0.1763137653114205
应该 " 向 友 军 靠 拢 " 的概率: = 0.02856626329560574
应该 " 赶 紧 闪 人 " 的概率: = 0.3441468773948959
以上是一个简单的入门例子,输出结果的可信度也未必高,但是相信当你成功实现它之后,会有一种喜悦,甚至震撼,事实上能做的还很多,需要继续去学习了解的也很多。
最后不要误认为神经网络只能做个小玩具,网络的神经元、隐藏层数目可以更多,输入值可以是二氧化碳含量、颗粒物含量、温度湿度,从而告诉我们空气质量是几级;可以是大气的各种参数,从而告诉我们明天会不会下雨,等等;人脑是目前最高的智能体,所以人工智能和神经网络,值得大家花时间去探索体会一下。
6、Java类库:
Joone,已经发展了很久,有图形化界面
7、推荐书籍
<游戏开发中的人工智能><AI for Game Developers>(David M. Borug & Glenn Seemann)
<游戏编程中的人工智能技术><AI Techniques for Game Programming>(Mat Buckland)
8、算法步骤
1、用很小的随机值将权初始化
2、选择一个输入模式,加入输入层
3、将信号通过网络前向传播
4、将实际输出和期望输出相比较
5、通过将误差方向传播,计算前一层的误差
6、更新所有连接权重
7、对于另一个输入,返回第二步并重复执行该过程
(即利用前一次更新过的权重,所以有偏爱后出现的输入的情况,喜新厌旧)
9、优缺点:
1、优点:
(1)一个3层BP网络可以完成任意n维到m维的映射
2、缺点
(1)非线性优化,会遇到局部最小值的情况
(2)收敛速度慢,千步以上
(3)信息向前、向后流过网络,并利用导数,生物学角度缺少可信性
(4)网络隐节点个数凭经验选取
(5)喜新厌旧,同时每个样本包含信息数必须相同
10、注意事项:
1、防止过拟合
(1)把神经元数目减到最小
(2)加入摇摆(jitter)
(3)提前终止