第6章 与学习相关的技巧(鱼书)
本章将介绍神经网络的学习中的一些重要观点,主题涉及寻找最优权重 参数的最优化方法、权重参数的初始值、超参数的设定方法等。
此外,为了 应对过拟合,本章还将介绍权值衰减、
Dropout
等正则化方法,并进行实现。
最后将对近年来众多研究中使用的
Batch Normalization
方法进行简单的介绍。
使用本章介绍的方法,可以高效地进行神经网络(深度学习)的学习,提高 识别精度。
6.1 参数的更新
神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻 找最优参数的问题,解决这个问题的过程称为
最优化(optimization)。遗憾的是, 神经网络的最优化问题非常难。这是因为参数空间非常复杂,无法轻易找到 最优解(无法使用那种通过解数学式一下子就求得最小值的方法)。而且,在 深度神经网络中,参数的数量非常庞大,导致最优化问题更加复杂。
在前几章中,为了找到最优参数,我们将参数的梯度(导数)作为了线索。 使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠 近最优参数,这个过程称为
随机梯度下降法(stochastic gradient descent), 简称SGD
。
SGD是一个简单的方法,不过比起胡乱地搜索参数空间,也算是“聪明”的方法。但是,根据不同的问题,也存在比
SGD更加聪明的方法。本节我们将指出
SGD
的缺点,并介绍
SGD
以外的其他最优化方法。
6.1.1 探险家的故事
有一个性情古怪的探险家。他在广袤的干旱地带旅行,坚持寻找幽 深的山谷。他的目标是要到达最深的谷底(他称之为“至深之地”)。这 也是他旅行的目的。并且,他给自己制定了两个严格的“规定”:一个 是不看地图;另一个是把眼睛蒙上。因此,他并不知道最深的谷底在这 个广袤的大地的何处,而且什么也看不见。在这么严苛的条件下,这位 探险家如何前往“至深之地”呢?他要如何迈步,才能迅速找到“至深 之地”呢?
寻找最优参数时,我们所处的状况和这位探险家一样,是一个漆黑的世界。我们必须在没有地图、不能睁眼的情况下,在广袤、复杂的地形中寻找 “至深之地”。大家可以想象这是一个多么难的问题。
在这么困难的状况下,地面的坡度显得尤为重要。探险家虽然看不到周 围的情况,但是能够知道当前所在位置的坡度(通过脚底感受地面的倾斜状况)。 于是,朝着当前所在位置的坡度最大的方向前进,就是
SGD的策略。勇敢 的探险家心里可能想着只要重复这一策略,总有一天可以到达“至深之地”。
6.1.2 SGD
让大家感受了最优化问题的难度之后,我们再来复习一下
SGD。用数学式可以将
SGD
写成如下的式(
6
.
1
)。

这里把需要更新的权重参数记为W,把损失函数关于W的梯度记为 。
η表示学习率,实际上会取0.01或0.001这些事先决定好的值。式子中的←表示用右边的值更新左边的值。如式(6.1)所示,SGD是朝着梯度方向只前进一定距离的简单方法。现在,我们将SGD实现为一个Python类(为方便 后面使用,我们将其实现为一个名为SGD的类)。
class SGD:
"""随机梯度下降法(Stochastic Gradient Descent)"""
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
这里,进行初始化时的参数
lr表示learning rate(学习率)
。这个学习率 会保存为实例变量。此外,代码段中还定义了
update(params, grads)方法
, 这个方法在
SGD
中会被反复调用。参数
params
和
grads(与之前的神经网络 的实现一样)是字典型变量,按
params['W1']
、
grads['W1']的形式,分别保 存了权重参数和它们的梯度。
使用这个
SGD类,可以按如下方式进行神经网络的参数的更新(下面的代码是不能实际运行的伪代码)。

这里首次出现的变量名
optimizer表示“优化器”的意思
,这里 由
SGD
承担这个角色。参数的更新由
optimizer负责完成。我们在这里需要 做的只是将参数和梯度的信息传给
optimizer。 像这样,通过单独实现进行最优化的类,功能的模块化变得更简单。
比如,后面我们马上会实现另一个最优化方法
Momentum,它同样会实现 成拥有
update(params, grads)这个共同方法的形式。这样一来,只需要将
optimizer = SGD()
这一语句换成
optimizer = Momentum()
,就可以从
SGD
切
换为
Momentum
。
很多深度学习框架都实现了各种最优化方法,并且提供了可以简单切换这些方法的构造。比如 Lasagne深度学习框架,在updates.py 这个文件中以函数的形式集中实现了最优化方法。用户可以从中选 择自己想用的最优化方法。
6.1.3 SGD的缺点
虽然
SGD简单,并且容易实现,但是在解决某些问题时可能
没有效率
。 这里,在指出
SGD的缺点之际,我们来思考一下求下面这个函数的最小值 的问题。

如图
6-1
所示,式(
6
.
2
)表示的函数是向
x轴方向延伸的“碗”状函数。 实际上,式(
6
.
2
)的等高线呈向
x
轴方向延伸的椭圆状。

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# 生成数据
x = np.linspace(-50, 50, 200) # 扩大范围以更好展示形状
y = np.linspace(-50, 50, 200)
X, Y = np.meshgrid(x, y)
Z = (1/20) * X**2 + Y**2
# 创建三维曲面图
fig = plt.figure(figsize=(14, 6))
# 三维曲面子图
ax1 = fig.add_subplot(121, projection='3d')
surf = ax1.plot_surface(X, Y, Z, cmap='viridis', alpha=0.8, rstride=1, cstride=1)
ax1.set_xlabel('X')
ax1.set_ylabel('Y')
ax1.set_zlabel('Z')
ax1.set_title('3D Surface Plot')
ax1.view_init(elev=30, azim=45) # 调整视角
# 添加颜色条
fig.colorbar(surf, ax=ax1, shrink=0.5, aspect=10)
# 二维等高线子图
ax2 = fig.add_subplot(122)
contour = ax2.contour(X, Y, Z, levels=20, cmap='viridis')
ax2.clabel(contour, inline=True, fontsize=8) # 添加等高线标签
ax2.set_xlabel('X')
ax2.set_ylabel('Y')
ax2.set_title('Contour Plot')
fig.colorbar(contour, ax=ax2, shrink=0.5, aspect=10)
plt.tight_layout()
plt.show()
现在看一下式(
6
.
2)表示的函数的梯度。如果用图表示梯度的话,则如 图
6-2
所示。这个梯度的特征是,
y
轴方向上大,
x轴方向上小。换句话说, 就是
y
轴方向的坡度大,而
x轴方向的坡度小。这里需要注意的是,虽然式 (
6
.
2
)的最小值在
(
x, y
) = (0
,
0)
处,但是图
6-2中的梯度在很多地方并没有指 向
(0
,
0)
。

import numpy as np
import matplotlib.pyplot as plt
# 生成数据
x = np.linspace(-10, 10, 20) # 缩小范围以便清晰显示梯度
y = np.linspace(-10, 10, 20)
X, Y = np.meshgrid(x, y)
# 计算函数值和梯度
Z = (1/20) * X**2 + Y**2
grad_x = X / 10 # ∂f/∂x = x/10
grad_y = 2 * Y # ∂f/∂y = 2y
# 创建图形
plt.figure(figsize=(10, 6))
# 绘制梯度场(箭头图)
plt.quiver(X, Y, grad_x, grad_y,
scale=20, # 控制箭头长度
scale_units='width',
color='r', # 箭头颜色
edgecolor='k', # 箭头边缘颜色
linewidth=0.5)
# 叠加等高线
contour = plt.contour(X, Y, Z, levels=10, colors='b', linestyles='dashed')
plt.clabel(contour, inline=True, fontsize=8) # 添加等高线标签
# 图形设置
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Gradient Field of f(x,y) = (1/20)x² + y²')
plt.grid(True, linestyle='--', alpha=0.7)
plt.axis('equal') # 保持坐标轴比例一致
plt.show()

我们来尝试对图
6-1
这种形状的函数应用
SGD
。从
(x, y) = (−7.0, 2.0)处
(初始值)开始搜索,结果如图
6-3
所示。
在图
6-3
中,
SGD呈“之”字形移动。这是一个相当低效的路径。也就是说, SGD
的缺点是,如果函数的形状非均向(
anisotropic),比如呈延伸状,搜索 的路径就会非常低效。因此,我们需要比单纯朝梯度方向前进的
SGD更聪 明的方法。
SGD
低效的根本原因是,梯度的方向并没有指向最小值的方向。

# 导入数值计算库
import numpy as np
# 导入绘图库
import matplotlib.pyplot as plt
# 导入3D绘图工具包
from mpl_toolkits.mplot3d import Axes3D
# 定义目标函数 Z = (1/20)X² + Y²
def f(x, y):
return (x**2)/20 + y**2
# 定义目标函数的梯度(导数)
def grad_f(x, y):
# X方向的梯度分量:∂Z/∂X = X/10
grad_x = x / 10
# Y方向的梯度分量:∂Z/∂Y = 2Y
grad_y = 2 * y
return np.array([grad_x, grad_y]) # 返回梯度向量
# 初始化随机梯度下降参数
start_x, start_y = -7.0, 2.0 # 起始点坐标
learning_rate = 0.1 # 学习率(步长)
iterations = 100 # 最大迭代次数
# 初始化存储路径的列表
x_vals, y_vals = [start_x], [start_y]
current_x, current_y = start_x, start_y # 当前位置初始化为起点
# 执行随机梯度下降算法
for _ in range(iterations):
grad = grad_f(current_x, current_y) # 计算当前点的梯度
# 更新参数:向梯度反方向移动(学习率 × 梯度)
current_x -= learning_rate * grad[0]
current_y -= learning_rate * grad[1]
# 记录新的位置到路径列表
x_vals.append(current_x)
y_vals.append(current_y)
# 创建画布和子图布局
fig = plt.figure(figsize=(12, 5))
# ==== 三维曲面图 ====
# 创建第一个子图(3D投影)
ax1 = fig.add_subplot(121, projection='3d')
# 生成网格数据用于绘制曲面
X = np.linspace(-8, 8, 100) # X轴范围-8到8,生成100个点
Y = np.linspace(-8, 8, 100) # Y轴范围同上
# 生成二维网格坐标矩阵
X, Y = np.meshgrid(X, Y)
# 计算对应的Z值
Z = (X**2)/20 + Y**2
# 绘制3D曲面,使用渐变色填充
ax1.plot_surface(X, Y, Z, cmap='viridis', alpha=0.7)
# 绘制优化路径:红色折线带圆点
ax1.plot(x_vals, y_vals,
f(np.array(x_vals), np.array(y_vals)), # Z轴坐标根据函数计算
color='red', marker='o', markersize=3, linewidth=1)
# 设置坐标轴标签
ax1.set_xlabel('X'), ax1.set_ylabel('Y'), ax1.set_zlabel('Z')
# 设置标题和标题位置
ax1.set_title('SGD Path on 3D Surface', y=0.9)
# ==== 二维等高线图 ====
# 创建第二个子图(普通二维坐标)
ax2 = fig.add_subplot(122)
# 生成等高线数据:20条对数间隔的等高线
contour = ax2.contour(X, Y, Z,
levels=np.logspace(-2, 3, 20), # 等高线层级范围
cmap='viridis') # 颜色映射
# 添加等高线数值标签
ax2.clabel(contour, inline=True, fontsize=8)
# 绘制优化路径:红色带圆点折线
ax2.plot(x_vals, y_vals, 'r-o', label='SGD Path')
# 标记起点(蓝色实心圆)
ax2.scatter(start_x, start_y,
color='blue', s=50,
label='Start')
# 标记最小值点(绿色实心圆)
ax2.scatter(0, 0,
color='green', s=50,
label='Minimum')
# 设置坐标轴标签
ax2.set_xlabel('X'), ax2.set_ylabel('Y')
# 设置标题和网格
ax2.set_title('SGD Path on Contour Plot')
ax2.legend() # 显示图例
ax2.grid(True) # 显示网格线
# 调整子图间距并显示图形
plt.tight_layout()
plt.show()

6.1.4 Momentum
Momentum是“动量”的意思,和物理有关。用数学式表示Momentum方法,如下所示。
和前面的SGD一样,W表示要更新的权重参数, ∂L/∂W表示损失函数关于W的梯度,η表示学习率。这里新出现了一个变量v,对应物理上的速度。
式(6.3)表示了物体在梯度方向上受力,在这个力的作用下,物体的速度增加这一物理法则。如图6-4所示,Momentum方法给人的感觉就像是小球在地面上滚动。


实例变量v会保存物体的速度。初始化时,v中什么都不保存,但当第一次调用update()时,v会以字典型变量的形式保存与参数结构相同的数据。剩余的代码部分就是将式(6.3)、式(6.4)写出来,很简单。 现在尝试使用Momentum解决式(6.2)的最优化问题,如图6-5所示。

图6-5中,更新路径就像小球在碗中滚动一样。和SGD相比,我们发现“
之”字形的“程度”减轻了。这是因为虽然x轴方向上受到的力非常小,但是一直在同一方向上受力,所以朝同一个方向会有一定的加速。反过来,虽然y轴方向上受到的力很大,但是因为交互地受到正方向和反方向的力,它们会互相抵消,所以y轴方向上的速度不稳定。因此,和SGD时的情形相比,可以更快地朝x轴方向靠近,减弱“之”字形的变动程度。
6.1.5 AdaGrad
在神经网络的学习中,学习率(数学式中记为η)的值很重要。学习率过小,会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能正确进行。
在关于学习率的有效技巧中,有一种被称为
学习率衰减(learning rate decay)的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多”学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低。而AdaGrad 进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。
AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习(AdaGrad的Ada来自英文单词Adaptive,即“适当的”的意思)。下面,让我们用数学式表示AdaGrad的更新方法。

和前面的SGD一样,W表示要更新的权重参数,∂L/∂W 表示损失函数关于W的梯度,η表示学习率。这里新出现了
变量h,如式(6.5)所示,它保存了以前的所有梯度值的平方和(式(6.5)中的⨀表示对应矩阵元素的乘法)。然后,在更新参数时,通过乘以1/
,就可以调整学习的尺度。这意味着,参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。
AdaGrad会记录过去所有梯度的平方和。因此,学习越深入,更新的幅度就越小。实际上,如果无止境地学习,更新量就会变为 0,完全不再更新。为了改善这个问题,可以使用
RMSProp方法。RMSProp方法并不是将过去所有的梯度一视同仁地相加,而是逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。这种操作从专业上讲,称为“
指数移动平均”,呈指数函数式地减小过去的梯度的尺度。
现在来实现 AdaGrad。AdaGrad 的 实 现 过 程 如 下 所 示(源 代 码 在common/optimizer.py中)。

这里需要注意的是,最后一行加上了微小值
1e-7。这是为了防止当 self.h[key]
中有
0
时,将
0用作除数的情况。在很多深度学习的框架中,这 个微小值也可以设定为参数,但这里我们用的是
1e-7
这个固定值。
现在,让我们试着使用
AdaGrad
解决式(
6
.
2
)的最优化问题,结果如图6-6 所示。


6.1.6 Adam
Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐。如果将这两个方法融合在一起会怎么样呢?这就是Adam方法的基本思路 。
Adam是2015年提出的新方法。它的理论有些复杂,直观地讲,就是融合了Momentum和AdaGrad的方法。通过组合前面两个方法的优点,有望实现参数空间的高效搜索。
此外,进行超参数的“
偏置校正”也是Adam的特征。这里不再进行过多的说明,详细内容请参考原作者的论文[8]。关于Python的实现,common/optimizer.py中将其实现为了Adam类,有兴趣的读者可以参考。现在,我们试着使用Adam解决式(6.2)的最优化问题,结果如图6-7所示。

在图6-7中,基于Adam的更新过程就像小球在碗中滚动一样。虽然Momentun也有类似的移动,但是相比之下,Adam的小球左右摇晃的程度有所减轻。这得益于学习的更新程度被适当地调整了。
Adam会设置 3个超参数。一个是学习率(论文中以α出现),另外两个是一次momentum系数β1和二次momentum系数β2。根据论文,标准的设定值是β1为 0.9,β2 为 0.999。设置了这些值后,大多数情况下都能顺利运行。
6.1.7 使用哪种更新方法呢
到目前为止,我们已经学习了
4种更新参数的方法。这里我们来比较一 下这
4
种方法(源代码在
ch06/optimizer_compare_naive.py
中)。
如图
6-8所示,根据使用的方法不同,参数更新的路径也不同。只看这 个图的话,
AdaGrad似乎是最好的,不过也要注意,结果会根据要解决的问 题而变。并且,很显然,超参数(学习率等)的设定值不同,结果也会发生变化。
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from common.optimizer import *
def f(x, y):
return x**2 / 20.0 + y**2
def df(x, y):
return x / 10.0, 2.0*y
init_pos = (-7.0, 2.0)
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0
optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95)
optimizers["Momentum"] = Momentum(lr=0.1)
optimizers["AdaGrad"] = AdaGrad(lr=1.5)
optimizers["Adam"] = Adam(lr=0.3)
idx = 1
for key in optimizers:
optimizer = optimizers[key]
x_history = []
y_history = []
params['x'], params['y'] = init_pos[0], init_pos[1]
for i in range(30):
x_history.append(params['x'])
y_history.append(params['y'])
grads['x'], grads['y'] = df(params['x'], params['y'])
optimizer.update(params, grads)
x = np.arange(-10, 10, 0.01)
y = np.arange(-5, 5, 0.01)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)
# for simple contour line
mask = Z > 7
Z[mask] = 0
# plot
plt.subplot(2, 2, idx)
idx += 1
plt.plot(x_history, y_history, 'o-', color="red")
plt.contour(X, Y, Z)
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.plot(0, 0, '+')
#colorbar()
#spring()
plt.title(key)
plt.xlabel("x")
plt.ylabel("y")
plt.show()
上面我们介绍了
SGD
、
Momentum
、
AdaGrad
、
Adam
这4种方法,那 么用哪种方法好呢?非常遗憾,(目前)并不存在能在所有问题中都表现良好 的方法。这
4种方法各有各的特点,都有各自擅长解决的问题和不擅长解决 的问题。
很多研究中至今仍在使用
SGD
。
Momentum
和
AdaGrad也是值得一试 的方法。最近,很多研究人员和技术人员都喜欢用
Adam。本书将主要使用 SGD
或者
Adam
,读者可以根据自己的喜好多多尝试。
6.1.8 基于MNIST数据集的更新方法的比较
我 们 以 手 写 数 字 识 别 为 例,比 较 前 面 介 绍 的
SGD
、
Momentum、 AdaGrad
、
Adam
这
4种方法,并确认不同的方法在学习进展上有多大程度 的差异。先来看一下结果,如图
6-9
所示(源代码在ch06/optimizer_compare_ mnist.py
中)。
# coding: utf-8
import os
import sys
sys.path.append(os.pardir) # 将父目录加入系统路径,用于导入自定义模块
# 导入必要的第三方库
import matplotlib.pyplot as plt # 绘图库
import numpy as np # 数值计算库
# 从自定义模块导入所需功能
from dataset.mnist import load_mnist # MNIST数据集加载函数(当前未使用)
from common.util import smooth_curve # 平滑曲线的工具函数
from common.multi_layer_net import MultiLayerNet # 多层神经网络类
from common.optimizer import * # 所有优化器类(SGD/Momentum/AdaGrad/Adam)
# ======================
# 主程序开始
# ======================
# 0: 加载MNIST数据集(改用TensorFlow Keras版本)
# 使用TensorFlow内置的MNIST数据集(无需手动下载)
(x_train, t_train), (x_test, t_test) = mnist.load_data()
# 数据预处理
# 将图像数据展平为向量(28x28 -> 784),并归一化到[0,1]范围
x_train = x_train.reshape(-1, 784).astype(np.float32)/255.0
x_test = x_test.reshape(-1, 784).astype(np.float32)/255.0
# 将标签转换为one-hot编码格式
t_train = np.eye(10)[t_train] # 创建10个类别的one-hot矩阵
t_test = np.eye(10)[t_test]
# 数据集基本信息
train_size = x_train.shape[0] # 训练样本总数(60000)
batch_size = 128 # 每次训练使用的样本数
max_iterations = 2000 # 最大训练轮次
# ======================
# 1. 实验配置初始化
# ======================
# 初始化优化器字典(包含四种优化算法)
optimizers = {
'SGD': SGD(), # 随机梯度下降法
'Momentum': Momentum(),# 带动量的梯度下降
'AdaGrad': AdaGrad(), # 自适应梯度算法
'Adam': Adam() # 自适应矩估计算法
}
# 初始化网络模型和损失记录器
networks = {} # 存储不同优化器对应的网络实例
train_loss = {} # 存储各优化器的训练损失历史
for key in optimizers.keys():
# 创建具有4层隐藏层(每层100节点)的全连接网络
networks[key] = MultiLayerNet(
input_size=784,
hidden_size_list=[100, 100, 100, 100],
output_size=10
)
# 为每个优化器创建独立的损失记录列表
train_loss[key] = []
# ======================
# 2. 训练过程
# ======================
for i in range(max_iterations):
# 随机采样一个mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask] # 选取输入样本
t_batch = t_train[batch_mask] # 选取对应标签
# 对每个优化器进行独立训练
for key in optimizers.keys():
# 计算梯度(通过反向传播)
grads = networks[key].gradient(x_batch, t_batch)
# 更新网络参数
optimizers[key].update(networks[key].params, grads)
# 计算并记录当前批次的损失值
loss = networks[key].loss(x_batch, t_batch)
train_loss[key].append(loss)
# 每100次迭代打印当前损失状态
if i % 100 == 0:
print("===========" + "iteration:" + str(i) + "===========")
for key in optimizers.keys():
# 获取当前优化器的最新损失值
loss = networks[key].loss(x_batch, t_batch)
print(f"{key}: {loss:.4f}") # 格式化输出损失值(保留4位小数)
# ======================
# 3. 结果可视化
# ======================
# 定义不同优化器的标记样式
markers = {
"SGD": "o", # 圆圈标记
"Momentum": "x", # 叉号标记
"AdaGrad": "s", # 方块标记
"Adam": "D" # 菱形标记
}
# 生成x轴坐标数据
x = np.arange(max_iterations)
# 绘制各优化器的损失曲线
for key in optimizers.keys():
# 对原始损失值进行平滑处理
smoothed_loss = smooth_curve(train_loss[key])
# 绘制曲线并设置标记样式
plt.plot(
x,
smoothed_loss,
marker=markers[key], # 设置标记形状
markevery=100, # 每隔100点显示标记
label=key # 图例标签
)
# 设置图表属性
plt.xlabel("iterations") # x轴标题
plt.ylabel("loss") # y轴标题
plt.ylim(0, 1) # 设置y轴范围
plt.legend() # 显示图例
plt.title("Optimizer Comparison") # 图表标题
plt.show() # 显示图表
===========iteration:1800===========
SGD:0.17020471001448634
Momentum:0.041975077112411635
AdaGrad:0.015143706148737824
Adam:0.011175943051347607
===========iteration:1900===========
SGD:0.2062572590920101
Momentum:0.05884984330174722
AdaGrad:0.03464677927561151
Adam:0.08108088815820025
这个实验以一个
5
层神经网络为对象,其中每层有
100个神经元。激活 函数使用的是
ReLU
。
从图
6-9
的结果中可知,与
SGD
相比,其他
3种方法学习得更快,而且 速度基本相同,仔细看的话,AdaGrad的学习进行得稍微快一点。这个实验 需要注意的地方是,实验结果会随学习率等超参数、神经网络的结构(几层 深等)的不同而发生变化。不过,一般而言,与
SGD
相比,其他
3种方法可以学习得更快,有时最终的识别精度也更高。
6.2 权重的初始值
在神经网络的学习中,权重的初始值特别重要。实际上,设定什么样的 权重初始值,经常关系到神经网络的学习能否成功。本节将介绍权重初始值 的推荐值,并通过实验确认神经网络的学习是否会快速进行。
6.2.1 可以将权重初始值设为0吗
后面我们会介绍抑制过拟合、提高泛化能力的技巧——
权值衰减(weight decay)
。简单地说,
权值衰减就是一种以减小权重参数的值为目的进行学习 的方法
。通过减小权重参数的值来抑制过拟合的发生。
如果想减小权重的值,一开始就将初始值设为较小的值才是正途。实际上, 在这之前的权重初始值都是像
0.01 * np.random.randn(10, 100)这样,使用 由高斯分布生成的值乘以
0
.
01
后得到的值(标准差为
0
.
01
的高斯分布)。
如果我们把权重初始值全部设为
0以减小权重的值,会怎么样呢?从结 论来说,
将权重初始值设为0不是一个好主意
。事实上,将权重初始值设为 0
的话,将无法正确进行学习。
为什么不能将权重初始值设为0呢?
严格地说,为什么不能将权重初始值设成一样的值呢?这是因为在误差反向传播法中,所有的权重值都会进行 相同的更新。比如,在
2
层神经网络中,假设第
1
层和第
2
层的权重为
0。这 样一来,正向传播时,因为输入层的权重为
0
,所以第
2层的神经元全部会 被传递相同的值。第
2层的神经元中全部输入相同的值,这意味着反向传播 时第
2层的权重全部都会进行相同的更新(回忆一下“乘法节点的反向传播”
的内容)。因此,权重被更新为相同的值,并拥有了对称的值(重复的值)。
这使得神经网络拥有许多不同的权重的意义丧失了。为了防止“权重均一化”
(严格地讲,是为了瓦解权重的对称结构),
必须随机生成初始值
。
6.2.2 隐藏层的激活值的分布
观察隐藏层的激活值 A (激活函数的输出数据)的分布,可以获得很多启 发。这里,我们来做一个简单的实验,观察权重初始值是如何影响隐藏层的 激活值的分布的。这里要做的实验是,
向一个5层神经网络(激活函数使用 sigmoid函数)传入随机生成的输入数据
,用直方图绘制各层激活值的数据分 布。这个实验参考了斯坦福大学的课程
CS231n
[5]
。
进行实验的源代码在
ch06/weight_init_activation_histogram.py中,下 面展示部分代码。
# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def ReLU(x):
return np.maximum(0, x)
def tanh(x):
return np.tanh(x)
input_data = np.random.randn(1000, 100) # 1000个数据
node_num = 100 # 各隐藏层的节点(神经元)数
hidden_layer_size = 5 # 隐藏层有5层
activations = {} # 激活值的结果保存在这里
x = input_data
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
# 改变初始值进行实验!
w = np.random.randn(node_num, node_num) * 1
# w = np.random.randn(node_num, node_num) * 0.01
# w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
# w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)
a = np.dot(x, w)
# 将激活函数的种类也改变,来进行实验!
z = sigmoid(a)
# z = ReLU(a)
# z = tanh(a)
activations[i] = z
# 绘制直方图
for i, a in activations.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
if i != 0: plt.yticks([], [])
# plt.xlim(0.1, 1)
# plt.ylim(0, 7000)
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
这里假设神经网络有
5
层,每层有
100个神经元。然后,用高斯分布随 机生成
1000
个数据作为输入数据,并把它们传给
5层神经网络。激活函数使 用
sigmoid
函数,各层的激活值的结果保存在
activations变量中。这个代码 段中需要注意的是权重的尺度。虽然这次我们使用的是标准差为1的高斯分 布,但实验的目的是通过改变这个尺度(标准差),观察激活值的分布如何变 化。现在,我们将保存在
activations
中的各层数据画成直方图。

从图
6-10
可知,各层的激活值呈偏向
0
和
1
的分布。这里使用的sigmoid 函数是
S
型函数,随着输出不断地靠近
0
(或者靠近
1),它的导数的值逐渐接 近
0
。因此,偏向
0
和
1的数据分布会造成反向传播中梯度的值不断变小,最 后消失。这个问题称为
梯度消失
(
gradient vanishing)。层次加深的深度学习 中,梯度消失的问题可能会更加严重。
下面,将权重的标准差设为
0
.
01,进行相同的实验。实验的代码只需要 把设定权重初始值的地方换成下面的代码即可。
# w = np.random.randn(node_num, node_num) * 1
w = np.random.randn(node_num, node_num) * 0.01
来看一下结果。使用标准差为
0
.
01的高斯分布时,各层的激活值的分布如图
6-11
所示。

这次呈
集中在0.5附近
的分布。因为不像刚才的例子那样偏向
0和1,所 以不会发生梯度消失的问题。但是,
激活值的分布有所偏向,说明在表现力上会有很大问题
。
为什么这么说呢?
因为如果有多个神经元都输出几乎相同 的值,那它们就没有存在的意义了
。比如,如果
100个神经元都输出几乎相 同的值,那么也可以由
1个神经元来表达基本相同的事情。因此,激活值在 分布上有所偏向会出现“
表现力受限
”的问题。
各层的激活值的分布都要求有适当的广度。为什么呢?因为通过在各层间传递多样性的数据,神经网络可以进行高效的学习。反过来,如果传递的是有所偏向的数据,就会出现梯度消失或者“表现力受限”的问题,导致学习可能无法顺利进行。
接着,我们尝试使用
Xavier Glorot
等人的论文
[9]中推荐的
权重初始值(俗称“Xavier初始值
”)。
现在,
在一般的深度学习框架中,Xavier初始值已被作为标准使用
。比如,
Caffe
框架中,通过在设定权重初始值时赋予
xavier参数, 就可以使用
Xavier
初始值。
Xavier的论文中,为了使各层的激活值呈现出具有相同广度的分布,推
导了合适的权重尺度。推导的结论是,如果前一层的节点数为
n
,则初始
值使用标准差为
的分布
(图
6-12
)。
