在机器学习这一个未来的、伟大的领域,有一个小小的分支叫做人工智能。
举个例子:照片识别、信用卡图片识别、声音语言解析、数据聚类与离散。
机器学习按照有无监督者(或者有无导师),区分为监督学习和无监督学习http://www.cnblogs.com/ysjxw/articles/1149004.html
监督学习是最常见的学习分类,在训练集中指出正确的结果,让网络自己去适应这种结果。
而无监督学习,则大致有两种实现思路。
第一种思路是在指导Agent时不为其指定明确的分类,而是在成功时采用某种形式的激励制度。
第二种思路是称之为聚合(原文为clustering,译者注)。这类学习类型的目标不是让效用函数最大化,而是找到训练数据中的近似点。聚合常常能发现那些与假设匹配的相当好的直观分类。例如,基于人口统计的聚合个体可能会在一个群体中形成一个富有的聚合,以及其他的贫穷的聚合。
今天我们来从算法上分析一下人工智能的一种实现brain.js。它有输入集、隐层、输出集的基本结构,采用了BP神经网络结构,有自己的激活函数simoid,有自己的误差backpropagation算法。
之前已经有一篇文章介绍了brain.js的用法和内部的代码结构,因此不再赘述,今天重点描述一下brain.js的BP算法和数据结构。了解这些是很有必要的,当你可以熟练掌握这些基础知识,你可以很直接的创造一套自己的人工智能工具,例如:一个简单的验证码识别工具,一个图片分类器,一个看似有点智能的自动对话程序……
第一章、构造函数
var NeuralNetwork = function(options) {
options = options || {}; //初始化全局变量options
this.learningRate = options.learningRate || 0.3; //设置学习比率 是options.learningRate或者0.3
this.momentum = options.momentum || 0.1; //惯性比率,用于设置“新的变动的影响程度”
this.hiddenSizes = options.hiddenLayers; //隐层的节点的数量是一个二维数组:层数/该层节点数
this.binaryThresh = options.binaryThresh || 0.5; //阈值
}
在构造函数中初始化一些全局的值,如备注所式,用于设置一些具有全局性影响的值。
参数含义如下:
learningRate用于影响节点对新知识的学习速度,如果该比例较大,则每个node在变动时会采用较剧烈的幅度,速度会加快稳定性会下降
momentum用于影响节点对旧知识的保留,如果该比例较大,则节点发生的变动会有较剧烈的幅度,速度会加快稳定性会下降
hiddenLayers是隐层的节点的数量,是一个二维数组:层数/该层节点数,涉及在训练时(神经网络从训练开始,因此此句话等价于“在开始时”)初始化隐层节点的数量。
binaryThresh是一个阈值,在test模式下才能使用
第二章、训练
train: function(data, options) {
data = this.formatData(data);
……
}
我们逐行解析一下,挑重要的看:
var iterations = options.iterations || 20000; //设置每个节点的迭代次数为20000,用逼近论逐步逼近目标
var errorThresh = options.errorThresh || 0.005; //误差的目标值为0.005
……
var inputSize = data[0].input.length; //设置输入数量为data[0]的输入的数组的长度
var outputSize = data[0].output.length; //设置输出数量为data[0]
var hiddenSizes = this.hiddenSizes; //隐层数量
if (!hiddenSizes) { //如果隐层数量未初始化,则设置隐层数量为一个数组,数组的0号元素为3到inputSize/2的MAX
hiddenSizes = [Math.max(3, Math.floor(inputSize / 2))];
}
this.initialize(sizes); //初始化,详见第二章第一节、初始化
for (var i = 0; i < iterations && error > errorThresh; i++) { //遍历iterations次,并且在当前偏差大于误差的目标值为0.005的情况下进行训练
var sum = 0; //误差初始值为0
for (var j = 0; j < data.length; j++) { //对data数量进行逐个训练
var err = this.trainPattern(data[j].input, data[j].output, learningRate);//详见第二章第二节训练匹配
//训练trainPattern,传入的值为第j个输入,第j个输出,学习比率,输出结果为当前这一行数据,在各个网络层各个节点的误差的平方的平均数
sum += err; //计算误差的和
}
error = sum / data.length; //计算误差的平均值
if (log && (i % logPeriod == 0)) { //如果在logPeriod的倍数,则打印日志
log(“iterations:”, i, “training error:”, error);
}
if (callback && (i % callbackPeriod == 0)) { //如果在callbackPeriod的倍数,则调用回调函数
callback({ error: error, iterations: i });
}
}
第二章第一节、初始化
this.initialize(sizes);
在该方法内部,会对神经网络的数据结构进行初始化,代码比较简单,我们关键描述几个数据结构
this.outputLayer = this.sizes.length - 1; //输出层的最后一个下标
this.biases = []; //阈值的二维数组:层/值
this.weights = []; //权重的三维数组:层/节点/值 [layer][node][k]
this.outputs = []; //输出结果的二维数组:层/值
this.deltas = []; //偏差的二维数组:层/值
this.changes = []; // for momentum //改变值的三维数组:层/节点/值 [layer][node][k]
this.errors = []; //误差的二维数组:层/值
第二章第二节、训练匹配
this.trainPattern(data[j].input, data[j].output, learningRate);
该方法在两层循环中被调用,第一层循环为训练次数iterations,第二层循环为data[j]输入的数据的数量
完成了加快训练的效果(目前很多神经网络的重点就在提高训练速度和减少误差的方向上)
this.runInput(input);//信号的前馈传播,详见第二章第二节第一部分、信号的前馈传播
this.calculateDeltas(target);//计算误差deltas,详见第二章第二节第二部分、误差的反向传播一计算误差deltas
this.adjustWeights(learningRate);//重新计算权重,详见第二章第二节第三部分,误差的反向传播二权重的重新调整
第二章第二节第一部分、信号的前馈传播
this.runInput(input);//信号的前馈传播
对input为单行的输入数据向量进行操作,该方法代表了一个神经网络对某行数据的操作
this.outputs[0] = input;//设置this.outputs的第0个元素为输入的数据
//针对每个输出层/隐层去执行,从第一层开始,(0层为输入)
for (var layer = 1; layer <= this.outputLayer; layer++) {
//针对当前隐层的节点进行操作
for (var node = 0; node < this.sizes[layer]; node++) {
//获取当前节点的权重数组
var weights = this.weights[layer][node];
//获取当前节点的阈值
var sum = this.biases[layer][node];
//对每个输入项进行遍历
for (var k = 0; k < weights.length; k++) {
//总和等于当前节点和上一个节点的输入乘以权重
sum += weights[k] input[k];
}
//激活函数-输出值等于1除以(1+-sum的以e为底的指数函数,即e的-sum次方,位于0-1之间)
this.outputs[layer][node] = 1 / (1 + Math.exp(-sum));
}
var output = input = this.outputs[layer]; //递归将当前的输出作为下一层的输入
}
return output; //在所有层的神经网络通过之后,输出结果
上述代码中的激活函数,并不是去激活什么,而是指如何把“激活的神经元的特征”通过函数把特征保留并映射出来(保留特征,去除一些数据中的冗余),这是神经网络能解决非线性问题关键。
激活函数是用来加入非线性因素的,因为线性模型的表达力不够
该算法为simoid激活函数,但是如果出现极值,该算法可能会出现大批节点死亡,即值为0无法激活的情况
其他可选算法推荐ReLU即f(x)=max(x,0);比较简单
关于激活函数的研究,可以参考http://www.mamicode.com/info-detail-873243.html
①单侧抑制 //通过激活函数把特征保留并映射出来
②相对宽阔的兴奋边界 //??
③稀疏激活性 //节省能量,生物神经具有稀疏激活性,因此在数学逻辑上也是需要有稀疏激活特性的
以上红色内容,是小白理解神经网络的关键~~~
但是对于专业数学家,这个是需要用数学证明的:证明特征是可再分的,再分的特征根据贝叶斯公式,多个特征将会休整人的信念,再根据逼近论用较简单的函数来代替复杂的函数。
最终可以通过任意个节点来拟合任何一个复杂函数。
这就是神经网络最神奇之处,它可以实现任何一个符合函数的拟合,也就是说:输入一个图片,它能告诉你这张图片上的物种叫做猫。
第二章第二节第二部分、误差的反向传播一计算误差deltas
this.calculateDeltas(target);//计算误差deltas
在BP神经网络中、误差的反向传播,进而对权重进行调整是核心内容
代码如下:
calculateDeltas: function(target) { //误差计算
for (var layer = this.outputLayer; layer >= 0; layer–) { //对每个输出层进行遍历,逆向计算
for (var node = 0; node < this.sizes[layer]; node++) { //对当前层的节点数进行遍历
var output = this.outputs[layer][node]; //获取当前节点的输出
var error = 0; //偏差为0
if (layer == this.outputLayer) { //如果是最后一层
error = target[node] - output; //误差等于目标值减去输出-》即初始误差
}
else { //非最后一层
var deltas = this.deltas[layer + 1]; //获取到下一层的误差的数组
//deltas在最后一层时赋予了初始值,此后随着层数的减少,低次获取上一次的误差//deletas是二维数组,层/值
for (var k = 0; k < deltas.length; k++) {
error += deltas[k] this.weights[layer + 1][k][node];
//当前节点的error为下一层的偏差按照权重的总和值
//此处对误差的评估有多种算法,算数加权 为其中一种,此外还有开方差等方法
}
}
this.errors[layer][node] = error; //当前层,当前node的error
this.deltas[layer][node] = error output (1 - output);
}
}
},
上述代码有两个要点
1、error的计算,是通过下一层的误差根据权重汇总起来的
2、deltas的计算,是通过公式deltas=error output (1-output)计算出的
在最后一行代码中,有一个神奇的deltas = erroroutput(1-output)
//当前层,当前node的为errors误差 当层的输出 (1-当层的输出)
//该算法为BP前馈申请网络的误差传播算法,详见论文http://www.doc88.com/p-601587528491.html
//大概原理如下:BP
//BP算法实质是求取误差函数的最小值问题。采用非线性规划中的最速下降方法,按误差函数的负
//梯度方向修改权系数。从其数学表达可知:多层网络的训练方法是把一个样本加到输入层,并根据
//向前传播的规则X(k,j) = f(U(k,j)),一层一层向输出层传递,最终在输出层得到输出X(m,i),把X(m,i)
//和期望输出Yi进行比较,若两者不等,则产生误差信号e,接着按下式反向传播修改权系
//此处公式推导可见http://blog.csdn.net/zhouchengyunew/article/details/6267193的“二、BP算法的数学表达”章节
//或者关于BP网络的数学表达可以参见http://www.cnblogs.com/wengzilin/archive/2013/04/24/3041019.html
//最终结论为this.deltas[layer][node] = error output (1 - output);
//但比较粗糙直观的理解是:
//计算出的error是子层误差的总和,而output和(1-output)是一种修正,因本例中激活函数为simoid,因此output在(0,1)之间
//因此output和(1-output)呈x在[0-1]区间的倒U型,因此output如果位于0.5时,error就会有较大的变动,
//位于两边时,就会有较小的变动,因此delta = error output (1 - output); 这个函数就会有一种“趋势”来
//倾向与0,结合adjustWeights误差反向传播调整公式,就会使误差在最终趋向于0->实现“误差降低的特性”
第二章第二节第三部分,误差的反向传播二权重的重新调整
this.adjustWeights(learningRate); //重新计算权重—-此处为核心操作
adjustWeights: function(learningRate) {
for (var layer = 1; layer <= this.outputLayer; layer++) { //对所有的层进行判断,除了输入层
var incoming = this.outputs[layer - 1]; //incoming为上一层的输出,这一层的输入
for (var node = 0; node < this.sizes[layer]; node++) { //对当层的节点进行遍历
var delta = this.deltas[layer][node]; //获取到当前节点的delta 是一个数字,为下一层的error下一层的output(1-下一层的output)
for (var k = 0; k < incoming.length; k++) { //根据上一层的输入节点
var change = this.changes[layer][node][k]; //获取到变动前的change
change = (learningRate delta incoming[k]) //对当前节点的change进行修正 change = (学习比率 差值 上一层的该点的输出值) + (当前的保持的势能 之前的change)
+ (this.momentum change); //其核心原理在于error越小->delta越小->change越小—->具有逼近作用
this.changes[layer][node][k] = change; //对当前的change进行保存
this.weights[layer][node][k] += change; //将change增加到weight上
}
this.biases[layer][node] += learningRate delta; //重新设置biases阈值
}
}
},
关键在于change 的计算和 weights的计算
change = (learningRate delta incoming[k]) + (this.momentum change)
其核心原理在于error越小->delta越小->change越小—->具有逼近作用
由上文可知delta是趋向于0的,因此change是最终趋向于0
而weights的计算公式为
this.weights[layer][node][k] += change;
因为change最终趋于0,因此weights最终趋于固定值,即:神经网络的联结的权重最终趋于固定值。
第三章、运行
run: function(input) {
if (this.inputLookup) {
input = lookup.toArray(this.inputLookup, input);
}
var output = this.runInput(input);
if (this.outputLookup) {
output = lookup.toHash(this.outputLookup, output);
}
return output;
},
调用了runInput方法,最终的输出结果为output
方法不再赘述。
—————————————————————————————————
由以上分析看到,神经网络最大的作用就是“拟合”一个复杂函数,该函数输入一堆复杂的值,通过训练,使得一些小函数的和最终模拟出了一个确定的值(逼近论)。
这种特性在“图像识别”“声音识别”等机器视觉和机器听觉领域十分有用。
但是:上述例子的输出结果只能是0~1的数值,代表了是或者否的判断,例如输入一堆图片,判断这些图片上是否有一只猫,它会告诉你是或者否
倘若:你需要输入一堆图片,然后将图片分成五类,或者输出一个图片的色情度是0%还是10000%,它需要进行一些深度的改造。
————————————————————————————————–
由于人类大脑本身的运行机制还不清晰,因此很难说这种程度的人工智能能否真正匹配真实的人类智能。
但前段时间的阿尔法狗人机大战,说明了人类的智能或许在本质上就是一种对过往的拟合,兴许抽象点说:能看懂的都是科学,看不懂但是有效果的叫做智能,看不懂也没效果但是还有人信的叫做宗教
人工做了一个程序,实现了看不懂但是有效果的功能,这就是人工智能。
此外人工智能领域从去年开始CNN比较火热,就是卷积神经网络,用隐层的深度换单个隐层的宽度,这又是另一个课题了,我比较有兴趣的是:卷积层究竟是由人类硬编码实现的?还是由人工智能训练出的?还是由人工智能在漫长的训练中自发形成的?
我究竟是怎么样一个程序员。总结那么几个标签:勤奋、浪、嘴炮型诗人。
——————————————————————————————–
一、勤奋
也许我在别的方面很懒,但是在编程方面我还是自认勤奋的,举个栗子:
1、结婚前一晚,花一夜看完了《UNIX进程间通讯》;
2、半夜睡不着就翻译一篇神经网络文章brain.js
3、星期六星期天不喜欢出门,喜欢在家看代码玩
4、。。。。。。
就像我之前看到的一句话:过了这条线,coding就不是为了钱是为了多巴胺了。这条线之上,程序不再是工具,而是玩具,会让你感到兴奋。你能感觉到语言,代码,算法,模式,思想都碎成粉碎,一块块等着你来拼。你能体会到程序里齿轮般环环相扣的精巧。你能闻到代码的味道,看到代码的形状。
我依稀觉得我差不多在这线的附近了,因为我看到程序就感觉看到了玩具一样,不论是任何语言,不论是任何模式。
而我的学习方法,大致是:
1、遇到一个不知道的东西,先让它能用,再让它好用;
2、把它拆成小碎块,然后拼成我需要的样子;
3、再系统学习一遍这个东西的文档,看看别人是怎么拼的,或者有没有什么神奇的小碎块;
4、随着小碎块越攒越多,我就能拼出越来越复杂的东西,我就越来越强大;
————–世界上有很多大神————-
————–你也可以成为其中一尊————–
二、浪
浪,是我对自己的评价。但当我想举例子的时候,还真举不出来…
难道是一颗闷骚的心脏,却配了一副老实的身体…
——但是真的很浪,竟然没出什么大事……
——从概率论上说,我这么干终有一天会倒霉的……
不过我至今没倒过霉…求虐…没倒过霉的人生是不完整的
浪的行为:主要体现在代码和技术上
1、我喜欢看别人的代码,沿着他的思路走,我就能看到当时候他脑袋里进了什么水,或者中了什么毒
2、技术上我喜欢走一些比较浪的路线,因为正常做没什么意思,如果时间充足的情况下,我还是喜欢走一些比较浪的路线
3、我喜欢伊布拉希莫维奇远超喜欢C罗和梅西——》这足以说明一切
三、嘴炮型诗人
朕是嘴炮型诗人………
这个毫无疑问,举例说明,我刚换的QQ签名:
此事是因为一群人在微信群里推脱着责任,各种使着无用的绊,各种可笑的推脱着责任…………..
在我看来,就是可笑……
睡不着,翻译点资料……
好久之前帮同学写过一篇论文,大致是基于ELM(Extreme Learning Machine极限学习机)的前馈式神经网络.
因此对神经网络的一些概念还算了解。
凑巧最近对人工智能感兴趣,所以百度了一下成型的产品,比如brain.js,然后翻译以及阅读一下。