1. 模型图示
感知机仅能解决线性的问题,这个局限性使得其无法适应多数的实际应用。因此人们提出了神经网络。如图2.1所示。
图2.1 神经网络
2. 相关技术
技术2.1 隐藏层
从结构上看,神经网络有多层,比感知机复杂。除了输入层、输出层,还增加了1个或多个隐藏层。输入层与输出层节点的个数由具体应用所确定,这个和感知机没有区别。隐藏层的层数、每层节点个数(这两个可称为神经网络的参数),则对神经网络的效果起到很大作用。
对于神经网络的新手玩家而言,针对具体应用,调整这些参数既可能是有意思的活儿,也可能是枯燥的活儿。
仅仅增加隐藏层能否增强网络的能力?很遗憾,答案是否定的。用简单的推导可以证明,增加隐藏层(可以是多层)并未改变线性的本质,和单层结构的作用完全相同。
技术2.2 激活函数
激函数才是使得神经网络变得无所不能的技术。如图2.1所示,第1层的输出是 ,它们经过变换,成为 。干这个事儿的就是激活函数。
sigmoid
它有良好的数学性质:
- 把数据从映射到;
- 从0往两边,变化速度很快,然后平稳;
显然,经过激活函数后,神经网络所表达的变换肯定不是线性的了。
图2.2 sigmoid函数
tanh
它也有良好的数学性质:
- 把数据从 映射到 ;
- 从0往两边,变化速度很快,然后平稳;
- 图2.3 tanh函数
技术2.3 多层反馈
一般将神经网络称为BP神经网络,BP就是Backpropagation。从输出层可以向输入层反馈,逐步调整每层的权重。越靠近输入层,梯度越小,权重改变越小,因此一般5层就够了。
权值更新的具体步骤如下:
Step 1. 根据激活函数调整loss. 使用 损失时, 令输出层的节点值为 , 标准的输出为 之间, 激活函数为 Sigmoid. 则输出节点的误差为:
Step 2. 将这些 loss 反向传递, 由前一层的各条边承担. 令 表示第 层的节点数, 表示第 层的第 个节点的当前数据, 表示第 层的第 个节点的当前误差, 表示第 层第 个节点与第 层第 个节点之间边的权重. 则边的权重更新为
其中 为学习速度. 如果还要考虑动量调整, 式(4) 可以加一个分量.
节点的误差更新为
其中,前半部分与激活函数求导有关, 与式(3)同理; 后半部分则与下层各节点上的损失, 以及边的权重有关.
技术2.4 输出层表示
神经网络的输出层是多样的,既可是一个节点,也可以是多个。这使得它可以应对不同的任务。例如,回归时只需要一个节点;分类( 个类别)需要 个节点,每个节点表示一个 区间的实数值,哪个节点的值大,就预测为相应类别;多标签学习( 个标签)需要
3. 代码分析本节给出 java 代码分析,原代码只有70行,点击访问。我加入读数据代码后放到gibhub,点击访问。
3.1 模型定义
//层数, 包含输入层与输出层.
int numLayers;
//每层的结点数(本数组长度即为层数), 如 {5, 8, 6, 2} 表示输入层5个节点,
//两个隐藏层分别为8个和6个,输出层为2个(可以做2分类)
int[] layerNumNodes;
//神经网络各层节点(对应的临时值)
//对于前面的例子, layerNodes.length = 4, layerNodes[0].length = 5
public double[][] layerNodes;
// 神经网络各节点误差, 与 layerNodes 维度一致
public double[][] layerNodesErr;
// 网络各边权重. 第 i 层节点 与第 i + 1 层之间有边,所以例中 i = {0, 1, 2}, edgeWeights.length = 3;
// edgeWeights[0].length = 5, edgeWeights[0][0].length = 8
public double[][][] edgeWeights;
// 各边权重变化量
public double[][][] edgeWeightsDelta;
// 动量系数, 模拟惯性
public double mobp;
// 学习系数
public double rate;
从代码看出,网络可以用二维、三维数组表示. 基础的表示使我们能够清晰观察内部机制.
3.2 前馈
public double[] computeOut(double[] paraIn) {
// 初始化输入层. paraIn为一条输入数据 (一个训练对象)
for (int i = 0; i < layerNodes[0].length; i++) {
layerNodes[0][i] = paraIn[i];
}// Of for i
// 逐层计算节点值
for (int l = 1; l < numLayers; l++) {
for (int j = 0; j < layerNodes[l].length; j++) {
// 初始化为偏移量, 因为它的输入为 +1
// l - 1表示边的层号
//layerNodes[l - 1].length 表示上一层的节点个数, 由于是偏移量,它在上一层没有节点,是附加的节点, 其取值为1
// j表示本层当前需要赋值的节点.
double z = edgeWeights[l - 1][layerNodes[l - 1].length][j];
// 计算加权和
for (int i = 0; i < layerNodes[l - 1].length; i++) {
// l - 1表示边的层号, i表示上一层节点号, j表示本层节点号
z += edgeWeights[l - 1][i][j] * layerNodes[l - 1][i];
}// Of for i
// Sigmoid激活函数, 代码和公式一样简单.
layerNodes[l][j] = 1 / (1 + Math.exp(-z));
}// Of for j
}// Of for l
return layerNodes[numLayers - 1];
}// Of computeOut
通过3层循环对 layerNodes 赋值,就完成了1次前馈。
3.3 反向传播
public void updateWeight(double[] paraTarget) {
// Step 1. 初始化输出层误差
int l = numLayers - 1;
for (int j = 0; j < layerNodesErr[l].length; j++) {
//前面一半是对 sigmoid 求导, 后面一半是误差 (带符号)
layerNodesErr[l][j] = layerNodes[l][j] * (1 - layerNodes[l][j])
* (paraTarget[j] - layerNodes[l][j]);
}// Of for j
// Step 2. 逐层反馈, l == 0时也需要计算
while (l > 0) {
l--;
// 第l层, 逐个节点计算
for (int j = 0; j < layerNumNodes[l]; j++) {
double z = 0.0;
// 针对下一层的每个节点
for (int i = 0; i < layerNumNodes[l + 1]; i++) {
if (l > 0) {
z += layerNodesErr[l + 1][i] * edgeWeights[l][j][i];
}// Of if
// 隐含层动量调整. mobp 表示对上一个对象训练时 delta 的怀念
// layerNodes[l][j] 是由加权和式子求偏导而得到,表示丢锅的程度
edgeWeightsDelta[l][j][i] = mobp
* edgeWeightsDelta[l][j][i] + rate
* layerNodesErr[l + 1][i] * layerNodes[l][j];
// 隐含层权重调整
edgeWeights[l][j][i] += edgeWeightsDelta[l][j][i];
if (j == layerNumNodes[l] - 1) {
// 截距动量调整, 对平常节点同理, 但需要单独计算
edgeWeightsDelta[l][j + 1][i] = mobp
* edgeWeightsDelta[l][j + 1][i] + rate
* layerNodesErr[l + 1][i];
// 截距权重调整
edgeWeights[l][j + 1][i] += edgeWeightsDelta[l][j + 1][i];
}// Of if
}// Of for i
// 记录误差. 又要乘以 sigmoid 函数的导数, 为下一次后向传播作准备.
layerNodesErr[l][j] = layerNodes[l][j] * (1 - layerNodes[l][j])
* z;
}// Of for j
}// Of while
}// Of updateWeight
反向传播的关键点包括:
- sigmoid 函数的求导. 如果激活函数换了, 相应代码进行变化即可.
- 加权和函数的求导. 估计一般的网络不需要变. 后面的 CNN 之类再说吧.
- 动量系数. 这个已经属于高阶功能了.
感慨一下:基础代码又简单又有效。
4. 小结
- 每层都有针对偏移量的 ,参见技术1.2.
- 在实际应用中,神经网络的层数、每层的节点个数、每个节点使用的激活函数,都对其性能产生重要影响。所谓的“调参师”,就是干这些活儿。
- 在本文所述的几个技术上进行改动,都可以做出新的工作。