久远的记忆,发上来和有缘人分享一下,格式有些乱掉了。。。

内部交流系列一

人工神经网络中的一种——反向传播神经网络(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)提前终止

 posted on 2011-04-29 22:47  goonfly  阅读(1113)  评论(0编辑  收藏  举报