与学习相关的技巧
参数的更新
神经网络的学习的目的是找到使损失函数的值尽可能小的参数,这是寻找最优参数的问题,解决这个问题的过程称为最优化(optimization)
SGD(随机梯度下降法)
SGD的缺点,如果函数的形状非均向(anisotropic),比如呈延伸状,搜索的路径就会非常低效,低效的根本原因是,梯度的方向并没有指向最小值的方向,如下图
这会导致它的效率很低,基于SGD的最优化的更新路径如下图
可以看到,它是Z字型的移动
Momentum(带动量的随机梯度下降法)
它是动量的意思,和物理有关,数学表达式如下
$$
v \leftarrow \alpha \cdot v - \eta \cdot \frac {\partial L} {\partial W} \
W \leftarrow W + v
$$W同样还是表示要更新的权重参数,$\frac{\partial L}{\partial W}$ 表示损失函数关于W的梯度,$\eta$ 是学习率,而 $v$ 是物理上的速度
第一个式子表示,物体在梯度方向上受力,在这个力的作用下,物体的速度增加这一物理法则,$\alpha v$ 这一项,其中的 $\alpha$ 是一个衰减因子,其作用是在物体不受力的作用的情况下,使得物体速度减小,一般设置为0.9之类的值,它对应物理中的地面摩擦或者空气阻力等
基于Momentum的最优化的更新路径如下图
可以发现,和SGD相比,它的Z字型程度减轻了,这是因为,虽然x轴方向上受到的力非常小,但是一直在同一方向上受力,所以朝同一个方向会有一定的加速;另一方面,虽然y轴方向上受到的力很大,但是因为交互地受到正方向和反方向的力,它们会互相抵消,所以y轴方向上的速度不稳定
AdaGrad(自适应梯度算法)
学习率衰减(learning rate decay):随着学习的进行,使学习率逐渐减小。它是针对全体参数的,它是将全体参数的学习率一起降低
AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习,Ada是取自Adaptive,即适当的
其数学表达式如下
$$
h \leftarrow h + \frac {\partial L} {\partial W} \odot \frac {\partial L} {\partial W} \
\mathbf{W} \leftarrow \mathbf{W} - \eta \frac{1}{\sqrt{\mathbf{h}}} \frac{\partial L}{\partial \mathbf{W}}
$$需要说明的是,变量 $h$ ,它保存了以前的所有梯度值的平方和,$\odot$ 表示对应矩阵元素的乘法,也就是实现了所有梯度值的平方和
另外,在更新参数时,通过乘以 $\frac {1}{\sqrt {h}}$ ,就可以调整学习的尺度,这意味着,参数的元素中变动较大(被大幅更新)的元素的学习率将变小,也即,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小
AdaGrad会记录过去所有梯度的平方和,因此,学习越深入,更新的幅度就越小,如果无休止的学习,最终更新量会变为0
RMSProp方法,并不是将过去所有的梯度一视同仁地相加,而是逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来,这样的操作被称为指数移动平均,呈指数函数式地减小过去的梯度的尺度
基于AdaGrad的最优化的更新路径如下图
可以看到,函数的取值高效地向着最小值移动
Adam(自适应矩估计算法)
它融合了Momentum和AdaGrad,结合两者的优点,它还可以进行超参数的偏置矫正
基于Adam的最优化的更新路径如下图
Adam会设置3个超参数,一个是学习率 $\eta$ ,另外两个是一次momentum系数 $\beta_1$ 和二次momentum系数 $\beta_2$ ,且标准的设定值是 $\beta_1 = 0.9, \beta_2 = 0.999$
权重的初始值
权值衰减(weight decay),就是一种以减小权重参数的值为目的进行学习的方法,通过减小权重参数的值来抑制过拟合的发生,以及提高泛化能力
在误差反向传播法中,所有的权重值都会进行相同的更新,所以不能把权重初始值设为一样的值(权重均一化)
激活函数使用sigmoid函数,随着输出不断地靠近0(或者靠近1),它的导数的值逐渐接近0,从而偏向0和1的数据分布会造成反向传播中梯度的值不断变小,最后消失,这就是所谓的梯度消失(gradient vanishing)
激活值在分布上有所偏向会出现表现了受限或梯度消失的问题
Xavier初始值
为了使各层的激活值呈现出具有相同广度的分布,推导了合适的权重尺度,结论是:如果前一层的节点数为n,则初始值使用标准差为 $\frac{1}{\sqrt{n}}$ 的分布
可以将激活函数sigmoid改为tanh双曲线函数,两者同为S型曲线函数,但tanh是关于原点对称的S型曲线,而sigmoid是关于(0,0.5)对称的,而用作激活函数的函数最好具有关于原点对称的性质
ReLU的权值重置
Xavier初始值是以激活函数是线性函数为前提而推导出来的,sigmoid函数和tanh函数左右对称,且中央附近可以视作线性函数,所以适合使用Xavier初始值
当激活函数使用ReLU时,一般推荐使用ReLU专用的初始值——He初始值
它是,当前一层的节点数为n时,He初始值使用标准差为 $\sqrt{\frac {2}{n}}$ 的高斯分布
相对于Xavier初始值的 $\sqrt{\frac {1}{n}}$ ,因为ReLU的负值区域的值为0,为了使它更有广度,所以需要2倍的系数
当激活函数使用ReLU时,权重初始值使用He初始值,当激活函数为sigmoid或tanh等S型曲线函数时,初始值使用Xavier初始值
Batch Normalization
Batch Normalization的算法
为了使各层拥有适当的广度,“强制性”地调整激活值的分布,这就是Batch Normalization方法
它有以下几个特点
Batch normalization层:对神经网络中的数据分布进行正规化的层
它以进行学习时的mini-batch为单位,按mini-batch进行正规化,具体来说就是,进行使数据分布的均值为0、方差为1的正规化,数学式表示如下
$$
\mu_B \leftarrow \frac {1}{m} \sum_{i=1}^m x_i \
\sigma_B^2 \leftarrow \frac{1}{m} \sum_{i=1}^m(x_i - \mu_B)^2 \
\hat x_i \leftarrow \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \varepsilon}}
$$第1、2个式子是对mini-batch的m个输入数据的集合 $B={x_1,x_2…x_m}$ 求均值 $\mu_B$ 和方差 $\sigma_B^2$ ,第三个式子对输入数据进行均值为0、方差为1(合适的分布)的正规化,$\varepsilon$ 是一个极小值,作用是防止除0
通过将这个处理插入到激活函数的前面(或者后面),可以减小数据分布的偏向
紧接着,Batch Norm层会对正规化后的数据进行缩放和平移的变换,数学式表示如下
$$
y_i \leftarrow \gamma \hat x_i + \beta
$$其中, $\gamma$ 和 $\beta$ 是参数,初始分别为1和0,然后再通过学习调整到合适的值
它是神经网络上的正向传播,计算图如下
留个坑:batch norm的反向传播
Batch Normalization的评估
正则化
过拟合:只能拟合训练数据,但不能很好地拟合不包含在训练数据中的其他数据的状态
发生过拟合的原因,一个是模型拥有大量参数、表现力强,一个是训练数据少
权值衰减
权值衰减是一直以来经常被使用的一种抑制过拟合的方法,该方法通过在学习的过程中对大的权重进行惩罚,来抑制过拟合
如,为损失函数加上权重的平方范数(L2范数),这样就可以抑制权重变大,将权重记为W,L2的范数的权值衰减就是 $\frac{1}{2} \lambda W^2$ ,然后将它加到损失函数上,$\lambda$ 是控制正则化强度的超参数,$\lambda$ 设置得越大,对大的权重施加的惩罚就越重,1/2是用于将 $\frac{1}{2} \lambda W^2$ 的求导结果变成 $\lambda W$ 的调整常用量
对于所有权重,权值衰减方法都会为损失函数加上 $\frac{1}{2} \lambda W^2$ ,因此,在求权重梯度的计算中,要为之前的误差反向传播法的结果加上正则化项的导数 $\lambda W$
L2范数相当于各元素的平方和,假设有权重 $W=(w_1,w_2…w_n)$ ,则L2范数可用 $\sqrt{w_1^2+w_2^2+…w_n^2}$ 计算出来
除了L2范数,还有L1范数、$L \infty$ 范数等,L1范数是各元素的绝对值之和,相当于 $|w_1|+|w_2|+…|w_n|$ ,$L\infty$ 范数也称为Max范数,相当于各个元素的绝对值中最大的那一个
以上三种范数都可以用作正则项
Dropout
前面的为损失函数加上权重的L2范数的权值衰减方法,在网络模型变得复杂的情况下,就难以应对了
由此,引出了Dropout这种方法
Dropout是一种在学习的过程中随机删除神经元的方法
训练时,随机选出隐藏层的神经元,然后将其删除,被删除的神经元不再进行信号的传递
训练时,每传递一次数据,就会随机选择要删除的神经元
测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出
集成学习,就是让多个模型单独进行学习,推理时再取多个模型的输出的平均值,通过进行集成学习,神经网络的识别精度可以提高好几个百分点,Dropout将集成学习的效果(模拟地)通过一个网络实现了
超参数的验证
超参数是指各层的神经元数量、batch大小、参数更新时的学习率或权值衰减等
不能使用测试数据评估超参数的性能,因为如果使用测试数据调整超参数,超参数的值会对测试数据发生过拟合
用于调整超参数的数据,一般称为验证数据
训练数据用于参数(权重和偏置)的学习,验证数据用于超参数的性能评估,为了确认泛化能力,要在最后使用测试数据
超参数的最优化
进行超参数的最优化时,要逐渐缩小超参数的“好值”的存在范围
所谓逐渐缩小范围,是指一开始先大致设定一个范围,从这个范围中随机选出一个超参数(采样,最好是随机采样),用这个采样到的值进行识别精度的评估,重复该操作,观察识别精度的结果,根据这个结果缩小超参数的“好值”的范围,最终确定超参数的合适范围(一般是 0.001~1000)
还可以使用贝叶斯最优化(Bayesian optimization)
code
SGD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 class SGD:
"""
随机梯度下降法(Stochastic Gradient Descent)
最基础的优化算法,直接使用梯度更新参数
"""
def __init__(self, lr=0.01):
"""
初始化SGD优化器
参数:
lr: 学习率,控制参数更新的步长
"""
self.lr = lr
def update(self, params, grads):
"""
更新参数
参数:
params: 需要更新的参数字典
grads: 对应的梯度字典
"""
for key in params.keys():
params[key] -= self.lr * grads[key] # 参数更新公式:θ = θ - lr * ∇θMomentum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 class Momentum:
"""
带动量的随机梯度下降法
通过引入动量项来加速收敛,减少震荡
"""
def __init__(self, lr=0.01, momentum=0.9):
"""
初始化Momentum优化器
参数:
lr: 学习率
momentum: 动量系数,通常设为0.9
"""
self.lr = lr
self.momentum = momentum
self.v = None # 速度向量
def update(self, params, grads):
"""
更新参数
参数:
params: 需要更新的参数字典
grads: 对应的梯度字典
"""
if self.v is None:
# 第一次调用时初始化速度向量
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
# 更新速度:v = momentum * v - lr * grad
self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
# 更新参数:θ = θ + v
params[key] += self.v[key]AdaGrad
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 class AdaGrad:
"""
AdaGrad(自适应梯度算法)
为每个参数自适应地调整学习率,适合处理稀疏梯度
"""
def __init__(self, lr=0.01):
"""
初始化AdaGrad优化器
参数:
lr: 初始学习率
"""
self.lr = lr
self.h = None # 累积梯度平方和
def update(self, params, grads):
"""
更新参数
参数:
params: 需要更新的参数字典
grads: 对应的梯度字典
"""
if self.h is None:
# 第一次调用时初始化累积梯度平方和
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
# 累积梯度平方和:h = h + grad^2
self.h[key] += grads[key] * grads[key]
# 参数更新:θ = θ - lr * grad / sqrt(h + ε)
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)Adam
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 class Adam:
"""
Adam(自适应矩估计算法)
结合了Momentum和RMSprop的优点,是目前最常用的优化算法之一
"""
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
"""
初始化Adam优化器
参数:
lr: 学习率
beta1: 一阶矩估计的指数衰减率
beta2: 二阶矩估计的指数衰减率
"""
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0 # 迭代次数
self.m = None # 一阶矩估计(梯度的指数移动平均)
self.v = None # 二阶矩估计(梯度平方的指数移动平均)
def update(self, params, grads):
"""
更新参数
参数:
params: 需要更新的参数字典
grads: 对应的梯度字典
"""
if self.m is None:
# 第一次调用时初始化
self.m, self.v = {}, {}
for key, val in params.items():
self.m[key] = np.zeros_like(val)
self.v[key] = np.zeros_like(val)
self.iter += 1
# 计算学习率的偏差修正
lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)
for key in params.keys():
# 更新一阶矩估计:m = beta1 * m + (1 - beta1) * grad
#self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
# 更新二阶矩估计:v = beta2 * v + (1 - beta2) * grad^2
#self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
# 偏差修正的更新方式
self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
# 参数更新:θ = θ - lr_t * m / sqrt(v + ε)
params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
# 另一种偏差修正的实现方式(注释掉)
#unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
#unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
#params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)几种更新方法的比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99 import sys, os
sys.path.append(os.pardir) # 添加父目录到路径,以便导入common模块
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from common.optimizer import * # 导入所有优化器类
def f(x, y):
"""
定义目标函数:f(x,y) = x^2/20 + y^2
这是一个简单的二次函数,用于测试优化算法的性能
参数:
x, y: 输入变量
返回:
函数值
"""
return x**2 / 20.0 + y**2
def df(x, y):
"""
计算目标函数的梯度
∂f/∂x = x/10, ∂f/∂y = 2y
参数:
x, y: 输入变量
返回:
(∂f/∂x, ∂f/∂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 # 梯度初始化为0
# 创建优化器字典,使用OrderedDict保持顺序
optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95) # 随机梯度下降,学习率0.95
optimizers["Momentum"] = Momentum(lr=0.1) # 带动量的SGD,学习率0.1
optimizers["AdaGrad"] = AdaGrad(lr=1.5) # 自适应梯度算法,学习率1.5
optimizers["Adam"] = Adam(lr=0.3) # Adam优化器,学习率0.3
idx = 1 # 子图索引
# 遍历每种优化器
for key in optimizers:
optimizer = optimizers[key] # 获取当前优化器
x_history = [] # 记录x坐标的历史轨迹
y_history = [] # 记录y坐标的历史轨迹
params['x'], params['y'] = init_pos[0], init_pos[1] # 重置参数到初始位置
# 进行30次优化迭代
for i in range(30):
x_history.append(params['x']) # 记录当前x位置
y_history.append(params['y']) # 记录当前y位置
# 计算当前点的梯度
grads['x'], grads['y'] = df(params['x'], params['y'])
# 使用优化器更新参数
optimizer.update(params, grads)
# 创建网格用于绘制等高线图
x = np.arange(-10, 10, 0.01) # x轴范围:-10到10,步长0.01
y = np.arange(-5, 5, 0.01) # y轴范围:-5到5,步长0.01
X, Y = np.meshgrid(x, y) # 创建网格坐标
Z = f(X, Y) # 计算网格上每个点的函数值
# 为了简化等高线图,将大于7的值设为0
mask = Z > 7
Z[mask] = 0
# 绘制子图
plt.subplot(2, 2, idx) # 创建2x2的子图,当前是第idx个
idx += 1 # 子图索引递增
# 绘制优化轨迹
plt.plot(x_history, y_history, 'o-', color="red") # 红色圆点连线表示优化路径
plt.contour(X, Y, Z) # 绘制等高线
plt.ylim(-10, 10) # 设置y轴范围
plt.xlim(-10, 10) # 设置x轴范围
plt.plot(0, 0, '+') # 在原点(0,0)绘制加号,表示全局最优点
#colorbar() # 注释掉的颜色条
#spring() # 注释掉的弹簧布局
plt.title(key) # 设置子图标题为优化器名称
plt.xlabel("x") # 设置x轴标签
plt.ylabel("y") # 设置y轴标签
plt.show() # 显示所有子图基于MNIST数据集的更新方法的比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 import os
import sys
sys.path.append(os.pardir) # 添加父目录到路径,以便导入其他模块
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist # 导入MNIST数据集加载函数
from common.util import smooth_curve # 导入平滑曲线函数,用于可视化
from common.multi_layer_net import MultiLayerNet # 导入多层神经网络类
from common.optimizer import * # 导入所有优化器类
# 0: 加载MNIST数据集 ==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True) # 加载并归一化MNIST数据集
train_size = x_train.shape[0] # 训练集大小
batch_size = 128 # 批量大小
max_iterations = 2000 # 最大迭代次数
# 1: 实验设置 ==========
# 创建不同优化器的字典
optimizers = {}
optimizers['SGD'] = SGD() # 随机梯度下降
optimizers['Momentum'] = Momentum() # 带动量的SGD
optimizers['AdaGrad'] = AdaGrad() # 自适应梯度算法
optimizers['Adam'] = Adam() # Adam优化器
#optimizers['RMSprop'] = RMSprop() # RMSprop优化器(已注释)
# 为每个优化器创建对应的神经网络和损失记录
networks = {} # 存储不同优化器对应的神经网络
train_loss = {} # 存储不同优化器的训练损失
for key in optimizers.keys():
# 创建具有4个隐藏层(每层100个神经元)的多层神经网络
networks[key] = MultiLayerNet(
input_size=784, # 输入层大小(28x28=784)
hidden_size_list=[100, 100, 100, 100], # 4个隐藏层,每层100个神经元
output_size=10) # 输出层大小(10个类别)
train_loss[key] = [] # 初始化损失列表
# 2: 开始训练 ==========
for i in range(max_iterations):
# 随机选择批量数据
batch_mask = np.random.choice(train_size, batch_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(key + ":" + str(loss)) # 打印每个优化器的损失
# 3: 绘制图表 ==========
# 设置不同优化器的标记样式
markers = {"SGD": "o", "Momentum": "x", "AdaGrad": "s", "Adam": "D"}
x = np.arange(max_iterations) # 创建x轴数据(迭代次数)
# 绘制每个优化器的损失曲线
for key in optimizers.keys():
# 使用平滑曲线绘制损失,每100次迭代标记一次
plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
# 设置图表属性
plt.xlabel("iterations") # x轴标签:迭代次数
plt.ylabel("loss") # y轴标签:损失值
plt.ylim(0, 1) # 设置y轴范围:0到1
plt.legend() # 显示图例
plt.show() # 显示图表隐藏层的激活值的分布(设计两种初始值Xavier和He初始值(作为权重的初始值),分别适用于不同的激活函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 # coding: utf-8
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
"""
Sigmoid激活函数
将输入值压缩到(0,1)区间
"""
return 1 / (1 + np.exp(-x))
def ReLU(x):
"""
ReLU激活函数
当输入大于0时返回原值,小于0时返回0
"""
return np.maximum(0, x)
def tanh(x):
"""
tanh激活函数
将输入值压缩到(-1,1)区间
"""
return np.tanh(x)
# 生成1000个样本,每个样本100个特征,x 就是高斯分布随机数
input_data = np.random.randn(1000, 100) # 且符合标准正态分布,也就是高斯分布
node_num = 100
hidden_layer_size = 5
activations = {}
x = input_data
# 模拟5层神经网络的前向传播过程
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 # 缩小100倍
# w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num) # Xavier初始化
# w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num) # He初始化
# 线性变换
a = np.dot(x, w)
# 激活函数实验
# 可以尝试不同的激活函数
z = sigmoid(a) # Sigmoid激活
# z = ReLU(a) # ReLU激活
# z = tanh(a) # tanh激活
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([], []) # 除第一层外,隐藏y轴刻度
# plt.xlim(0.1, 1) # 可以设置x轴范围
# plt.ylim(0, 7000) # 可以设置y轴范围
plt.hist(a.flatten(), 30, range=(0,1)) # 绘制直方图,30个bin,范围0-1
plt.show() # 显示图形基于MNIST数据集的权重初始值的比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 import os
import sys
# 添加父目录到系统路径,以便导入common模块
sys.path.append(os.pardir)
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
from common.optimizer import SGD
# 加载MNIST数据集并进行归一化处理
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 设置训练参数
train_size = x_train.shape[0] # 训练数据总数
batch_size = 128 # 批次大小
max_iterations = 2000 # 最大迭代次数
# 定义不同的权重初始化方法
# std=0.01: 标准差为0.01的正态分布
# Xavier: Xavier初始化(适用于sigmoid激活函数)
# He: He初始化(适用于ReLU激活函数)
weight_init_types = {'std=0.01': 0.01, 'Xavier': 'sigmoid', 'He': 'relu'}
optimizer = SGD(lr=0.01) # 随机梯度下降优化器,学习率为0.01
# 创建不同初始化方法的神经网络和损失记录
networks = {}
train_loss = {}
for key, weight_type in weight_init_types.items():
# 创建多层神经网络:输入784维,4个隐藏层各100个神经元,输出10维
networks[key] = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100],
output_size=10, weight_init_std=weight_type)
train_loss[key] = [] # 初始化损失记录列表
# 开始训练循环
for i in range(max_iterations):
# 随机选择批次数据
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 对每种权重初始化方法进行训练
for key in weight_init_types.keys():
# 计算梯度
grads = networks[key].gradient(x_batch, t_batch)
# 更新网络参数
optimizer.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 weight_init_types.keys():
loss = networks[key].loss(x_batch, t_batch)
print(key + ":" + str(loss))
# 绘制训练损失曲线
markers = {'std=0.01': 'o', 'Xavier': 's', 'He': 'D'} # 不同方法的标记符号
x = np.arange(max_iterations) # x轴:迭代次数
for key in weight_init_types.keys():
# 绘制平滑后的损失曲线,每100次迭代显示一个标记
plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations") # x轴标签
plt.ylabel("loss") # y轴标签
plt.ylim(0, 2.5) # 设置y轴范围
plt.legend() # 显示图例
plt.show() # 显示图形Batch Normalization的评估
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106 # coding: utf-8
# 本脚本用于对比带有批归一化(Batch Normalization)和不带批归一化的多层神经网络在不同权重初始值下的训练表现。
# 通过在MNIST数据集上训练网络,观察批归一化对训练收敛速度和准确率的影响。
import sys, os
sys.path.append(os.pardir) # 将父目录加入sys.path,便于导入上级目录中的模块
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist # 导入MNIST数据集加载函数
from common.multi_layer_net_extend import MultiLayerNetExtend # 导入可扩展多层网络实现
from common.optimizer import SGD, Adam # 导入优化器
# 加载MNIST数据集,并进行归一化处理
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 为了加快实验速度,仅取前1000个训练样本
x_train = x_train[:1000]
t_train = t_train[:1000]
max_epochs = 20 # 最大训练轮数
train_size = x_train.shape[0] # 训练集样本数
batch_size = 100 # 每个批次的样本数
learning_rate = 0.01 # 学习率
def __train(weight_init_std):
"""
训练带有和不带有批归一化的神经网络。
参数:
weight_init_std: 权重初始化的标准差
返回:
train_acc_list: 不带批归一化的网络在每个epoch的训练准确率
bn_train_acc_list: 带批归一化的网络在每个epoch的训练准确率
"""
# 构建带批归一化的网络
bn_network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100], output_size=10,
weight_init_std=weight_init_std, use_batchnorm=True)
# 构建不带批归一化的网络
network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100], output_size=10,
weight_init_std=weight_init_std)
optimizer = SGD(lr=learning_rate) # 使用SGD优化器
train_acc_list = [] # 记录不带BN的准确率
bn_train_acc_list = [] # 记录带BN的准确率
iter_per_epoch = max(train_size / batch_size, 1) # 每个epoch的迭代次数
epoch_cnt = 0 # 当前epoch计数
# 训练循环
for i in range(1000000000): # 迭代次数设置为极大,实际会在达到max_epochs时break
# 随机采样一个batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 对两个网络分别进行反向传播和参数更新
for _network in (bn_network, network):
grads = _network.gradient(x_batch, t_batch)
optimizer.update(_network.params, grads)
# 每经过一个epoch,记录一次准确率
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
bn_train_acc = bn_network.accuracy(x_train, t_train)
train_acc_list.append(train_acc)
bn_train_acc_list.append(bn_train_acc)
print("epoch:" + str(epoch_cnt) + " | " + str(train_acc) + " - " + str(bn_train_acc))
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
return train_acc_list, bn_train_acc_list
# 生成16个权重初始化标准差,从1到1e-4,等比数列
weight_scale_list = np.logspace(0, -4, num=16)
x = np.arange(max_epochs) # x轴为epoch数
# 对每个权重初始化标准差分别进行实验
for i, w in enumerate(weight_scale_list):
print( "============== " + str(i+1) + "/16" + " ==============")
train_acc_list, bn_train_acc_list = __train(w)
# 绘制每组实验的准确率曲线
plt.subplot(4,4,i+1)
plt.title("W:" + str(w))
if i == 15:
plt.plot(x, bn_train_acc_list, label='Batch Normalization', markevery=2)
plt.plot(x, train_acc_list, linestyle = "--", label='Normal(without BatchNorm)', markevery=2)
else:
plt.plot(x, bn_train_acc_list, markevery=2)
plt.plot(x, train_acc_list, linestyle="--", markevery=2)
plt.ylim(0, 1.0) # y轴范围
if i % 4:
plt.yticks([])
else:
plt.ylabel("accuracy")
if i < 12:
plt.xticks([])
else:
plt.xlabel("epochs")
plt.legend(loc='lower right')
plt.show() # 显示所有子图Dropout的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14 class Dropout:
def __init__(self, dropout_ratio=0.5):
slef.dropout_ratio = dropout_ratio
self.mask = None
def forward(self,x, train_flag=True):
if train_flag:
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
return x * self.mask
else:
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask使用Mnist数据集进行验证Dropout的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 # coding: utf-8
# 本脚本用于实验Dropout对深层神经网络过拟合的抑制作用。
# 通过在MNIST数据集上训练一个较深的网络,观察在有无Dropout时训练集和测试集准确率的变化。
import os
import sys
sys.path.append(os.pardir) # 将父目录加入sys.path,便于导入上级目录中的模块
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist # 导入MNIST数据集加载函数
from common.multi_layer_net_extend import MultiLayerNetExtend # 导入可扩展多层神经网络实现(支持Dropout)
from common.trainer import Trainer # 导入训练器类,简化训练流程
# 加载MNIST数据集,并进行归一化处理
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 为了更容易出现过拟合,仅取前300个训练样本
x_train = x_train[:300]
t_train = t_train[:300]
# 设置Dropout参数
use_dropout = True # 是否使用Dropout
# dropout_ratio为每层神经元被随机丢弃的比例,常用0.2~0.5
# 若不使用Dropout,可将use_dropout设为False
# ====================================================
dropout_ratio = 0.2 # Dropout比例
# 构建一个6层隐藏层的全连接神经网络,设置Dropout参数
network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100],
output_size=10, use_dropout=use_dropout, dropout_ration=dropout_ratio)
# 使用Trainer类进行训练,自动完成mini-batch梯度下降、准确率记录等
trainer = Trainer(network, x_train, t_train, x_test, t_test,
epochs=301, mini_batch_size=100,
optimizer='sgd', optimizer_param={'lr': 0.01}, verbose=True)
trainer.train()
# 获取训练集和测试集的准确率变化曲线
train_acc_list, test_acc_list = trainer.train_acc_list, trainer.test_acc_list
# 绘制训练集和测试集的准确率曲线
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)
plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()超参数最优化的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82 # coding: utf-8
import sys, os
sys.path.append(os.pardir) # 将父目录添加到sys.path,便于导入上级目录的模块
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.multi_layer_net import MultiLayerNet
from common.util import shuffle_dataset
from common.trainer import Trainer
# 加载MNIST数据集,并进行归一化处理
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 只取前500个训练样本,减少计算量,加快超参数搜索
x_train = x_train[:500]
t_train = t_train[:500]
# 设置验证集比例为20%
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)
# 先打乱数据,保证训练集和验证集的分布一致
x_train, t_train = shuffle_dataset(x_train, t_train)
x_val = x_train[:validation_num]
t_val = t_train[:validation_num]
x_train = x_train[validation_num:]
t_train = t_train[validation_num:]
# 定义训练函数,输入学习率和权重衰减,返回每轮的验证集和训练集准确率
def __train(lr, weight_decay, epocs=50):
# 构建一个6层隐藏层的全连接神经网络,带权重衰减
network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100],
output_size=10, weight_decay_lambda=weight_decay)
# 构建训练器,使用SGD优化器
trainer = Trainer(network, x_train, t_train, x_val, t_val,
epochs=epocs, mini_batch_size=100,
optimizer='sgd', optimizer_param={'lr': lr}, verbose=False)
trainer.train()
return trainer.test_acc_list, trainer.train_acc_list
# 超参数优化实验次数
optimization_trial = 100
results_val = {} # 存储每组超参数下的验证集准确率
results_train = {} # 存储每组超参数下的训练集准确率
for _ in range(optimization_trial):
# 随机采样权重衰减和学习率(对数均匀分布采样,覆盖大范围)
weight_decay = 10 ** np.random.uniform(-8, -4)
lr = 10 ** np.random.uniform(-6, -2)
# ================================================
# 训练网络并记录结果
val_acc_list, train_acc_list = __train(lr, weight_decay)
print("val acc:" + str(val_acc_list[-1]) + " | lr:" + str(lr) + ", weight decay:" + str(weight_decay))
key = "lr:" + str(lr) + ", weight decay:" + str(weight_decay)
results_val[key] = val_acc_list
results_train[key] = train_acc_list
# 输出超参数优化结果
print("=========== Hyper-Parameter Optimization Result ===========")
graph_draw_num = 20 # 最多画出前20组最优超参数的曲线
col_num = 5
row_num = int(np.ceil(graph_draw_num / col_num))
i = 0
# 按验证集最终准确率从高到低排序,依次画出前20组的准确率曲线
for key, val_acc_list in sorted(results_val.items(), key=lambda x:x[1][-1], reverse=True):
print("Best-" + str(i+1) + "(val acc:" + str(val_acc_list[-1]) + ") | " + key)
plt.subplot(row_num, col_num, i+1)
plt.title("Best-" + str(i+1))
plt.ylim(0.0, 1.0)
if i % 5: plt.yticks([])
plt.xticks([])
x = np.arange(len(val_acc_list))
plt.plot(x, val_acc_list) # 验证集准确率
plt.plot(x, results_train[key], "--") # 训练集准确率(虚线)
i += 1
if i >= graph_draw_num:
break
plt.show()