1. 模型图示

感知机仅能解决线性的问题,这个局限性使得其无法适应多数的实际应用。因此人们提出了神经网络。如图2.1所示。

神经网络隐藏层 神经网络隐藏层数_激活函数

图2.1 神经网络

2. 相关技术

技术2.1 隐藏层

从结构上看,神经网络有多层,比感知机复杂。除了输入层、输出层,还增加了1个或多个隐藏层。输入层与输出层节点的个数由具体应用所确定,这个和感知机没有区别。隐藏层的层数、每层节点个数(这两个可称为神经网络的参数),则对神经网络的效果起到很大作用。
对于神经网络的新手玩家而言,针对具体应用,调整这些参数既可能是有意思的活儿,也可能是枯燥的活儿。
仅仅增加隐藏层能否增强网络的能力?很遗憾,答案是否定的。用简单的推导可以证明,增加隐藏层(可以是多层)并未改变线性的本质,和单层结构的作用完全相同。

技术2.2 激活函数

激函数才是使得神经网络变得无所不能的技术。如图2.1所示,第1层的输出是 神经网络隐藏层 神经网络隐藏层数_感知机_02,它们经过变换,成为 神经网络隐藏层 神经网络隐藏层数_神经网络_03。干这个事儿的就是激活函数。

sigmoid

神经网络隐藏层 神经网络隐藏层数_神经网络隐藏层_04
它有良好的数学性质:

  1. 把数据从神经网络隐藏层 神经网络隐藏层数_感知机_05映射到神经网络隐藏层 神经网络隐藏层数_神经网络_06
  2. 从0往两边,变化速度很快,然后平稳;
  3. 神经网络隐藏层 神经网络隐藏层数_激活函数_07
    显然,经过激活函数后,神经网络所表达的变换肯定不是线性的了。
    图2.2 sigmoid函数

tanh

神经网络隐藏层 神经网络隐藏层数_感知机_08
它也有良好的数学性质:

  1. 把数据从 神经网络隐藏层 神经网络隐藏层数_感知机_05 映射到 神经网络隐藏层 神经网络隐藏层数_神经网络隐藏层_10
  2. 从0往两边,变化速度很快,然后平稳;
  3. 神经网络隐藏层 神经网络隐藏层数_神经网络_11
  4. 神经网络隐藏层 神经网络隐藏层数_神经网络隐藏层_12

  5. 图2.3 tanh函数

技术2.3 多层反馈

一般将神经网络称为BP神经网络,BP就是Backpropagation。从输出层可以向输入层反馈,逐步调整每层的权重。越靠近输入层,梯度越小,权重改变越小,因此一般5层就够了。
权值更新的具体步骤如下:
Step 1. 根据激活函数调整loss. 使用 神经网络隐藏层 神经网络隐藏层数_感知机_13 损失时, 令输出层的节点值为 神经网络隐藏层 神经网络隐藏层数_神经网络隐藏层_14, 标准的输出为 神经网络隐藏层 神经网络隐藏层数_激活函数_15之间, 激活函数为 Sigmoid. 则输出节点的误差为:
神经网络隐藏层 神经网络隐藏层数_激活函数_16
Step 2. 将这些 loss 反向传递, 由前一层的各条边承担. 令 神经网络隐藏层 神经网络隐藏层数_神经网络隐藏层_17 表示第 神经网络隐藏层 神经网络隐藏层数_神经网络_18层的节点数, 神经网络隐藏层 神经网络隐藏层数_神经网络_19表示第 神经网络隐藏层 神经网络隐藏层数_激活函数_20 层的第 神经网络隐藏层 神经网络隐藏层数_神经网络_18 个节点的当前数据, 神经网络隐藏层 神经网络隐藏层数_感知机_22 表示第 神经网络隐藏层 神经网络隐藏层数_激活函数_20 层的第 神经网络隐藏层 神经网络隐藏层数_神经网络_18 个节点的当前误差, 神经网络隐藏层 神经网络隐藏层数_感知机_25 表示第 神经网络隐藏层 神经网络隐藏层数_激活函数_20 层第 神经网络隐藏层 神经网络隐藏层数_神经网络_18 个节点与第 神经网络隐藏层 神经网络隐藏层数_神经网络隐藏层_28 层第 神经网络隐藏层 神经网络隐藏层数_神经网络_29 个节点之间边的权重. 则边的权重更新为
神经网络隐藏层 神经网络隐藏层数_神经网络_30
其中 神经网络隐藏层 神经网络隐藏层数_激活函数_31 为学习速度. 如果还要考虑动量调整, 式(4) 可以加一个分量.
节点的误差更新为
神经网络隐藏层 神经网络隐藏层数_感知机_32
其中,前半部分与激活函数求导有关, 与式(3)同理; 后半部分则与下层各节点上的损失, 以及边的权重有关.

技术2.4 输出层表示

神经网络的输出层是多样的,既可是一个节点,也可以是多个。这使得它可以应对不同的任务。例如,回归时只需要一个节点;分类(神经网络隐藏层 神经网络隐藏层数_神经网络_33 个类别)需要 神经网络隐藏层 神经网络隐藏层数_神经网络_33 个节点,每个节点表示一个 神经网络隐藏层 神经网络隐藏层数_感知机_35 区间的实数值,哪个节点的值大,就预测为相应类别;多标签学习(神经网络隐藏层 神经网络隐藏层数_激活函数_20 个标签)需要 神经网络隐藏层 神经网络隐藏层数_激活函数_20

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

反向传播的关键点包括:

  1. sigmoid 函数的求导. 如果激活函数换了, 相应代码进行变化即可.
  2. 加权和函数的求导. 估计一般的网络不需要变. 后面的 CNN 之类再说吧.
  3. 动量系数. 这个已经属于高阶功能了.
    感慨一下:基础代码又简单又有效。

4. 小结

  1. 每层都有针对偏移量的 神经网络隐藏层 神经网络隐藏层数_感知机_38,参见技术1.2.
  2. 在实际应用中,神经网络的层数、每层的节点个数、每个节点使用的激活函数,都对其性能产生重要影响。所谓的“调参师”,就是干这些活儿。
  3. 在本文所述的几个技术上进行改动,都可以做出新的工作。