神经网络入门 第3章 S函数
2017-04-19 12:53 JavaDaddy 阅读(881) 评论(0) 编辑 收藏 举报前言
神经网络是一种很特别的解决问题的方法。本书将用最简单易懂的方式与读者一起从最简单开始,一步一步深入了解神经网络的基础算法。本书将尽量避开让人望而生畏的名词和数学概念,通过构造可以运行的Java程序来实践相关算法。
关注微信号“逻辑编程"来获取本书的更多信息。
上一章我们构造的神经元输入和输出没有任何限制,当我们组成神经网络时就会造成很多问题。输出值过大不但容易造成溢出,而且很很多时候是无意义的。比如,判断一个图片是否写着数字“2”,我们需要的是它是“2”的概率,[0-1]范围的实数是最合适的。
因此,我们需要对我们的神经元输出作出限制。把上一章的函数[y=w*x+b]的输出值通过一个函数变换到[0-1]范围,就可以达到我们的目的。其它逻辑与上一章一致。
那么有什么函数可以把任意实数值映射到[0-1]范围呢?这样的函数不止一种。在这里我们加入一个比较常用的,叫做S函数(Sigmoid)。
当t等于无穷小时函数值趋近0;t值趋近无穷大时函数值趋近于1;0处函数值是0.5。其函数图形如下:
将上一章的简单线性函数替换S函数的自变量我们得到这个函数:
f(x) = 1/(1 + exp(-w*x - b))
其中w决定了S函数在中间处的斜率包括变化方向,b决定了S函数在x轴方向的平移量。下图中展示了几个不同参数的S曲线的形状:
从图中我们可以看到S曲线的一些特征:斜率非常大时类似于二值函数;可以由参数决定从0到1或者从1到0的不同方向;可以平移。S函数是神经网络的一个关键点。我们后面会讨论它的重要性。本章中我们将关注应用S函数之后的单个神经元。
很明显这个函数无法完成我们上一章说的直线拟合,不过它依然可以通过训练拟合到它自己的参数。这就是本章我们要解决的问题。现在我们来继承上一章的Java类,只要覆盖几个函数即可。
首先覆盖f函数,抽取上一章的f函数成z函数,然后加个S函数:
public double f(double x) {
return sigmoid(z(x));
}
double z(double x) {
return x * weight + bias;
}
double sigmoid(double z) {
return 1.0 / (1.0 + Math.exp(-z));
}
经过上边的变换,f的输出被影射到[0-1]范围内了。f函数是通过两个函数链接完成的。
函数变了,其梯度函数也不一样了。比如y(z)=2*z表示一条斜率为2的斜线;z(x)=3*x表示一条斜率为3的斜线。那么y(x)=2*(3*x)=6*x,其斜率是2*3。可见函数的斜率具有链式规则。所以我们只要把S函数的导数乘以z函数的导数就可以了。这里的z函数是我们上一章的f函数。所以我们的梯度函数现在是这样的:
public double[] gradient(double x, double y) {
double[] g = super.gradient(x, y);
double z = z(x);
double dz = dz(z);
g[0] *= dz;
g[1] *= dz;
return g;
}
protected double dz(double z) {
return sigmoid(z) * (1 - sigmoid(z));
}
最后,我们覆盖获得测试目标的函数并对入口函数稍作修改,使用新的神经元类:
protected SingleNeuron getTarget() {
return new SingleSigmoidNeuron(3, 3);
}
public static void main(String... args) {
SingleSigmoidNeuron n = new SingleSigmoidNeuron(0, 0);
double rate = 5;
int epoch = 100;
int trainingSize = 20;
for (int i = 0; i < epoch; i++) {
double[][] data = n.generateTrainingData(trainingSize);
n.train(data, rate);
System.out.printf("Epoch: %3d, W: %f, B: %f \n", i, n.weight, n.bias);
}
}
我们的目标(w,b)与上一章一样,产生训练数据的函数也不做修改。现在我们运行一下新的程序:
Epoch: 90, W: 2.915254, B: 2.966388
Epoch: 91, W: 2.918170, B: 2.968286
Epoch: 92, W: 2.919169, B: 2.968809
Epoch: 93, W: 2.919169, B: 2.968809
Epoch: 94, W: 2.919169, B: 2.968809
Epoch: 95, W: 2.919169, B: 2.968809
Epoch: 96, W: 2.920288, B: 2.969410
Epoch: 97, W: 2.939107, B: 2.992186
Epoch: 98, W: 2.939107, B: 2.992186
Epoch: 99, W: 2.939229, B: 2.992233
呀吼!还是能训练的到我们的目标(w,b)。实验成功!
读者可以思考一下S函数有什么替代品。答案肯定是有的。您可以自己把S函数替换成另外一个符合条件的函数,看看训练结果和效率怎么样。
最后完整代码如下:
package com.luoxq.ann.single;
/**
* Created by luoxq on 17/4/9.
*/
public class SingleSigmoidNeuron extends SingleNeuron {
public SingleSigmoidNeuron(double weight, double bias) {
super(weight, bias);
}
public double f(double x) {
return sigmoid(z(x));
}
double z(double x) {
return x * weight + bias;
}
double sigmoid(double z) {
return 1.0 / (1.0 + Math.exp(-z));
}
// c = w*x + b - y
public double[] gradient(double x, double y) {
double[] g = super.gradient(x, y);
double z = z(x);
double dz = dz(z);
g[0] *= dz;
g[1] *= dz;
return g;
}
protected double dz(double z) {
return sigmoid(z) * (1 - sigmoid(z));
}
protected SingleNeuron getTarget() {
return new SingleSigmoidNeuron(3, 3);
}
public static void main(String... args) {
SingleSigmoidNeuron n = new SingleSigmoidNeuron(0, 0);
//target: y = 3*x + 3;
double rate = 5;
int epoch = 100;
int trainingSize = 20;
for (int i = 0; i < epoch; i++) {
double[][] data = n.generateTrainingData(trainingSize);
n.train(data, rate);
System.out.printf("Epoch: %3d, W: %f, B: %f \n", i, n.weight, n.bias);
}
}
}
这一章我们使用了S函数和链式求导法则。要了解其它章节,请关注微信订阅号逻辑编程。