当前位置: 首页 > news >正文

第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的梯度记为\frac{\partial L}{\partial W}

η表示学习率,实际上会取0.010.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/ \sqrt{h} ,就可以调整学习的尺度。这意味着,参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。
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 ,则初始 值使用标准差为\frac{1}{\sqrt{n}} 的分布 (图 6-12 )。
http://www.dtcms.com/a/103649.html

相关文章:

  • (二)机器学习---常见任务及算法概述
  • 3.31 代码随想录第三十一天打卡
  • 第十章 VGA显示圆
  • # 使用 OpenCV 和神经网络实现图像风格化
  • AISEO中的JSON 如何部署?
  • 为 MinIO AIStor 引入模型上下文协议(MCP)服务器
  • kafka副本同步时HW和LEO
  • Linux驱动开发 中断处理
  • IP-PBX(IP专用交换机)
  • 《HarmonyOS Next开发实战:从零构建响应式Todo应用的基石》
  • 车载以太网网络测试-25【SOME/IP-报文格式-1】
  • RabbitMQ简单介绍和安装
  • OkHttp的拦截器是如何工作的?
  • 【Django】教程-7-分页,默认使用django的
  • LiteDB 数据存储与检索效率优化的最佳实践指导
  • Linux内核内存管理 ARM32页表映射流程和案例分享
  • 华三交换机配置常用命令
  • 【nvidia】Windows 双 A6000 显卡双显示器驱动更新问题修复
  • excel时间与时间戳互转,excel纯数字sql转为时间,日期函数
  • 从压测到选型:如何通过压力测试确定高防需求?
  • 软件测试 - 测试分类篇
  • 使用Java ApI 实现Hadoop文件上传
  • Navicat导出mysql数据库表结构说明到excel、word,单表导出方式记录
  • 乐橙R10 AI智能锁:以「技术减法」终结智能家居「参数内卷」
  • 音频进阶学习二十四——IIR滤波器设计方法
  • 构建智能旅游路线规划服务:deepseek与地理信息的完美结合
  • 音视频基础(音视频的录制和播放原理)
  • 1.计算机系统基础知识【软考视频笔记】
  • 【数据结构】数据结构与算法:开启编程世界的钥匙
  • FastPillars:一种易于部署的基于支柱的 3D 探测器