局部最小值 local minima, 鞍点saddle point
- 前言
- 一、鞍点和局部最小值
- 1.1 判断鞍点和局部最小值
- 1.2 参数更新
- 二、作业代码讲解
- 2.1 问题描述
- 2.2 代码部分
- 2.2.1 学号验证
- 2.2.2 安装外部库
- 2.2.3 导入外部库
- 2.2.4 创建模型
- 2.2.5 装载预训练的数据和检查点
- 2.2.6 计算 Hess 阵
- 2.2.6.1 梯度下降法
- 2.2.6.2 牛顿法
- 2.2.6.3 高斯牛顿法
- 2.2.6.4 hess矩阵计算
- 2.2.7 计算特征值的比率
- 2.2.8 构建主函数
- 2.2.9 调用文件
- 总结
前言
在深度学习中,了解模型在训练过程中参数更新停止时所处的点(即优化停止时的点)是鞍点(saddle point)还是局部最小值(local minimum)是非常重要的。这有助于理解模型的收敛行为以及可能出现的问题。接下来,本文基于Lee老师课程中的21年度机器学习作业2.2中的代码,尝试解释相关概念和知识点。便于读者使用需学习。本章节将会对代码中涉及到代码知识进行延伸尽可能的让本部分的内容看起来直观容易接受。
一、鞍点和局部最小值
在深度学习模型的训练过程中,梯度下降算法确实扮演了关键角色。该方法通过不断迭代参数来最小化损失函数,其核心在于利用损失函数梯度指示的方向来调整参数,目标是不断减少损失值以提升模型性能。
然而,并不是所有的优化过程都能保证顺利进行。有时,即使应用了梯度下降,模型参数的更新依然无法做到让损失函数值收敛到最低点。这可以在以下示意图上观察到:模型参数经过连续的更新后,损失仍与预期目标存在明显偏差。
另一种观察到的现象是,模型在训练过程中不断迭代后,损失值没有任何显著变化。这通常表现为图中黄色线条所展现的情况,即损失值保持不变。这种情况下,普遍的推测是遇到了梯度为零的局面,这直接导致了梯度下降算法无法正常工作,因为缺乏足够的方向性指引来更新模型参数。
当模型在训练过程中出现梯度为零的情况时,往往首先被想到的是局部最小值问题。这意味着模型参数在损失函数的梯度图中可能已经达到了一个驻点,这个点在局部区域内是最小的,但不一定是整个函数的最低点,即全局最小值。
另外一种可能遇到的情况是鞍点,这个名称源于它类似于马鞍的形状,其特征是在函数图像的中心点位置。请参考下图进行了解和对比。在鞍点处,梯度也会降至零。从图中可以直观地观察到,尽管在局部最小值附近,梯度信息不足以指引优化算法离开该点,但在鞍点附近,通常仍存在可以引导模型摆脱当前位置的梯度信息。
1.1 判断鞍点和局部最小值
当模型无法进行迭代优化的时候,如果要是鞍点的话实际上模型还是可以通过一些方式解决从而模型的性能能够提升。
那么如何进行判断当前所处的点是 local minima,还是saddle point呢?
为了深入理解这部分的证明,需借助于高等数学的概念。尽管可能并不了解具体损失函数的形状,泰勒级数的展开却能在一个特定点附近近似地表示任何可微分的函数。通过对该点附近的函数进行泰勒展开,实际上是在进行函数的近似微分分析。
高等数学必备知识这里不做过多解释,泰勒展开的基本想法
泰勒公式是一种将可微函数在某一点的邻域内近似为多项式的方法。对于在点
a
a
a 附近可导的函数
f
(
x
)
f(x)
f(x),其泰勒展开式为:
f
(
x
)
=
f
(
a
)
+
f
′
(
a
)
(
x
−
a
)
+
f
′
′
(
a
)
2
!
(
x
−
a
)
2
+
⋯
+
f
(
n
)
(
a
)
n
!
(
x
−
a
)
n
+
R
n
(
x
)
f(x) = f(a) + f'(a)(x - a) + \frac{f''(a)}{2!}(x - a)^2 + \cdots + \frac{f^{(n)}(a)}{n!}(x - a)^n + R_n(x)
f(x)=f(a)+f′(a)(x−a)+2!f′′(a)(x−a)2+⋯+n!f(n)(a)(x−a)n+Rn(x)
其中
f
(
n
)
(
a
)
f^{(n)}(a)
f(n)(a) 表示
f
(
x
)
f(x)
f(x) 在点
a
a
a 处的第
n
n
n 阶导数,
n
!
n!
n! 是
n
n
n 的阶乘,
R
n
(
x
)
R_n(x)
Rn(x) 是余项,表示误差项。
深度学习中的应用
简单说就是使用泰勒公式去展开损失函数,可以通过观察泰勒展开从而理解损失函数中当前参数的状态。
假设当前参数为 θ ′ \theta' θ′ ,我们希望理解损失函数 θ \theta θ 在 θ ′ \theta' θ′ 附近的变化。通过将 L ( θ ) L(\theta) L(θ) 在 θ ′ \theta' θ′ 处进行泰勒展开,可以得到下图:
g g g 是参数的一阶导数,而 H H H 是二阶导数,也被称为hess阵。
下图中展示的
L
(
θ
)
L(\theta)
L(θ)和
L
(
θ
′
)
L(\theta')
L(θ′) 之间的差距:
要判断 θ ′ \theta' θ′ 参数时模型是否处于鞍点和局部最小值,看上公式中绿色方框肯定是为 0 的,那就只能依赖剩下部分进行分析。
如果这部分是大于 0 的,那么思考下就是 L ( θ ) L(\theta) L(θ)一定大于 L ( θ ′ ) L(\theta') L(θ′),这个时候就到了 local minima, 而这部分小于 0 ,同理 L ( θ ) L(\theta) L(θ)一定小于 L ( θ ′ ) L(\theta') L(θ′)整体结果就如下图所示。
就是说下图红色圈圈位置存在正负的时候就是鞍点。
然而,要挑战的是进行穷尽性测试,检查所有可能的向量
v
v
v 来确定这一点是不切实际的。一个更为行之有效的方法是依赖于Hessian矩阵的特征值。当Hessian矩阵中的所有特征值都是正值时,可以确认该点是局部最小值;如果都是负值,同样可以确定。而当Hessian矩阵的特征值中既包含正值也包含负值时,该点则为鞍点。为了更直观地理解,读者可以参考下图进行对比理解:
1.2 参数更新
在上文中,探讨了当模型参数到达梯度为零的点时,怎样去判断该点是鞍点还是局部最小值点,并决定下一步如何更新模型参数的问题。在这种情况下,仅仅依赖于梯度信息已经不能为模型的参数更新提供清晰的方向。因而,需要运用更进阶的技术和策略来克服优化过程中遇到的难题。
实际上,为了解决这一问题,可以通过简单地将 v v v 替换为 u u u 来进行优化,因为之前提到的 v v v 代表的是参数之间的差异。通过这样人为地替换成特征向量,公式便得以相应改进:
可以直观的看到此时这部分的大小全部通过特征值来进行控制,因此特征值小于0,整体
u
T
H
u
u^THu
uTHu 都小于0,下图所示:
在 θ \theta θ 的参数损失是比当前梯度为 0 的 θ ′ \theta' θ′ 要小的,因此参数更新就变成了。
对上述问题如果还是存在疑问的话可以回顾下当前的整体过程。
二、作业代码讲解
本章节的作业重点在于判断鞍点与局部最小值,但需要注意的是,由于模型数据的下载须依照学号进行,因此,本章节无法直接演示代码运行结果。将专注于分析代码的整体逻辑以及讲解涉及的新知识点。
2.1 问题描述
在训练一个神经网络,并试图判断模型是处于局部最小值、鞍点,还是其他状况。可以通过计算Hessian矩阵来做出决定。
实际上,要找到一个梯度等于零或Hessian矩阵中所有特征值都大于零的点是非常困难的。在这次作业中,做了如下两项假设:
- 将梯度范数小于1e-3的情况视为梯度等于零。
- 如果最小特征值比例大于0.5且梯度范数小于1e-3,那么我们假设模型处于“类似局部最小值”的状态。
2.2 代码部分
本章代码并非深度学习训练代码,因此无完整训练逻辑,故此不使用脑图分析。在对某些涉及到具体的知识的部分在进行详细的讲解。
2.2.1 学号验证
通过一个断言进行提示用户对这部分修改成自己的学号,而assert student_id != ‘your_student_id’,则条件为假才会执行。
student_id = 'your_student_id' # fill with your student ID
assert student_id != 'your_student_id', 'Please fill in your student_id before you start.'
2.2.2 安装外部库
模型在计算hess阵的时候需要采用外部函数来完成这一复杂操作,因此需要安装这一库。
2.2.3 导入外部库
import numpy as np
from math import pi # 3.1415926
from collections import defaultdict # 可以直接新型的实例化创建
'''
from collections import defaultdict
# 创建一个 defaultdict,其默认值类型为 int
d = defaultdict(int)
# 当你尝试访问一个不存在的键时,它会自动返回 0
print(d['key']) # 输出:0
# 然后你可以像普通字典一样添加新的键值对
d['key'] = 5
print(d) # 输出:{'key': 5}
'''
from autograd_lib import autograd_lib # 外部导入的库目的是为了计算hess,后续会讲解
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
import warnings
warnings.filterwarnings("ignore")
'''
import warnings 是Python中用于处理警告(Warnings)的语句。
当你在代码中使用可能引发警告的函数或模块时,通常会看到一些消息,
这些消息提示你可能存在潜在问题或者不推荐的做法。例如,某些函数
可能即将被弃用,或者你在使用非标准库函数。
warnings.filterwarnings("ignore") 这一行代码的作用是设置
警告过滤器,将所有的警告级别(如FutureWarning, UserWarning,
DeprecationWarning等)都忽略。这意味着当你运行含有这些警告
的代码时,Python不会打印出警告信息,而是直接跳过它们。这在调
试阶段或你确定可以忽略这些警告的情况下很有用,但长期使用可能会
隐藏真正的错误。
然而,忽略警告并不总是最佳实践,因为有些警告是重要的,可以帮助你
改进代码。因此,你应该首先理解警告的内容,如果确实不需要理会,再
使用 filterwarnings("ignore")。在完成开发并修复所有警告后,
可以考虑移除这个过滤器,以便在生产环境中获取更完整的错误报告。
'''
2.2.4 创建模型
为了使模型能够达到梯度为零的状态,后续需要导入模型的具体参数,这里可以简单的看一下模型接受的就是一个单一的数值,以及输出一个结果。
class MathRegressor(nn.Module):
def __init__(self, num_hidden=128):
super().__init__()
self.regressor = nn.Sequential(
nn.Linear(1, num_hidden),
nn.ReLU(),
nn.Linear(num_hidden, 1)
)
def forward(self, x):
x = self.regressor(x)
return x
2.2.5 装载预训练的数据和检查点
这个检查点主要是按照学号的最后一位进行筛选,从而不同的学生分配到的是不同的题目。
# 检查点
import re # 导入正则表达式的库
key = student_id[-1]
if re.match('[0-9]', key) is not None: # 将 key即学号最后一位比对是否是数0-9的数字
key = int(key) # 强制类型转换
else:
key = ord(key) % 10 # 如果不是数字是字母的话进行ascii编码查找,取和10的余数作为检查点。
model = MathRegressor() # 实例化模型
autograd_lib.register(model) # 这部分是对模型的每一层埋下钩子。
data = torch.load('data.pth')[key] # 正常的装载数据
model.load_state_dict(data['model']) # 将模型的参数导入,即加载模型参数
train, target = data['data'] # 导入训练数据和目标
autograd_lib.register(model)
这部分的工作主要是由autograd_lib.这个库来进行完成,而这个register的实际作用就是埋下钩子🪝。当前部分对这个钩子编程思想进行讲解:
# 定义一个简单的神经网络
class SimpleNet(nn.Module):
def __init__(self):
super(SimpleNet, self).__init__()
self.fc = nn.Linear(10, 10) # 定义一个线性层
def forward(self, x):
x = self.fc(x)
x = self.fc(x)
# 数据通过线性层
return x
# 实例化网络
net = SimpleNet()
# 定义一个钩子函数
def forward_hook(module, input, output):
print("在这一层的激活值:")
print(output)
# 将钩子注册到我们的线性层上
hook = net.fc.register_forward_hook(forward_hook)
# 创建一些随机数据来模拟一次前向传播
test_input = torch.randn(1, 10)
# 执行前向传播
output = net(test_input)
# 不再需要钩子时,可以移除它
hook.remove()
下图是代码的运行结果:
对模型中隐藏层设置一个钩子,而这个钩子钩住的函数,就会随着这个层一起被执行。而autograd_lib.register(model) 做的就是在埋下钩子,目的是可以检测取到模型的参数数据。
2.2.6 计算 Hess 阵
在上文中描述如何判断鞍点和局部最小值的时候使用了Hess 阵,那么如何计算这个矩阵呢? 简单的看下下列代码,似乎只用到了 A B AB AB 两个矩阵的外积就可以,那么这又是怎么来的呢?接下来首先对这部分具体细节进行讲解在对代码部分进行详述。
本文对当前部分的详细讲解,借鉴了两片文章出自同一作者,具体链接如下。
https://www.hmoonotes.org/2020/05/intro-gradient-descent-newton.html
2.2.6.1 梯度下降法
先从梯度下降法讲起,梯度下降法(Gradient Descent)是一种用于寻找函数最小值的优化算法。其核心思想是在每一步迭代过程中,向函数梯度下降最陡峭的反方向迈进,从而逐渐接近函数的极小值点。具体来说,梯度下降法的更新公式为:
x k + 1 = x k − s k ∇ F ( x k ) x_{k+1} = x_{k} - s_{k} \nabla F(x_{k}) xk+1=xk−sk∇F(xk)
其中, ∇ F ( x k ) \nabla F(x_{k}) ∇F(xk) 表示梯度, s k s_{k} sk 代表步长,即每次迭代走的距离,也被称为学习率。实现了参数 x x x 的更新。
2.2.6.2 牛顿法
牛顿法(Newton’s method)则是另一种优化算法,目标也是找到一组
x
∗
x^*
x∗ 使得函数
F
F
F 对所有
x
x
x 的偏微分接近零,即
∇
F
=
0
\nabla F = 0
∇F=0。和梯度下降法不同,牛顿法试图直接找到使
∇
F
(
x
k
+
1
)
=
0
\nabla F(x_{k+1}) = 0
∇F(xk+1)=0 的
x
k
+
1
x_{k+1}
xk+1。基于泰勒展开,我们可以得到如下近似:没看懂这个泰勒展开的就是将展开的函数变成了一个导数再次展开就是从一阶导数开始了
∇ F ( x k + 1 ) ≈ ∇ F ( x k ) + H ( x k ) ( x k + 1 − x k ) = 0 \nabla F(x_{k+1}) \approx \nabla F(x_k) + H(x_k)(x_{k+1} - x_k) = 0 ∇F(xk+1)≈∇F(xk)+H(xk)(xk+1−xk)=0
在上式中, H ( x k ) H(x_k) H(xk) 是 Hessian 矩阵,可以认为是梯度函数 ∇ F \nabla F ∇F 的导数,即梯度函数的 Jacobian。由此可以推导出更新步骤 Δ x = ( x k + 1 − x k ) = − H ( x k ) − 1 ∇ F ( x k ) \Delta x = (x_{k+1} - x_k) = -H(x_k)^{-1} \nabla F(x_k) Δx=(xk+1−xk)=−H(xk)−1∇F(xk),因此,牛顿法的更新公式为:
x k + 1 = x k − H ( x k ) − 1 ∇ F ( x k ) x_{k+1} = x_k - H(x_k)^{-1} \nabla F(x_k) xk+1=xk−H(xk)−1∇F(xk)
通过上述公式,牛顿法利用梯度和Hessian矩阵的信息,进行迭代更新,试图快速找到函数的极小值点。相比梯度下降法,牛顿法通常收敛速度更快,但计算每一步的 Hessian 矩阵和其逆矩阵可能会带来更高的计算成本。
注意⚠️牛顿法的更新公式,一会要对比
2.2.6.3 高斯牛顿法
提到牛顿法中要计算 Hessian matrix 的时间与空间的复杂度太大,而高斯牛顿法的精神就是去近似 Hessian matrix 从而降低梯度。 高斯牛顿法的前提是这个最优化问题必须为 least square problem,也就是以下式子:
x ∗ = arg min x F ( x ) , x^* = \arg \min_x F(x), x∗=argxminF(x),
F ( x ) = 1 2 ∑ i = 1 m ( f i ( x ) ) 2 = 1 2 ∥ f ( x ) ∥ 2 = 1 2 f ( x ) T f ( x ) F(x) = \frac{1}{2} \sum_{i=1}^{m} (f_i(x))^2 = \frac{1}{2} \|f(x)\|^2 = \frac{1}{2} f(x)^Tf(x) F(x)=21i=1∑m(fi(x))2=21∥f(x)∥2=21f(x)Tf(x)
以上的问题当然可以用梯度下降法或牛顿法来解,但是如果用高斯牛顿法的话会更有效率。
高斯牛顿法的概念是去近似
f
(
x
)
f(x)
f(x),如果用泰勒展开式展开
f
(
x
)
f(x)
f(x) 可得:
可以直观的看到下面只是用到一阶导数,其实在简单点就是高数的导数变形而已
f ( x + Δ x ) ≈ f ( x ) + J ( x ) Δ x f(x + \Delta x) \approx f(x) + J(x)\Delta x f(x+Δx)≈f(x)+J(x)Δx
注意在这边
x
x
x 与
Δ
x
\Delta x
Δx 都是 n 维的向量,而
J
(
x
)
J(x)
J(x)针对所有参数的一阶导数
Jacobian matrix:
J ( x ) = [ ∂ f 1 ( x ) ∂ x 1 … ∂ f 1 ( x ) ∂ x n ⋮ ⋱ ⋮ ∂ f m ( x ) ∂ x 1 … ∂ f m ( x ) ∂ x n ] J(x) = \left[ \begin{array}{ccc} \frac{\partial f_1(x)}{\partial x_1} & \dots & \frac{\partial f_1(x)}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial f_m(x)}{\partial x_1} & \dots & \frac{\partial f_m(x)}{\partial x_n} \\ \end{array} \right] J(x)= ∂x1∂f1(x)⋮∂x1∂fm(x)…⋱…∂xn∂f1(x)⋮∂xn∂fm(x)
回到要求解的问题,也就是想找
Δ
x
\Delta x
Δx 使得
F
(
x
+
Δ
x
)
F(x + \Delta x)
F(x+Δx) 最小,也就是:
就是找到这样的一个增量
Δ
x
\Delta x
Δx从而让这个损失目标函数最小化,梯度为0
Δ x ∗ ≈ arg min Δ x F ( x + Δ x ) ≈ arg min Δ x 1 2 ∥ f ( x + Δ x ) ∥ 2 ≈ arg min Δ x 1 2 ∥ f ( x ) + J ( x ) Δ x ∥ 2 ≈ arg min Δ x 1 2 ( f ( x ) + J ( x ) Δ x ) T ( f ( x ) + J ( x ) Δ x ) ≈ arg min Δ x 1 2 ( ∥ f ( x ) ∥ 2 + 2 Δ x T J ( x ) T f ( x ) + Δ x T J ( x ) T J ( x ) Δ x ) \begin{align*} \Delta x^* &\approx \arg \min_{\Delta x} F(x + \Delta x) \\ &\approx \arg \min_{\Delta x} \frac{1}{2} \|f(x + \Delta x)\|^2 \\ &\approx \arg \min_{\Delta x} \frac{1}{2} \|f(x) + J(x)\Delta x\|^2 \\ &\approx \arg \min_{\Delta x} \frac{1}{2} (f(x) + J(x)\Delta x)^T(f(x) + J(x)\Delta x) \\ &\approx \arg \min_{\Delta x} \frac{1}{2} (\|f(x)\|^2 + 2\Delta x^TJ(x)^Tf(x) + \Delta x^TJ(x)^TJ(x)\Delta x) \end{align*} Δx∗≈argΔxminF(x+Δx)≈argΔxmin21∥f(x+Δx)∥2≈argΔxmin21∥f(x)+J(x)Δx∥2≈argΔxmin21(f(x)+J(x)Δx)T(f(x)+J(x)Δx)≈argΔxmin21(∥f(x)∥2+2ΔxTJ(x)Tf(x)+ΔxTJ(x)TJ(x)Δx)
可能会有部分读者没看懂这个公式是如何来的其实就是将泰勒公式带进去,然后进行了一个平方的展开。
取对于 Δ x \Delta x Δx 的导数并设为零求极值:
J ( x ) T f ( x ) + J ( x ) T J ( x ) Δ x = 0 J(x)^Tf(x) + J(x)^TJ(x)\Delta x = 0 J(x)Tf(x)+J(x)TJ(x)Δx=0
可以求得 Δ x \Delta x Δx:
Δ x = − ( J ( x ) T J ( x ) ) − 1 J ( x ) T f ( x ) \Delta x = -(J(x)^TJ(x))^{-1}J(x)^Tf(x) Δx=−(J(x)TJ(x))−1J(x)Tf(x)
因此我们就可以利用以下式子一直更新 x x x:
x k + 1 = x k + Δ x = x k − ( J ( x k ) T J ( x k ) ) − 1 J ( x k ) T f ( x k ) x_{k+1} = x_k + \Delta x = x_k - (J(x_k)^TJ(x_k))^{-1}J(x_k)^Tf(x_k) xk+1=xk+Δx=xk−(J(xk)TJ(xk))−1J(xk)Tf(xk)
跟牛顿法的式子对照可以看出高斯牛顿法的精神便是拿 J ( x ) T J ( x ) J(x)^TJ(x) J(x)TJ(x) 来近似 Hessian matrix,这部分读者可以看看牛顿法的更新公式对照一下。
2.2.6.4 hess矩阵计算
首先明确下 J ( x ) T J(x)^T J(x)T 实际上等于 下文的 A B AB AB的乘积,为什么是这样呢?
在神经网络中,希望计算损失函数相对于网络参数(如权重 w w w)的梯度,以便使用梯度下降等优化算法来更新这些参数。在反向传播算法中,需要计算损失函数 L L L 相对于输入 w w w 的导数,即 ∂ L ∂ w \frac{\partial L}{\partial w} ∂w∂L。
对于简单的两层神经网络, y y y 是网络的输出,定义如下:
y = σ ( w x + b ) y = \sigma(wx + b) y=σ(wx+b)
其中:
- x x x 是输入,
- w w w 是权重,
- b b b 是偏置,
- σ \sigma σ 是激活函数。
假设损失函数 L L L 的定义为 L ( y ) L(y) L(y)。为了计算梯度 ∂ L ∂ w \frac{\partial L}{\partial w} ∂w∂L,需要应用链式法则:
∂ L ∂ w = ∂ L ∂ y ⋅ ∂ y ∂ w \frac{\partial L}{\partial w} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial w} ∂w∂L=∂y∂L⋅∂w∂y
其中:
- ∂ L ∂ y \frac{\partial L}{\partial y} ∂y∂L 表示损失函数相对于输出 y y y 的导数。
- ∂ y ∂ w \frac{\partial y}{\partial w} ∂w∂y 是输出 y y y 相对于权重 w w w 的导数。
让我们计算 ∂ y ∂ w \frac{\partial y}{\partial w} ∂w∂y:
y = σ ( w x + b ) y = \sigma(wx + b) y=σ(wx+b)
应用链式法则:
∂ y ∂ w = ∂ y ∂ z ⋅ ∂ z ∂ w \frac{\partial y}{\partial w} = \frac{\partial y}{\partial z} \cdot \frac{\partial z}{\partial w} ∂w∂y=∂z∂y⋅∂w∂z
其中 z = w x + b z = wx + b z=wx+b。
考虑 ∂ y ∂ z \frac{\partial y}{\partial z} ∂z∂y:
∂ y ∂ z = σ ′ ( z ) \frac{\partial y}{\partial z} = \sigma'(z) ∂z∂y=σ′(z)
这里 σ ′ ( z ) \sigma'(z) σ′(z) 是激活函数 σ \sigma σ 对 z z z 的导数,是一个对角矩阵。
而 ∂ z ∂ w \frac{\partial z}{\partial w} ∂w∂z 则是 x x x,因为 z = w x + b z = wx + b z=wx+b。
将这两部分组合起来得到:
∂ y ∂ w = σ ′ ( z ) ⋅ x \frac{\partial y}{\partial w} = \sigma'(z) \cdot x ∂w∂y=σ′(z)⋅x
因此,在反向传播中,梯度 ∂ L ∂ w \frac{\partial L}{\partial w} ∂w∂L 可以表示为:
∂ L ∂ w = ∂ L ∂ y ⋅ ( σ ′ ( z ) ⋅ x ) \frac{\partial L}{\partial w} = \frac{\partial L}{\partial y} \cdot (\sigma'(z) \cdot x) ∂w∂L=∂y∂L⋅(σ′(z)⋅x)
这表明了梯度 ∂ L ∂ w \frac{\partial L}{\partial w} ∂w∂L 可以由损失函数相对于输出 y y y 的导数和激活函数的导数与输入 x x x 的乘积来表示。在梯度 ∂ L ∂ w \frac{\partial L}{\partial w} ∂w∂L 的计算中,我们可以将其看作是两个矩阵相乘的结果。假设我们有两个矩阵 A A A 和 B B B:
- A A A 是损失函数相对于输出 y y y 的导数和激活函数的导数构成的矩阵,
- B B B 是输入 x x x 构成的矩阵。
这两个矩阵相乘的结果就构成了梯度 ∂ L ∂ w \frac{\partial L}{\partial w} ∂w∂L。
def save_activations(layer, A, _): # 存储输入层的信息,而6是由于批次的原因
'''
A is the input of the layer, we use batch size of 6 here
layer 1: A has size of (6, 1)
layer 2: A has size of (6, 128)
'''
activations[layer] = A
# helper function to compute Hessian matrix
def compute_hess(layer, _, B):
'''
B is the backprop value of the layer
layer 1: B has size of (6, 128)
layer 2: B ahs size of (6, 1)
'''
A = activations[layer]
BA = torch.einsum('nl,ni->nli', B, A) # 构成了其中的一个J(x)
# full Hessian
hess[layer] += torch.einsum('nli,nkj->likj', BA, BA) # J(x)TJ(x)约等于H
'''
多批次的计算结果累加,这部分和损失函数的导数实际上是一致的,最终结果需要全部样本数据计算
采用了批次为6的计算,故此需要结果累加
'''
2.2.7 计算特征值的比率
在上一个小节中成功实现了对hess阵的计算,现阶段主要的目的就是通过特征向量的正负值判断,判断当前参数是否属于local minima还是saddle point。
# function to compute the minimum ratio
def compute_minimum_ratio(model, criterion, train, target):
model.zero_grad() # 梯度清0
# compute Hessian matrix
# save the gradient of each layer
with autograd_lib.module_hook(save_activations):
'''
with 函数执行则会进入到 autograd_lib.module_hook中这个钩子钩住了save_activations
,然后执行output = model(train)执行下列代码模型在训练中会被触发之前设定的钩子,
这个钩子钩住的autog rad_lib.module_hook钩住过来的的save_activations函数,从而记录中间层结果。
'''
output = model(train)
loss = criterion(output, target)
# compute Hessian according to the gradient value stored in the previous step
with autograd_lib.module_hook(compute_hess):
'''
with 函数执行则会进入到 autograd_lib.module_hook中这个钩子钩住了compute_hess
,然后执行autograd_lib.backward_hessian(output, loss='LeastSquares')这个钩
子钩住的autog rad_lib.module_hook钩住过来的的compute_hess函数,从而记录hess阵。
'''
autograd_lib.backward_hessian(output, loss='LeastSquares') # 和上文中的一部分对应,可以自己寻找下,主要是限定损失形式
layer_hess = list(hess.values()) # 将两层数据存入列表迭代
minimum_ratio = []
# 计算hess阵的特征值
# compute eigenvalues of the Hessian matrix
for h in layer_hess: # 取出一层的hess阵
size = h.shape[0] * h.shape[1]
h = h.reshape(size, size)
h_eig = torch.symeig(h).eigenvalues # torch.symeig() returns eigenvalues and eigenvectors of a real symmetric matrix
num_greater = torch.sum(h_eig > 0).item() #计算特征值大于0的个数
minimum_ratio.append(num_greater / len(h_eig)) #记录当前层的比率
ratio_mean = np.mean(minimum_ratio) # 两层一起的
return ratio_mean
再次阐述下这个部分:
with autograd_lib.module_hook(save_activations):
output = model(train)
loss = criterion(output, target)
这里的过程涉及了Python的with
语句和钩子(hook)机制如何在深度学习框架中使用,以实现在模型训练过程中捕捉每一层的信息。
-
当使用
autograd_lib.register(model)
时,为模型的每一层注册了基础钩子。这个过程本身并没有指定当这些钩子被触发时会执行什么操作。 -
然后,使用
with autograd_lib.module_hook(save_activations)
这个语句。这里的with
语法构建了一个上下文管理器,意指在这个with
语句块内,所有的操作都会以某种额外的上下文进行。对于这种情况,这个with
语句块意味着在它内部的代码执行期间,任何经过模型每一层的数据都会触发save_activations
函数。这是因为module_hook(save_activations)
指定了当基础钩子被触发时应该执行save_activations
函数。 -
当你执行
output = model(train)
这条代码时,数据开始流经模型。因为你处于with autograd_lib.module_hook(save_activations)
语句块的作用范围内,模型的每一层在处理数据时,都会触发先前通过autograd_lib.register(model)
注册的基础钩子。由于这些钩子现在与save_activations
函数相连,因此每层的数据都会被save_activations
捕捉并处理。 -
这样,就可以在模型的训练过程中,准确记录每一层的中间结果。这对于理解模型的行为、调试模型或者分析模型的内部表示非常有价值。
总而言之,with autograd_lib.module_hook(save_activations)
确保了模型在这个语句块内训练时,你特定函数(这里是save_activations
)会在数据流经模型每一层时被触发,从而达到捕捉和记录模型每一层输出的目的。
2.2.8 构建主函数
主函数文件创建,在运行过程中,直接进行赋值调用就可以了。
def main(model, train, target):
criterion = nn.MSELoss() #实例化损失函数
#讲解下下面两个函数
gradient_norm = compute_gradient_norm(model, criterion, train, target)
minimum_ratio = compute_minimum_ratio(model, criterion, train, target)
print('gradient norm: {}, minimum ratio: {}'.format(gradient_norm, minimum_ratio))
# function to compute gradient norm
def compute_gradient_norm(model, criterion, train, target):
model.train()
model.zero_grad()
output = model(train)
loss = criterion(output, target)
loss.backward() #上述是正常的模型计算模块
grads = [] #存储梯度信息
for p in model.regressor.children(): # 会迭代当前模块regressor的的子层
if isinstance(p, nn.Linear): # 判断p是不是nn.Linear的实例化对象。即是不是线性层
param_norm = p.weight.grad.norm(2).item() #计算L2范数
grads.append(param_norm) #储存一下啊
grad_mean = np.mean(grads) # 计算范数均值
return grad_mean
在上文中,模型计算了各层权重的 L2 范数。这样做的目的是为了更有效地监控模型的所有参数。如果参数出现重大变化,这可能表明模型的权重正在为了适应离群点而调整。通过这种方式,可以预见并识别潜在的问题,如模型过拟合或参数的异常变化。返回各层范数的均值有助于在更宽广的层面上监控模型的行为,确保模型训练的健康性。
2.2.9 调用文件
接下来模型进行主要的参数设置及代码运行模块。
if __name__ == '__main__': #判断是不是在当前文件下执行
# fix random seed
torch.manual_seed(0) # 随机数设置
# reset compute dictionaries
activations = defaultdict(int)
'''
这行代码使用了 Python 的 collections.defaultdict 类型来创建一个名为
activations 的字典。这种特殊的字典类型非常有用,特别是在你需要字典项默
认初始化而不是抛出一个 KeyError 时。这里,int 作为 defaultdict 的参数
意味着任何尚未显式设置的键将会自动被赋予整数 0 作为其值。这在处理例如激活统
计或计数器等功能时非常方便。
'''
hess = defaultdict(float)
# compute Hessian
main(model, train, target)
总结
本文作为一个学习实例,旨在作为深度学习入门的参考。然而,在学习过程中,遇到了诸多挑战,这促使我对特定主题进行了更深入的探索。希望读者能够再次细化理解更好的迈入深度学习的大门,文章详细探讨了在深度学习中如何识别和区分鞍点与局部最小值的问题。在PyTorch环境中,设置模型、计算梯度、监测激活状态,还通过具体的代码示例,展现了一个完整的操作流程。这些核心知识点和操作技巧对于有效训练深度学习模型和诊断问题非常关键,帮助研究人员和开发者优化算法性能,提高模型的稳定性和可靠性。