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

深度学习核心概念:优化器、模型可解释性与欠拟合

文章目录

  • 前言
  • 一、优化器与优化方法
    • 1.1 SGD
    • 1.2 SGD_Momentum 优化算法概述
      • 1.2.1 TensorFlow 框架中的更新规则:
      • 1.2.2 PyTorch 框架中的更新规则:
      • 1.2.3 动量累积机制图解
      • 1.2.4 代码示例:
    • 1.3 AdaGrad
      • 1.3.1 核心思想与背景
      • 1.3.2 工作原理
      • 1.3.3 优点
      • 1.3.4 缺点
      • 1.3.5 代码示例
    • 1.4 RMSProp
      • 1.4.1 更新方案
      • 1.4.2 指数加权移动平均(Exponential Moving Average,EMA)
      • 1.4.3 优点
      • 1.4.4 缺点
      • 1.4.5 代码示例
    • 1.5 Adam 优化器详解
      • 1.5.1 矩估计引入
      • 1.5.2 矩估计的初始偏差问题
      • 1.5.3 偏差校正引入
      • 1.5.4 Adam 更新方案
      • 1.5.5 代码示例
  • 二、神经网络的可解释性
  • 三、欠拟合(Underfitting)
  • 总结


前言

在昨天的文章中,我们共同迈出了探索深度学习世界的坚实第一步。通过对感知机、全连接层以及至关重要的链式求导法则的学习,我们不仅掌握了神经网络的基本构成与运作机制,更初步揭开了模型训练的神秘面纱。今天,我们将不懈地沿着这条求知之路继续前行,进一步巩固和拓展我们对深度学习基础知识的理解,为未来学习更复杂的模型与技术打下更加坚实的基础。


一、优化器与优化方法

不同的优化器有着各自的优缺点,本节主要介绍梯度下降(Gradient Descent)及其主要变体。

1.1 SGD

  • 背景问题: 传统的梯度下降在面对海量数据时,若一次性计算所有样本的梯度并求平均,会导致内存/显存不足(OOM),超出硬件承载能力。为解决大数据量下的梯度更新问题,演变出了以下几种方案:

    1. 批量梯度下降 (Batch Gradient Descent, BGD)

    • 原理: 每次迭代使用全部训练数据计算梯度并更新参数。
    • 优点:
      • 梯度计算更准确。
      • 易收敛至全局最优解。
    • 缺点:
      • 大规模数据集下易导致内存/显存溢出。
      • 计算开销极大。
    • 适用: 仅适用于小数据集。

    2. 随机梯度下降 (Stochastic Gradient Descent, SGD)

    • 原理: 每次迭代仅使用单个样本计算梯度并更新参数。
    • 优点:
      • 计算开销极低(因每次只处理一个样本)。
    • 缺点:
      • 需迭代次数激增(例如100万样本需100万次迭代)。
      • 梯度方向随机性大,训练过程震荡明显,效率较低。
    • 适用: 超大规模数据集的快速近似优化。

    3. 小批量梯度下降 (Mini-batch Gradient Descent, MBGD)

    • 原理: 平衡 BGD 与 SGD。将数据集划分为多个固定大小的 mini-batch(如 32/64/128),每次迭代使用一个 mini-batch 计算梯度并更新参数。
    • 优点:
      • 兼顾计算效率与内存优化。
      • 可根据硬件性能调整 batch_size
    • 地位: 深度学习的主流实现方案。
  • 重要说明:

    • 实际应用中,人们常将 MBGD 简称为 “SGD”,但这在严格意义上是不准确的。
    • 严格区分:SGD 每次只用 1 个样本,而 MBGD 使用多个样本(一个 mini-batch)。
  • batch_size 与更新次数关系:

    • 一个 Epoch(遍历全部样本一次)的更新次数 = 总样本数 / batch_size
    • 示例: 若总样本数 = 10,
      • batch_size = 10 时,1 个 epoch 仅需 1 次更新(遍历全部样本)。
      • batch_size = 2 时,1 个 epoch 需 5 次更新(10 / 2 = 5 个 mini-batch)。
  • 代码示例:

# 导入相关库
import numpy as np  # 导入NumPy库,用于进行数值计算,尤其是数组操作
import matplotlib.pyplot as plt  # 导入Matplotlib的pyplot模块,用于绘制图表
import matplotlib.gridspec as gridspec  # 导入Matplotlib的gridspec模块,用于更灵活地创建子图布局# 1. 散点输入 (Input Scatter Points)
# 定义一组二维数据点,每个点包含一个特征X和一个目标Y (或称标签)
points = np.array([[-0.5, 7.7], [1.8, 98.5], [0.9, 57.8],[0.4, 39.2], [-1.4, -15.7], [-1.4, -37.3],[-1.8, -49.1], [1.5, 75.6], [0.4, 34.0], [0.8, 62.3]])
# 从数据点中分离特征X(第一列)和目标Y(第二列)
X = points[:, 0]  # 提取所有点的X坐标(特征值)
Y = points[:, 1]  # 提取所有点的Y坐标(目标值/真实标签)# 2. 参数初始化 (Parameter Initialization)
# 初始化线性回归模型的参数w(权重)和b(偏置),以及学习率lr
# 线性回归模型通常表示为 y_pred = w * X + b
w = 0  # 权重(weight),表示X对Y影响的斜率
b = -1  # 偏置(bias),表示Y轴截距
lr = 0.001  # 学习率(learning rate),控制每次参数更新的步长,决定模型收敛的速度和稳定性# 3. 定义损失函数 (Define Loss Function)
# 这里使用均方误差 (Mean Squared Error, MSE) 作为损失函数
def loss_func(X, w, b):# 定义前向传播 (Define Forward Propagation)# 根据当前权重w和偏置b,计算模型的预测值 pre_ypre_y = np.dot(X, w) + b  # 对于一维特征X和标量权重w,np.dot(X,w)等同于X*w# 计算均方误差:所有样本 (Y - pre_y)^2 的平均值loss = np.mean((Y - pre_y) ** 2)  # np.mean会自动处理求和并除以样本数量return loss# 定义优化器 (Define Optimizer)
# 这里使用随机梯度下降(Stochastic Gradient Descent, SGD)
# SGD的参数更新规则是: param = param - lr * d(Loss)/d(param)
def SGD(points, w, b, lr, batch_size=1):# 第一步:打乱数据顺序,以引入随机性,避免模型训练时陷入局部最优或震荡np.random.shuffle(points)  # np.random.shuffle(points) 直接在原始数组上进行打乱,无返回值# 第二步:根据batch_size(批量大小)迭代处理数据# range(0, len(points), batch_size) 会生成每个批次的起始索引for num_batch in range(0, len(points), batch_size):# 提取当前批次的数据点batch_points = points[num_batch:num_batch + batch_size, :]# 分离当前批次的特征 (batch_x) 和标签 (batch_y)batch_x = batch_points[:, 0]  # 当前批次的X值batch_y = batch_points[:, 1]  # 当前批次的Y值# 计算当前批次的模型预测值batch_pre_y = w * batch_x + b# 计算损失函数对w的梯度 (dw)# 损失 L = 1/N * Σ(batch_pre_y - batch_y)^2# dL/dw = 1/N * Σ(2 * (batch_pre_y - batch_y) * batch_x)dw = np.mean(2 * (batch_pre_y - batch_y) * batch_x)# 计算损失函数对b的梯度 (db)# dL/db = 1/N * Σ(2 * (batch_pre_y - batch_y))db = np.mean(2 * (batch_pre_y - batch_y))# 更新权重w和偏置bw = w - lr * dw  # w = w - 学习率 * 梯度(w)b = b - lr * db  # b = b - 学习率 * 梯度(b)return w, b  # 返回一个epoch结束后更新的w和b# 设置训练的迭代轮次 (epochs) 和批量大小 (batch size)
epoches = 100  # 此处的epoches和bs会在后面主循环前被重新定义,这里可视为默认或占位
bs = 2  # 批量大小Batch Size# 6. 绘制和显示 - 准备损失曲面图 (Plotting and Display - Preparing Loss Surface Plot)
# 构建w和b的取值范围,用于绘制损失函数的3D曲面和等高线图
w_values = np.linspace(-20, 80, 100)  # w从-20到80之间取100个等间距的点
b_values = np.linspace(-20, 80, 100)  # b从-20到80之间取100个等间距的点
# 使用meshgrid创建w和b的网格点矩阵,用于计算每个(w, b)组合对应的损失值
W, B = np.meshgrid(w_values, b_values)# 计算网格中每个(w, b)组合对应的损失值
# loss_values将存储一个与W和B形状相同的矩阵,每个元素是对应(w, b)的损失
loss_values = np.zeros_like(W)  # 初始化一个与W形状相同的零矩阵来存储损失值
for i, w_val in enumerate(w_values):  # 遍历w的所有可能值 (列索引)for j, b_val in enumerate(b_values):  # 遍历b的所有可能值 (行索引)# 注意:这里调用loss_func时,会使用全局的X和Y数据来计算损失loss_values[j][i] = loss_func(X, w_val, b_val)  # loss_values[行][列] = loss_func(w_val, b_val)# 构建图像对象 (Create Figure Object)
fig = plt.figure(figsize=(12, 6))  # 创建一个尺寸为12x6英寸的Matplotlib图形# 创建画布布局 (Create Gridspec Layout)
# gridspec.GridSpec将整个图像窗口切分为一个2行2列的网格
gs = gridspec.GridSpec(2, 2)# 定义左上角的子图 (ax1):用于显示数据点和当前的拟合直线
ax1 = fig.add_subplot(gs[0, 0])  # gs[0,0]表示网格的第0行第0列# 定义左下角的子图 (ax2):用于显示损失函数的等高线图和梯度下降的路径
ax2 = fig.add_subplot(gs[1, 0])  # gs[1,0]表示网格的第1行第0列# 定义整个右侧的子图 (ax3):用于显示损失函数的3D曲面和梯度下降的3D路径
ax3 = fig.add_subplot(gs[:, 1], projection="3d")  # gs[:,1]表示网格的所有行(:)的第1列
# projection="3d"指定这是一个3D坐标系
# 绘制3D损失曲面
# cmap是颜色映射(例如'viridis'是一种常用的颜色方案),alpha是透明度
ax3.plot_surface(W, B, loss_values, cmap='viridis', alpha=0.8)
ax3.set_xlabel("w")  # 设置X轴标签
ax3.set_ylabel("b")  # 设置Y轴标签
ax3.set_zlabel("Loss")  # 设置Z轴标签# 存储梯度下降路径 (Store Gradient Descent Path)
# gd_path用于存储每次迭代后w和b的值 (w, b) 组成的元组列表
gd_path = []
# gd_path_value用于存储每次迭代后对应的损失值
gd_path_value = []# 5. 开始迭代 (Start Iteration) - 梯度下降训练循环
epoches = 1000  # 设定总的训练轮次 (epochs),即完整遍历数据集的次数
bs = 2  # 设定批量大小 (batch size)# 循环进行指定次数的迭代(epochs)
for epoch in range(1, epoches + 1):# 调用SGD函数,用当前批次的数据更新w和bw, b = SGD(points, w, b, lr, batch_size=bs)# 记录当前迭代结束后的(w, b)值,用于绘制梯度下降路径gd_path.append((w, b))# 计算并记录当前(w, b)对应的损失值gd_path_value.append(loss_func(X, w, b))# 6. 设置显示频率 (Set Display Frequency)# 每20个epoch或在第一个epoch时更新并显示图表,以观察训练过程if epoch == 1 or epoch % 20 == 0:# 打印当前epoch的损失值,便于观察模型收敛情况print(f"Epoch {epoch}: Loss = {loss_func(X, w, b):.4f}")  # 使用f-string格式化输出# 7. 绘制和显示 (Plotting and Display) - 动态更新图表内容# 绘制左上角的图 (ax1) - 数据点与拟合线ax1.cla()  # 清除当前子图 (ax1) 的所有内容,以便重新绘制ax1.scatter(X, Y, c='r')  # 绘制原始数据散点图,颜色为红色ax1.set_title(f"Epoch: {epoch}, Loss: {loss_func(X, w, b):.2f}")  # 设置子图标题,显示当前epoch和损失# 绘制当前模型拟合的直线x_line = np.linspace(np.min(X), np.max(X), 100)  # 生成用于绘制直线的X值范围y_line = np.dot(x_line, w) + b  # 根据当前w和b计算对应的Y值 (即预测值)ax1.plot(x_line, y_line, c='g')  # 绘制拟合线,颜色为绿色# 绘制左下角的图 (ax2) - 损失函数的等高线图和梯度下降路径ax2.cla()  # 清除当前子图 (ax2) 的所有内容# 绘制损失函数的等高线图,levels=50表示生成50条等高线,cmap='viridis'设置颜色映射ax2.contourf(W, B, loss_values, levels=50, cmap='viridis', alpha=0.8)# 在等高线图上绘制当前(w,b)点的位置,黑色大点ax2.scatter(w, b, c="black", s=20, zorder=10)  # zorder确保点绘制在等高线之上# 提取梯度下降路径中所有存储的w和b分量gd_w, gd_b = zip(*gd_path)  # zip(*)用于解压列表中的元组,gd_w是所有w值,gd_b是所有b值# 绘制梯度下降的路径(从初始点到当前点)ax2.plot(gd_w, gd_b, c='black', linewidth=1.5)  # 绘制黑色路径线ax2.set_xlabel("w")  # 设置X轴标签ax2.set_ylabel("b")  # 设置Y轴标签# 绘制右侧的图 (ax3) - 损失函数的3D曲面图和梯度下降的3D路径# 注意:每次`ax3.cla()`都会清除整个3D图(包括曲面),然后`ax3.plot_surface`会重新绘制曲面。# 对于非常多的epoch,这可能效率较低,但在此示例中效果是正确的。# ax3.cla() # 清除3D子图内容# ax3.plot_surface(W,B,loss_values,cmap='viridis',alpha=0.8) # 重新绘制损失曲面 (如果之前被cla清除了)# 绘制当前(w,b)点在3D曲面上,黑色大点ax3.scatter(w, b, loss_func(X, w, b), c='black', s=20, zorder=10)# 绘制梯度下降的3D路径ax3.plot(gd_w, gd_b, gd_path_value, c='black', linewidth=1.5)  # 路径在3D空间中展示plt.pause(0.01)  # 暂停0.01秒,使图形窗口有时间更新并显示动画效果,不建议太长(原为1秒)# 再次暂停1秒,因为原代码有两次plt.pause(1)# plt.pause(1) # 这行可能是复制导致,通常一次足够# 显示最后的图,不退出 (Display the final plot, keep it open)
plt.show()  # 显示所有图形,并阻塞程序执行,直到图形窗口被手动关闭

1.2 SGD_Momentum 优化算法概述

SGD+Momentum 是随机梯度下降(Stochastic Gradient Descent,SGD)的一种变体,通过引入“动量”(Momentum)的概念来改进传统 SGD 算法,旨在解决其训练过程中可能出现的震荡和收敛速度较慢的问题。

动量的引入有助于解决SGD中的震荡收敛速度较慢的问题。可以更好地应对梯度变化和梯度消失问题,从而提高训练模型的效率稳定性

  1. 加速收敛:当梯度方向一致时,动量会持续累积,使得参数更新步伐更大。
  2. 减小震荡:当梯度方向频繁变化时,动量有助于平滑更新路径,减少参数在最优解附近来回震荡,使得参数更新更加平稳。
  3. 应对复杂地形:有助于模型更好地穿越局部最小值或鞍点,避免被困住。

简单来说,动量让参数更新“记住之前的路”,避免被当前局部的、可能带有噪声的梯度“带偏”,从而走得更稳、更快。

SGD+Momentum 的更新规则在不同框架中可能略有差异。

1.2.1 TensorFlow 框架中的更新规则:

  • 动量计算 v t = η ⋅ v t − 1 + α ⋅ g t v_{t}=\eta\cdot v_{t-1}+\alpha\cdot g_t vt=ηvt1+αgt
  • 参数更新 P t + 1 = P t − v t P_{t+1}=P_{t}-v_{t} Pt+1=Ptvt

参数说明:

  • α \alpha α:学习率(learning rate)。
  • g t g_t gt:在t时刻参数( W t W_{t} Wt b t b_{t} bt)的损失梯度。
  • η \eta η:动量系数(momentum coefficient),取值范围通常在0~1之间,默认值为0.9。该值越大,表示历史梯度对当前更新的影响越大。
  • v t v_{t} vt:当前时刻的动量,是一个滑动平均值,用于积累梯度的方向。
  • v t − 1 v_{t-1} vt1:上一时刻的动量。初始值通常设置为0。
  • P t P_{t} Pt:t时刻的模型参数值。
  • P t + 1 P_{t+1} Pt+1:t+1时刻的模型参数值。

1.2.2 PyTorch 框架中的更新规则:

  • 动量计算 v t = η ⋅ v t − 1 + g t v_{t}=\eta\cdot v_{t-1}+g_t vt=ηvt1+gt
  • 参数更新 P t + 1 = P t − α ⋅ v t P_{t+1}=P_{t}-{\alpha}\cdot v_{t} Pt+1=Ptαvt

参数说明(与TensorFlow类似,但有以下关键差异):

  • η \eta η:动量系数,处于0到1之间,值越大,说明越重视上一次的影响。为零时,算法退化为标准SGD。通常设置为0.9。
  • v t − 1 v_{t-1} vt1:上一时刻的动量。初始值通常是第一步的梯度值。
  • PyTorch 特性:PyTorch 将 SGD 和 Momentum 功能整合在一起,用户在调用时即可通过参数启用动量。
    在这里插入图片描述

TensorFlow 与 PyTorch 规则的主要区别在于学习率 α \alpha α 的应用位置

  • TensorFlow 在计算 v t v_t vt 时就将学习率与梯度 g t g_t gt 相乘。
  • PyTorch 则在最终更新 P t + 1 P_{t+1} Pt+1 时才将学习率与计算出的 v t v_t vt 相乘。

1.2.3 动量累积机制图解

下图展示了动量如何修正参数更新方向:

在这里插入图片描述

  1. 基础流程(从 A 到 B)

    • A 点:记录当前参数的梯度下降方向与大小(图中表示为水平向右的箭头)。此梯度信息会以“动量”形式保留(乘以动量系数 η \eta η 后传递到 B 点)。
    • 动量传递:这意味着在 B 点更新时,“记住”了 A 点的梯度方向。
  2. 动量的作用(B 点的更新)

    • B 点自身梯度:当前参数在 B 点的梯度方向是“斜向左下”。如果只看当前梯度,更新方向可能会导致震荡。
    • 动量修正:从 A 点传递来的动量(水平向右的箭头)会与 B 点自身的梯度叠加。
    • 实际梯度下降方向:最终形成的 B 点的实际梯度下降方向(指向 C 点的箭头)是两者的合力,它会更稳定、更平滑。

本质:SGD+Momentum 的核心在于利用 A 点的历史梯度“惯性”,来平滑 B 点当前梯度的“震荡”,从而让参数更新方向更稳定、收敛更快。

1.2.4 代码示例:

# 导入相关库
import numpy as np  # 用于进行数值计算,特别是数组和矩阵操作
import matplotlib.pyplot as plt  # 用于绘制各种静态、动态、交互式的图表
import matplotlib.gridspec as gridspec  # 用于更灵活地创建子图布局,将图表窗口划分为网格# 1. 散点输入 (数据准备)
# 定义训练数据点,每个点包含一个特征值 (X) 和一个标签值 (Y)
# 这是一个用于线性回归的二维数据集
points = np.array([[-0.5, 7.7], [1.8, 98.5], [0.9, 57.8], [0.4, 39.2], [-1.4, -15.7], [-1.4, -37.3], [-1.8, -49.1],[1.5, 75.6],[0.4, 34.0], [0.8, 62.3]])# 将数据分离为特征 (X) 和标签 (Y)
X = points[:, 0]  # 取所有行的第一列作为特征 (自变量)
Y = points[:, 1]  # 取所有行的第二列作为标签 (因变量)# 2. 参数初始化
# 初始化模型参数:权重 (w) 和偏置 (b)
# 这两个参数将通过训练逐步优化,以使模型的预测尽可能接近真实标签
w = 0  # 初始权重
b = -1  # 初始偏置
lr = 0.001  # 学习率 (learning rate),控制每次参数更新的步长,决定了模型收敛的速度和稳定性
vt = 0  # 初始的动量累积项 (此处定义但未在全局直接使用,具体在优化器内部定义 v_w 和 v_b)# 3. 定义损失函数
# 损失函数用于衡量模型预测值与真实值之间的差距
def loss_func(X, w, b):# 定义前向传播 (forward propagation):根据当前参数 w 和 b 计算预测值# 对于线性回归模型,预测公式为:y_pred = X * w + bpre_y = np.dot(X, w) + b# 计算均方误差 (Mean Squared Error, MSE) 作为损失# MSE = mean((真实值 - 预测值)^2)# 目标是最小化这个损失值loss = np.mean((Y - pre_y) ** 2)return loss# 4. 定义优化器
# 这里使用带有动量 (Momentum) 的随机梯度下降 (SGD) 优化器
# 动量算法的原理是:在更新参数时,不仅考虑当前梯度,还结合之前梯度的方向和大小
# 动量更新公式:v_t = momentum * v_{t-1} + lr * g_t (其中 g_t 是当前梯度)
# 参数更新公式:P_{t+1} = P_t - v_t
# 这里采用类似TensorFlow的实现风格,初始动量累积项 v_{t-1} 设置为0momentum = 0.9  # 动量系数 (momentum coefficient),控制之前梯度的影响程度,0表示无动量,1表示完全依赖历史
v_w = 0  # 权重 (w) 的动量累积项,初始化为0
v_b = 0  # 偏置 (b) 的动量累积项,初始化为0def SGD_Momentum(points, w, b, lr, batch_size=1):# 声明使用全局变量,以便在函数内部修改它们的值,并让修改在函数外部也生效global momentum, v_w, v_b# 第一步:打乱数据顺序# 随机打乱整个数据集的顺序。这对于SGD是至关重要的,# 可以避免模型陷入局部最优或在特定数据模式上过度拟合,增加训练的随机性np.random.shuffle(points)# print(points) # 调试用,可以注释掉# print("------------------------------") # 调试用,可以注释掉# 第二步:实现小批量 (batch) 迭代# 将数据集分割成小批量 (mini-batch) 进行训练。每次只用一部分数据计算梯度并更新参数。# 优点:相比全量梯度下降更快,相比纯SGD更稳定。# 例如:如果10个数据,batch_size=2,则分为5个批次 ([0,1], [2,3], ..., [8,9])# 如果batch_size=3,则分为4个批次 ([0,1,2], [3,4,5], [6,7,8], [9]),最后一个批次可能不足batch_sizefor num_batch in range(0, len(points), batch_size):# 提取当前批次的数据batch_points = points[num_batch:num_batch + batch_size, :]# print(batch_points) # 调试用,可以注释掉# 分离当前批次的特征 (batch_x) 和标签 (batch_y)batch_x = batch_points[:, 0]batch_y = batch_points[:, 1]# 计算当前批次的梯度# 首先计算当前批次数据点的预测值batch_pre_y = w * batch_x + b# 权重 w 的梯度 (dw):损失函数对 w 的偏导数# 对于 MSE = (y_pred - y_true)^2,对 w 求导:2 * (y_pred - y_true) * xdw = np.mean(2 * (batch_pre_y - batch_y) * batch_x)# 偏置 b 的梯度 (db):损失函数对 b 的偏导数# 对于 MSE = (y_pred - y_true)^2,对 b 求导:2 * (y_pred - y_true)db = np.mean(2 * (batch_pre_y - batch_y))# 更新参数# step1: 计算动量累积项 (v_w, v_b)# v_t = momentum * v_{t-1} + lr * g_tv_w = momentum * v_w + lr * dwv_b = momentum * v_b + lr * db# step2: 使用动量累积项更新参数# P_{t+1} = P_t - v_tw = w - v_wb = b - v_breturn w, b  # 返回更新后的权重和偏置# 调用示例 (已注释,用于说明函数用法,实际训练在主循环中)
# SGD_Momentum(points,3)# 6. 绘制和显示 (初始化绘图环境)
# 为损失函数的三维可视化构建网格点
w_values = np.linspace(-20, 80, 100)  # 生成 w 的取值范围,从 -20 到 80,共 100 个点
b_values = np.linspace(-20, 80, 100)  # 生成 b 的取值范围,从 -20 到 80,共 100 个点
W, B = np.meshgrid(w_values, b_values)  # 创建 w 和 b 的二维网格,用于绘制等高线图和三维表面图# 计算网格中每个 (w, b) 组合对应的损失值
# 将计算出的损失值存入与 W 和 B 形状相同的矩阵中
loss_values = np.zeros_like(W)  # 初始化一个与 W 相同形状的零矩阵,用于存储损失值
for i, w_val in enumerate(w_values):  # 遍历所有 w 值for j, b_val in enumerate(b_values):  # 遍历所有 b 值# 计算在当前 w_val 和 b_val 下的损失值,并存储到 loss_values 矩阵中# 注意:这里调用 loss_func(X, w_val, b_val)# 实际 loss_func 内部是使用全局变量 Y,因此这里计算的是针对全局 Y 的损失loss_values[j][i] = loss_func(X, w_val, b_val)  # 注意索引顺序,通常是 loss_values[行][列] 对应 B[行], W[列]# 构建 Matplotlib 图像对象
fig = plt.figure(figsize=(12, 6))  # 创建一个图表对象,并设置其大小 (宽12英寸,高6英寸)# 创建子图布局
# gridspec.GridSpec 将整个图像窗口切分成 2 行 2 列的网格
gs = gridspec.GridSpec(2, 2)
# 添加第一个子图:左上角的散点图和回归线 (用于显示数据和模型的拟合情况)
ax1 = fig.add_subplot(gs[0, 0])
# 添加第二个子图:左下角的损失函数等高线图 (用于显示参数空间中的损失值分布和梯度下降路径)
ax2 = fig.add_subplot(gs[1, 0])
# 添加第三个子图:整个右侧的损失函数三维表面图 (更直观地显示损失函数的形状)
ax3 = fig.add_subplot(gs[:, 1], projection="3d")  # projection="3d" 表示这是一个三维子图# 在三维子图上绘制损失函数的表面图
# cmap 是颜色映射 (colormap),'viridis' 是一种常用的颜色方案,用于给表面着色
# alpha 控制透明度,0.8 表示半透明
ax3.plot_surface(W, B, loss_values, cmap='viridis', alpha=0.8)# 存储梯度下降过程中的权重 (w) 和偏置 (b) 路径
gd_path = []
# 存储梯度下降过程中对应的损失值路径 (用于三维路径绘制)
gd_path_value = []# 5. 开始迭代训练
epoches = 1000  # 定义训练的迭代次数 (epochs)。一个 epoch 表示所有训练数据都被使用过一次
bs = 2  # 定义批次大小 (batch size)。每次梯度更新使用的样本数量for epoch in range(1, epoches + 1):  # 从1到epoches进行迭代# 调用 SGD_Momentum 优化器更新权重 w 和偏置 bw, b = SGD_Momentum(points, w, b, lr, batch_size=bs)# 将当前更新后的 (w, b) 对添加到梯度下降路径列表中gd_path.append((w, b))# 计算并存储当前 (w, b) 对应的损失值gd_path_value.append(loss_func(X, w, b))# 6. 设置显示频率# 每20个 epoch 或在第1个 epoch 时打印损失值并更新图表,以便观察训练过程if epoch == 1 or epoch % 20 == 0:print(f"Epoch {epoch:4d}, Loss: {loss_func(X, w, b):.4f}")  # 格式化输出当前 epoch 和损失值# 7. 绘制和显示实时训练过程# 绘制左上角的图 (ax1): 散点图和当前回归线ax1.cla()  # 清除当前子图的内容,以便绘制新的图像ax1.scatter(X, Y, c='r')  # 绘制原始数据点,颜色为红色# 绘制当前的线性回归预测线x_line = np.linspace(np.min(X), np.max(X))  # 生成用于绘制直线的 X 值范围y_line = np.dot(x_line, w) + b  # 根据当前 w 和 b 计算对应的 Y 值 (预测值)ax1.plot(x_line, y_line, c='g')  # 绘制绿色回归线ax1.set_title(f"Data & Regression Line (Epoch {epoch})")  # 设置标题显示当前 epochax1.set_xlabel("X (Feature)")ax1.set_ylabel("Y (Label)")plt.pause(0.01)  # 暂停0.01秒,以便看到动画效果,让图表有时间更新# 绘制左下角的图 (ax2): 损失函数等高线图和梯度下降路径ax2.cla()  # 清除当前子图的内容# 绘制损失函数的等高线图 (填充区域)ax2.contourf(W, B, loss_values, levels=50, cmap='viridis', alpha=0.8)  # 填充等高线区域# 绘制损失函数的等高线 (轮廓线)ax2.contour(W, B, loss_values, levels=50, colors='k', linewidths=0.5, alpha=0.5)# 绘制当前 (w, b) 点ax2.scatter(w, b, c="black", s=50, edgecolors='yellow', zorder=5)  # 用大黑点表示当前参数位置,zorder确保点在前面# 绘制梯度下降路径 (从开始到当前位置)gd_w, gd_b = zip(*gd_path)  # 将路径列表 (w,b) 对解压为 w 坐标元组和 b 坐标元组ax2.plot(gd_w, gd_b, c='black', linewidth=2)  # 绘制黑色路径ax2.set_xlabel("Weight (w)")ax2.set_ylabel("Bias (b)")ax2.set_title("Loss Contour with GD Path")plt.pause(0.01)# 绘制右侧图 (ax3): 损失函数三维表面图和梯度下降路径# 绘制当前 (w, b) 对应的损失值点 (在三维空间中)ax3.scatter(w, b, loss_func(X, w, b), c='red', s=50, marker='o', edgecolors='white', zorder=5)  # 用红点表示当前位置# 绘制梯度下降路径 (在三维空间中)ax3.plot(gd_w, gd_b, gd_path_value, c='black', linewidth=2)  # 绘制黑色路径ax3.set_xlabel("Weight (w)")ax3.set_ylabel("Bias (b)")ax3.set_zlabel("Loss")ax3.set_title("3D Loss Surface with GD Path")# 调整视角,让路径更清晰(可选,取消注释使用)# ax3.view_init(elev=30, azim=-60)plt.pause(0.01)# 显示最终的图表,直到用户手动关闭窗口
plt.show()

1.3 AdaGrad

AdaGrad(全称 Adaptive Gradient,自适应梯度)是一种梯度下降优化算法的扩展,旨在解决传统SGD和动量法中学习率固定且统一的问题。

1.3.1 核心思想与背景

  • 问题所在: SGD及动量法对所有参数使用相同的固定学习率,但这不适用于神经网络中更新频率差异大的参数。
    • 对于经常更新的参数,应采用较慢的学习率,避免单个样本影响过大。
    • 对于偶尔更新的参数,应采用较大的学习率,以便从稀有样本中学习更多信息。
  • AdaGrad的解决方案: 引入“二阶动量”,为每个参数适配不同的学习率,使其根据自身的梯度历史进行调整。

1.3.2 工作原理

  • AdaGrad在训练过程中,根据每个参数历史梯度的平方累积值来自动调整其学习率。
  • 更新方案:
    • 累计平方梯度 ( S t S_t St): S t = S t − 1 + g t ⋅ g t S_{t}=S_{t-1}+g_t\cdot g_t St=St1+gtgt
      • S t S_t St 是从开始到当前时刻 t t t 的所有历史梯度平方的累积和。
    • 参数更新 ( P t + 1 P_{t+1} Pt+1): P t + 1 = P t − α S t + ε ⋅ g t P_{t+1}=P_{t}-{\frac{\alpha}{\sqrt{S_{t}+\varepsilon}}}\cdot g_t Pt+1=PtSt+ε αgt
  • 参数定义:
    • α \alpha α:全局学习率。
    • g t g_t gt:在时刻 t t t 时参数(如 W t W_t Wt b t b_t bt)的损失梯度。
    • ε \varepsilon ε:一个极小值(通常为 10 − 7 10^{-7} 107),用于防止分母为零。

1.3.3 优点

  • 动态学习率调整: 训练前期学习率较高、更新快,后期自动衰减、更新慢,有助于稳定训练并加速收敛。
  • 参数特异性: 为不同更新频率的参数适配独立的学习率,有效减少震荡,在处理稀疏数据时表现尤为出色。

1.3.4 缺点

  • 学习率衰减过快: 历史梯度平方的累积和 ( S t S_t St) 会持续增大,导致分母不断变大,从而使得学习率过快地衰减,可能导致训练在尚未收敛时便过早停滞。

1.3.5 代码示例

# 导入相关库
import numpy as np  # 导入NumPy库,用于进行科学计算,特别是数组操作
import matplotlib.pyplot as plt  # 导入Matplotlib的pyplot模块,用于绘制图表
import matplotlib.gridspec as gridspec  # 导入gridspec模块,用于更灵活地布局Matplotlib子图# 1.散点输入
# 定义一个包含10个数据点的NumPy数组。
# 每个点是一个二维数组 [特征X, 标签Y],代表自变量和因变量。
points = np.array([[-0.5, 7.7], [1.8, 98.5], [0.9, 57.8], [0.4, 39.2], [-1.4, -15.7], [-1.4, -37.3], [-1.8, -49.1],[1.5, 75.6],[0.4, 34.0], [0.8, 62.3]])
# 分离特征和标签
X = points[:, 0]  # 从points数组中提取所有行的第一个元素,作为特征X
Y = points[:, 1]  # 从points数组中提取所有行的第二个元素,作为标签Y#2.参数初始化
w = 0  # 初始化模型权重w为0
b = -1  # 初始化模型偏置b为-1
lr = 0.5  # 设置学习率lr (learning rate),控制每次参数更新的步长#3.定义损失函数
def loss_func(X, w, b):# 定义前向传播 (Forward Pass):线性回归模型的预测值pre_y = np.dot(X, w) + b  # 计算预测值 pre_y = X * w + b# 计算均方误差 (Mean Squared Error, MSE) 作为损失loss = np.mean((Y - pre_y) ** 2)  # 损失 = 真实值Y 减去 预测值pre_y 的平方 的均值return loss#4.定义优化器
#这里使用AdaGrad优化器
# AdaGrad (Adaptive Gradient Algorithm) 是一种自适应学习率优化算法。
# St = St-1 + g(Wt)^2 (累积的历史梯度的平方)
# Wt+1 = Wt - lr / (sqrt(St + epsilon)) * gt (参数更新公式)
epsilon = 1e-7  # 定义一个小的常数epsilon,用于数值稳定性,防止分母为零
S_w = 0  # 初始化权重w的累积平方梯度S_w为0
S_b = 0  # 初始化偏置b的累积平方梯度S_b为0def AdaGrad(points, w, b, lr, batch_size=1):# 使用全局变量:由于S_w和S_b需要在函数调用之间保持累积状态,所以声明为全局变量global S_w, S_b# 第一步:打乱数据顺序,以便进行随机梯度下降或小批量梯度下降np.random.shuffle(points)  # 对原始数据点进行原地洗牌,没有返回值# print(points) # 用于调试,显示打乱后的数据# print("------------------------------")# 第二步:实现小批量梯度下降 (Mini-Batch Gradient Descent)# 迭代遍历数据,每次取batch_size个样本# 当batch_size=10时,一次取10个数据,完成一个epoch# 当batch_size=2时,一次取2个数据,取5次完成一个epoch# 当batch_size=3时,一次取3个数据,取3次,每次数据是3,3,3,1次完成一个epochfor num_batch in range(0, len(points), batch_size):# 提取当前批次的数据batch_points = points[num_batch:num_batch + batch_size, :]# print(batch_points) # 用于调试,显示当前批次数据# 分离当前批次的特征和标签batch_x = batch_points[:, 0]batch_y = batch_points[:, 1]# 计算当前批次的梯度batch_pre_y = w * batch_x + b  # 计算当前批次的预测值# 权重w的梯度 (dw)# 损失函数 L = (Y - (wX + b))^2# dL/dw = 2 * (Y - (wX + b)) * (-X) = -2 * (Y - pre_y) * X# 由于我们计算的是 (pre_y - Y)^2 的梯度,所以 dw = 2 * (pre_y - Y) * Xdw = np.mean(2 * (batch_pre_y - batch_y) * batch_x)# 偏置b的梯度 (db)# dL/db = 2 * (pre_y - Y) * 1db = np.mean(2 * (batch_pre_y - batch_y))# 更新参数# step1: 二阶动量计算 (累积梯度的平方)S_w = S_w + dw * dw  # 累积权重梯度的平方S_b = S_b + db * db  # 累积偏置梯度的平方# step2: 参数更新 (AdaGrad更新公式)# w = w - 学习率 / (sqrt(累积平方梯度 + epsilon)) * 梯度w = w - lr / np.sqrt(S_w + epsilon) * dwb = b - lr / np.sqrt(S_b + epsilon) * dbreturn w, b  # 返回更新后的权重w和偏置b# 调用一下 (这行代码被注释掉,因为它只是一个示例调用,实际训练在主循环中)
# SGD_Momentum(points,3) # 这是一个历史遗留的注释或错误,实际使用的是AdaGrad#6.绘制和显示
# 构建网格点,用于可视化损失函数的三维表面和等高线
w_values = np.linspace(-20, 80, 100)  # 在-20到80之间生成100个等间隔的w值
b_values = np.linspace(-20, 80, 100)  # 在-20到80之间生成100个等间隔的b值
W, B = np.meshgrid(w_values, b_values)  # 创建一个W和B的2D网格,用于在平面上表示(w, b)组合# 计算网格中每个点的损失值
# 将损失值存入和W一样的矩阵中
loss_values = np.zeros_like(W)  # 初始化一个与W形状相同的零矩阵来存储损失值
for i, w_val in enumerate(w_values):  # 遍历所有w值for j, b_val in enumerate(b_values):  # 遍历所有b值# 注意这里X是全局的原始数据,loss_func计算的是整个数据集的损失loss_values[j][i] = loss_func(X, w_val, b_val)  # 计算当前(w_val, b_val)组合下的损失# 构建图像的对象
fig = plt.figure(figsize=(12, 6))  # 创建一个尺寸为12x6英寸的Matplotlib图形对象
# 创建画布(子图布局)
# gridspec.GridSpec将整个图像窗口切分为2行2列的网格
gs = gridspec.GridSpec(2, 2)
# 左上角的格子:用于显示数据点和当前的回归直线
ax1 = fig.add_subplot(gs[0, 0])# 左下角的格子:用于显示损失函数的等高线图,以及优化路径
ax2 = fig.add_subplot(gs[1, 0])# 整个右侧格子:用于显示损失函数的三维表面图,以及优化路径
ax3 = fig.add_subplot(gs[:, 1], projection="3d")  # projection="3d" 表示这是一个3D子图
# cmap 是颜色映射 (Colormap),'viridis' 是一种常用的颜色方案
ax3.plot_surface(W, B, loss_values, cmap='viridis', alpha=0.8)  # 绘制3D损失表面,alpha设置透明度# 存储梯度下降路径 (optimization path)
gd_path = []  # 存储 (w, b) 对的列表
gd_path_value = []  # 存储对应 (w, b) 的损失值的列表#5.开始迭代训练
epoches = 1000  # 设置训练的总epoch数量 (完整遍历数据集的次数)
bs = 2  # 设置批次大小 (batch size) 为2,表示每次更新使用2个样本for epoch in range(1, epoches + 1):  # 从1开始迭代,直到epoches# 调用AdaGrad优化器更新权重w和偏置bw, b = AdaGrad(points, w, b, lr, batch_size=bs)# 将当前的(w, b)添加到路径列表中gd_path.append((w, b))# 将当前(w, b)对应的损失值添加到损失路径列表中gd_path_value.append(loss_func(X, w, b))# 6.设置显示频率:每隔20个epoch或在第一个epoch时更新并显示图表if epoch == 1 or epoch % 20 == 0:print(f"Epoch {epoch}, Loss: {loss_func(X, w, b):.4f}")  # 打印当前epoch和损失值# 7.绘制和显示实时更新的图# 绘制左上角的图 (数据点和回归直线)ax1.cla()  # 清除当前子图的内容,以便绘制新的内容ax1.scatter(X, Y, c='r')  # 绘制原始数据点,颜色为红色# 绘制绿色的回归直线x_line = np.linspace(np.min(X), np.max(X))  # 生成用于绘制直线的X值范围y_line = np.dot(x_line, w) + b  # 根据当前的w和b计算对应的Y值ax1.plot(x_line, y_line, c='g')  # 绘制直线,颜色为绿色ax1.set_title(f"Epoch: {epoch}, Loss: {loss_func(X, w, b):.2f}")  # 设置标题plt.pause(0.01)  # 暂停0.01秒,使动画效果可见# 绘制左下角的图 (损失函数的等高线图和优化路径)ax2.cla()  # 清除当前子图内容# 绘制等高线图ax2.contourf(W, B, loss_values, levels=50, cmap='viridis', alpha=0.8)  # 绘制填充的等高线图,levels设置等高线数量# 绘制当前(w, b)点ax2.scatter(w, b, c="black", s=20)  # 在等高线上绘制当前参数点,黑色,大小为20# 解包gd_path列表,将w和b值分别提取到gd_w和gd_b中gd_w, gd_b = zip(*gd_path)ax2.plot(gd_w, gd_b, c='black')  # 绘制优化路径,黑色线ax2.set_xlabel("Weight (w)")  # 设置X轴标签ax2.set_ylabel("Bias (b)")  # 设置Y轴标签ax2.set_title("Loss Contour & Path")  # 设置标题plt.pause(0.01)  # 暂停,使动画效果可见# 绘制右侧图 (损失函数的三维表面图和优化路径)# 绘制当前(w, b)点及其对应的损失值ax3.scatter(w, b, loss_func(X, w, b), c='black', s=20)  # 在3D表面上绘制当前参数点# 绘制3D优化路径ax3.plot(gd_w, gd_b, gd_path_value, c='black')  # 绘制3D路径ax3.set_xlabel("Weight (w)")  # 设置X轴标签ax3.set_ylabel("Bias (b)")  # 设置Y轴标签ax3.set_zlabel("Loss")  # 设置Z轴标签ax3.set_title("Loss Surface & Path")  # 设置标题plt.pause(0.01)  # 暂停,使动画效果可见# 显示最后的图,不退出
plt.show()  # 保持最终的图表显示,直到用户手动关闭窗口

1.4 RMSProp

RMSProp(Root Mean Square Propagation)是一种自适应学习率的优化算法。它由 Geoff Hinton 提出,旨在解决 Adagrad 优化器学习率递减过快的问题。

RMSProp 的核心思想是在更新参数时,利用梯度平方的指数加权移动平均来动态调整学习率。通过引入一个衰减系数( η \eta η),它能够更灵活地控制历史梯度信息对当前学习率的影响,从而克服了 Adagrad 中学习率衰减过快的问题。

1.4.1 更新方案

  1. 梯度平方的指数加权移动平均 ( S t S_t St):
    S t = η ⋅ S t − 1 + ( 1 − η ) ⋅ g t ⋅ g t S_{t}=\eta \cdot S_{t-1}+(1-\eta)\cdot{g_t}\cdot g_t St=ηSt1+(1η)gtgt

  2. 参数更新 ( P t + 1 P_{t+1} Pt+1):
    P t + 1 = P t − α S t + ε ⋅ g t P_{t+1}=P_{t}-{\frac{\alpha}{\sqrt{S_{t}+\varepsilon}}}\cdot g_t Pt+1=PtSt+ε αgt

其中:

  • α \alpha α:学习率(learning rate)。
  • g t g_t gt:在 t t t 时刻,参数 W t W_t Wt b t b_t bt 的损失梯度。
  • ε \varepsilon ε:一个极小的常数,用于防止分母为零,一般取值为 10 − 7 10^{-7} 107
  • η \eta η:控制衰减速度的参数,通常设置为 0.9 0.9 0.9

1.4.2 指数加权移动平均(Exponential Moving Average,EMA)

在 RMSProp 中, S t S_t St 的计算采用了指数加权移动平均(EMA)

  • 定义: EMA 是一种对时间序列数据进行平滑处理的方法。它赋予过去的观测值不同的权重,且权重随时间呈指数递减。这意味着离当前时刻越近的观测值,其权重越大。
  • 在 RMSProp 中的应用: S t S_t St 实际上是过去所有梯度平方的加权平均,其中最近的梯度平方权重最大,历史梯度平方的权重随时间呈指数衰减。
    例如,当 η = 0.9 \eta=0.9 η=0.9 时:
    S 100 = 0.1 ⋅ g 100 2 + 0.1 ⋅ 0.9 ⋅ g 99 2 + 0.1 ⋅ 0.9 2 ⋅ g 98 2 . . . + 0.1 ⋅ 0.9 99 ⋅ g 1 2 S_{100}=0.1\cdot g_{100}^2 + 0.1\cdot 0.9 \cdot g_{99}^2+ 0.1\cdot 0.9^2 \cdot g_{98}^2 ... +0.1\cdot 0.9^{99} \cdot g_1^2 S100=0.1g1002+0.10.9g992+0.10.92g982...+0.10.999g12
    这表明 S 100 S_{100} S100 的值主要受最近几个梯度的平方影响,而更早的梯度影响则逐渐减小。

1.4.3 优点

  • 克服 Adagrad 缺陷: 解决了 Adagrad 优化器中学习率递减过快的问题。
  • 加速收敛: 通过动态调整学习率,有助于模型更快地收敛。
  • 稳定性与易调参: 相较于 Adagrad,RMSProp 更稳定,并且通常更容易调整参数。

1.4.4 缺点

  • 未利用梯度方向信息: 与 Adagrad 类似,RMSProp 仅基于梯度的幅值来调整学习率,没有利用梯度方向信息,这可能限制其在某些复杂场景下的适应性。

1.4.5 代码示例

# 导入相关库
import numpy as np  # 导入NumPy库,用于进行科学计算和数组操作
import matplotlib.pyplot as plt  # 导入Matplotlib的pyplot模块,用于绘制图表
import matplotlib.gridspec as gridspec  # 导入GridSpec模块,用于更灵活地布局子图# 1.散点输入
# 定义一组二维数据点,每个点包含一个特征X和一个标签Y
points = np.array([[-0.5, 7.7], [1.8, 98.5], [0.9, 57.8], [0.4, 39.2], [-1.4, -15.7], [-1.4, -37.3], [-1.8, -49.1],[1.5, 75.6],[0.4, 34.0], [0.8, 62.3]])
# 分离特征和标签
X = points[:, 0]  # 所有点的第一个维度作为特征X
Y = points[:, 1]  # 所有点的第二个维度作为标签Y#2.参数初始化
# 初始化模型的权重w和偏置b,这是线性回归模型的参数
w = 0  # 权重(斜率)
b = -1  # 偏置(截距)
lr = 0.1  # 学习率(learning rate),控制每次参数更新的步长#3.定义损失函数
def loss_func(X, w, b):# 定义前向传播 (forward propagation),计算模型预测值# 对于线性回归,预测值 pre_y = X * w + bpre_y = np.dot(X, w) + b# 计算均方误差 (Mean Squared Error, MSE) 作为损失函数# MSE = 平均((实际值 - 预测值)^2)loss = np.mean((Y - pre_y) ** 2)return loss#4.定义优化器
# 这里使用RMSProp优化器
# RMSProp是一种自适应学习率优化算法,它维护一个按元素平方梯度加权移动平均值。
# St = η * St-1 + (1 - η) * g(Wt) * g(Wt)
# Wt+1 = Wt - lr / (sqrt(St + epsilon)) * gt
epsilon = 1e-7  # 一个非常小的常数,用于防止除以零,增加数值稳定性
# 定义η (eta),即衰减率 (decay rate),控制历史梯度信息对当前梯度的影响权重
decay_rate = 0.9
S_w = 0  # 权重w的平方梯度移动平均初始化
S_b = 0  # 偏置b的平方梯度移动平均初始化def RMSProp(points, w, b, lr, batch_size=1):# 使用global关键字声明变量,表示函数内部修改的是全局变量,而不是创建局部变量global S_w, S_b, decay_rate, epsilon# 第一步:打乱数据顺序# 在每个epoch开始时打乱整个数据集的顺序,有助于防止模型陷入局部最优,提高训练的随机性np.random.shuffle(points)# print(points)# print("------------------------------")# 第二步:引入batch_size的概念,实现小批量梯度下降# 遍历数据集,每次取batch_size个数据进行训练(一个batch)# 当batch_size=len(points)时,是批量梯度下降 (Batch Gradient Descent)# 当batch_size=1时,是随机梯度下降 (Stochastic Gradient Descent)# 当batch_size介于1和len(points)之间时,是小批量梯度下降 (Mini-batch Gradient Descent)for num_batch in range(0, len(points), batch_size):# 提取当前批次的数据batch_points = points[num_batch:num_batch + batch_size, :]# print(batch_points)# 分离当前批次的特征和标签batch_x = batch_points[:, 0]batch_y = batch_points[:, 1]# 计算当前批次的梯度# 根据损失函数 L = (Y - (wX + b))^2# dL/dw = 2 * (wX + b - Y) * X# dL/db = 2 * (wX + b - Y)batch_pre_y = w * batch_x + b  # 当前批次的预测值dw = np.mean(2 * (batch_pre_y - batch_y) * batch_x)  # 权重w的梯度db = np.mean(2 * (batch_pre_y - batch_y))  # 偏置b的梯度# 更新参数 (RMSProp更新规则)# step1: 计算二阶动量 (squared gradients moving average)# S_w和S_b是梯度的平方的指数加权移动平均,用于自适应地调整学习率S_w = decay_rate * S_w + (1 - decay_rate) * dw * dwS_b = decay_rate * S_b + (1 - decay_rate) * db * db# step2: 参数更新# 参数更新公式:参数 = 参数 - 学习率 / (sqrt(二阶动量 + epsilon)) * 梯度# 这里的学习率是自适应的,对于梯度大的参数,分母大,更新步长小;对于梯度小的参数,分母小,更新步长大。w = w - lr / np.sqrt(S_w + epsilon) * dwb = b - lr / np.sqrt(S_b + epsilon) * dbreturn w, b# 调用一下 (这行代码在实际运行时被注释掉,只是一个示例)
# RMSProp(points,3)#6.绘制和显示初始化
# 构建网格点,用于绘制损失函数的三维表面和等高线图
w_values = np.linspace(-20, 80, 100)  # w的取值范围,100个点
b_values = np.linspace(-20, 80, 100)  # b的取值范围,100个点
W, B = np.meshgrid(w_values, b_values)  # 创建w和b的二维网格# 计算网格中每个点对应的损失值
# 将损失值存入和W、B一样大小的矩阵中
loss_values = np.zeros_like(W)  # 初始化一个与W形状相同的零矩阵来存储损失值
for i, w_val in enumerate(w_values):for j, b_val in enumerate(b_values):# 注意:这里计算loss_values时,使用的是全局的X和Y,而不是局部变量,这假设了loss_func在网格计算时需要访问这些全局数据loss_values[j][i] = loss_func(X, w_val, b_val)
# 构建图像对象
fig = plt.figure(figsize=(12, 6))  # 创建一个大小为12x6英寸的图表# 创建画布,使用gridspec.GridSpec将整个图像窗口切分为2行2列的网格
gs = gridspec.GridSpec(2, 2)
# 左上角的格子,用于显示数据点和当前拟合直线
ax1 = fig.add_subplot(gs[0, 0])
# 左下角的格子,用于显示损失函数的等高线图和优化路径的2D投影
ax2 = fig.add_subplot(gs[1, 0])
# 整个右侧格子(跨越两行),用于显示损失函数的三维表面和优化路径的3D投影
ax3 = fig.add_subplot(gs[:, 1], projection="3d")
# 绘制3D损失函数表面
# cmap是颜色映射,alpha是透明度
ax3.plot_surface(W, B, loss_values, cmap='viridis', alpha=0.8)# 存储梯度下降路径 (w, b) 坐标和对应的损失值
gd_path = []  # 存储 (w, b) 元组的列表
gd_path_value = []  # 存储对应损失值的列表#5.开始迭代训练
epoches = 1000  # 训练的总迭代次数(或称“轮次”)
bs = 2  # 每次迭代处理的批次大小 (batch size)
for epoch in range(1, epoches + 1):# 调用RMSProp优化器更新权重w和偏置bw, b = RMSProp(points, w, b, lr, batch_size=bs)# 将当前epoch的(w, b)和对应的损失值添加到路径列表中gd_path.append((w, b))gd_path_value.append(loss_func(X, w, b))# 6.设置显示频率# 每1个epoch(首次)或每20个epoch打印一次损失值并更新可视化图表if epoch == 1 or epoch % 20 == 0:print(f"Epoch {epoch}: Loss = {loss_func(X, w, b):.4f}")  # 打印当前损失值# 7.绘制和显示动态图表# 绘制左上角的图 (数据点与拟合直线)ax1.cla()  # 清除当前子图的内容ax1.scatter(X, Y, c='r', label='Data Points')  # 绘制原始数据点,红色# 绘制当前的拟合直线x_line = np.linspace(np.min(X), np.max(X))  # 生成用于绘制直线的X值范围y_line = np.dot(x_line, w) + b  # 根据当前的w和b计算对应的Y值ax1.plot(x_line, y_line, c='g', label=f'Predicted Line (Epoch {epoch})')  # 绘制绿色拟合直线ax1.set_title(f'Data & Regression Line (Epoch {epoch})')ax1.legend()plt.pause(0.01)  # 暂停一段时间,以便观察动态变化 (可以根据需要调整暂停时间)# 绘制左下角的图 (损失函数等高线图和优化路径的2D投影)ax2.cla()  # 清除当前子图的内容# 绘制损失函数的等高线图ax2.contourf(W, B, loss_values, levels=50, cmap='viridis', alpha=0.8)# 在等高线图上标记当前的 (w, b) 位置ax2.scatter(w, b, c="black", s=50, zorder=5, label='Current (w,b)')  # zorder确保点在等高线上方# 提取优化路径中的w和b分量gd_w, gd_b = zip(*gd_path)# 绘制优化路径ax2.plot(gd_w, gd_b, c='black', label='Optimization Path')ax2.set_title('Loss Contour & Optimization Path (2D)')ax2.set_xlabel('Weight (w)')ax2.set_ylabel('Bias (b)')ax2.legend()plt.pause(0.01)  # 暂停一段时间# 绘制右侧的图 (损失函数三维表面和优化路径的3D投影)# 标记当前 (w, b, loss) 位置ax3.scatter(w, b, loss_func(X, w, b), c='black', s=50, label='Current (w,b,Loss)')# 绘制优化路径的3D轨迹ax3.plot(gd_w, gd_b, gd_path_value, c='black', label='Optimization Path')ax3.set_title('Loss Surface & Optimization Path (3D)')ax3.set_xlabel('Weight (w)')ax3.set_ylabel('Bias (b)')ax3.set_zlabel('Loss')ax3.legend()plt.pause(0.01)  # 暂停一段时间# 训练结束后,显示最后的图表,并保持窗口不退出
plt.tight_layout()  # 调整子图布局,防止重叠
plt.show()

1.5 Adam 优化器详解

Adam (Adaptive Moment Estimation) 是一种在深度学习中广泛使用的优化器。它结合了动量(Momentum)和自适应学习率(如 RMSProp)的优势,通过同时跟踪梯度的一阶矩(均值,控制更新方向)和二阶矩(方差,控制更新步长)来实现参数的自适应更新。

核心特点:

  • 收敛速度快
  • 鲁棒性强
  • 调参简单
  • 广泛适用于各类神经网络模型
  • 能够处理噪声及非平稳数据
  • 适合大型模型训练

1.5.1 矩估计引入

Adam 引入了两个衰减率参数 β 1 \beta_1 β1 β 2 \beta_2 β2,用于计算梯度的一阶矩和二阶矩的指数加权移动平均。

  • 一阶矩估计 ( m t m_t mt): 对梯度的指数加权移动平均,反映梯度的均值和方向。
    m t = β 1 ⋅ m t − 1 + ( 1 − β 1 ) ⋅ g ( t ) m_{t}=\beta_{1}\cdot m_{t-1}+(1-\beta_{1})\cdot g(t) mt=β1mt1+(1β1)g(t)
    其中, m t m_t mt 是时刻 t t t 的一阶矩估计, g ( t ) g(t) g(t) 是时刻 t t t 的梯度, β 1 \beta_1 β1 是梯度的衰减率。

  • 二阶矩估计 ( v t v_t vt): 对梯度平方的指数加权移动平均,反映梯度的方差和大小。
    v t = β 2 ⋅ v t − 1 + ( 1 − β 2 ) ⋅ g ( t ) ⋅ g ( t ) v_{t}=\beta_{2}\cdot v_{t-1}+(1-\beta_{2})\cdot g(t)\cdot g(t) vt=β2vt1+(1β2)g(t)g(t)
    其中, v t v_t vt 是时刻 t t t 的二阶矩估计, g ( t ) g(t) g(t) 是时刻 t t t 的梯度, β 2 \beta_2 β2 是梯度平方的衰减率。

常用参数:

  • β 1 = 0.9 \beta_1 = 0.9 β1=0.9
  • β 2 = 0.999 \beta_2 = 0.999 β2=0.999

1.5.2 矩估计的初始偏差问题

在训练初期,由于一阶矩 m 0 m_0 m0 和二阶矩 v 0 v_0 v0 通常初始化为 0,这会导致早期的矩估计值会显著偏小,从而低估了真实的梯度均值和方差。

示例(以二阶矩为例):
初始化 v 0 = 0 v_0=0 v0=0
v 1 = β 2 ⋅ v 0 + ( 1 − β 2 ) ⋅ g 1 2 = ( 1 − β 2 ) ⋅ g 1 2 v_1 = \beta_2 \cdot v_0 + (1-\beta_2) \cdot g_1^2 = (1-\beta_2) \cdot g_1^2 v1=β2v0+(1β2)g12=(1β2)g12
例如,若 β 2 = 0.999 \beta_2=0.999 β2=0.999,则 v 1 = 0.001 ⋅ g 1 2 v_1 = 0.001 \cdot g_1^2 v1=0.001g12。这显然远小于 g 1 2 g_1^2 g12,导致估计不准确。随着迭代次数增加,这种低估现象会逐渐减弱。

1.5.3 偏差校正引入

为了解决初期矩估计的偏差问题,Adam 引入了偏差校正(Bias Correction)。这种校正可以确保在训练初期,矩估计的期望值更接近真实值,从而提供更准确的自适应学习率。

校正公式:

  • 一阶矩估计的偏差修正:
    m t ^ = m t 1 − β 1 t \widehat{m_{t}}=\frac{m_{t}}{1-\beta_{1}^{t}} mt =1β1tmt
  • 二阶矩估计的偏差修正:
    v t ^ = v t 1 − β 2 t \widehat{v_{t}}=\frac{v_{t}}{1-\beta_{2}^{t}} vt =1β2tvt

原理:
t t t 较小时,分母 ( 1 − β t ) (1-\beta^t) (1βt) 会小于 1,从而对 m t m_t mt v t v_t vt 进行放大,修正了它们由于初始值过小而导致的低估。随着迭代次数 t t t 的增加, β t \beta^t βt 趋近于 0,分母 ( 1 − β t ) (1-\beta^t) (1βt) 趋近于 1,此时偏差校正的影响逐渐减弱,因为此时的矩估计值已经足够准确。

1.5.4 Adam 更新方案

经过偏差校正后的矩估计值 m t ^ \widehat{m_t} mt v t ^ \widehat{v_t} vt 被用于更新模型的权重 W W W

W t + 1 = W t − α v ^ t + ϵ ⋅ m ^ t W_{t+1}=W_{t}-{\frac{\alpha}{\sqrt{\widehat{v}_{t}}+\epsilon}}\cdot\widehat{m}_{t} Wt+1=Wtv t +ϵαm t

其中:

  • W t + 1 W_{t+1} Wt+1 是更新后的权重。
  • W t W_t Wt 是当前权重。
  • α \alpha α 是学习率。
  • ϵ \epsilon ϵ 是一个极小的常数(通常为 10 − 7 10^{-7} 107),用于防止分母为零,增加数值稳定性。

1.5.5 代码示例

# 导入相关库
import numpy as np  # 用于数值计算,特别是数组操作
import matplotlib.pyplot as plt  # 用于绘制图表
import matplotlib.gridspec as gridspec  # 用于创建复杂的子图布局# 1. 散点输入
# 定义训练数据点,每个点是一个二维数组 [特征X, 标签Y]
points = np.array([[-0.5, 7.7], [1.8, 98.5], [0.9, 57.8], [0.4, 39.2], [-1.4, -15.7], [-1.4, -37.3], [-1.8, -49.1],[1.5, 75.6],[0.4, 34.0], [0.8, 62.3]])
# 分离特征和标签
X = points[:, 0]  # 所有行的第0列作为特征X
Y = points[:, 1]  # 所有行的第1列作为标签Y# 2. 参数初始化
# 初始化模型的权重(w)和偏置(b),这是我们在线性回归中需要学习的参数
w = 0  # 初始权重
b = -1  # 初始偏置
lr = 0.1  # 学习率(Learning Rate),控制每次参数更新的步长# 3. 定义损失函数
def loss_func(X, w, b):"""定义均方误差 (Mean Squared Error, MSE) 损失函数。它衡量模型预测值与真实值之间的差异。"""# 定义前向传播:线性模型预测值 pre_y = w * X + bpre_y = np.dot(X, w) + b# 计算均方误差:(真实值 - 预测值)^2 的平均值loss = np.mean((Y - pre_y) ** 2)return loss# 4. 定义优化器
# 这里使用Adam优化器
# Adam (Adaptive Moment Estimation) 是一种结合了动量 (momentum) 和 RMSProp 的优化算法。
# 它维护了梯度的一阶矩(均值)和二阶矩(非中心方差)的指数加权移动平均。
#
# Adam的更新公式大致如下:
# m_t = beta1 * m_{t-1} + (1 - beta1) * g_t  (梯度的一阶矩估计,即梯度的指数加权平均)
# v_t = beta2 * v_{t-1} + (1 - beta2) * g_t^2 (梯度的二阶矩估计,即梯度的平方的指数加权平均)
#
# 偏差修正 (Bias Correction):由于m_t和v_t在初始阶段会偏向0,需要进行修正。
# m_hat_t = m_t / (1 - beta1^t)
# v_hat_t = v_t / (1 - beta2^t)
#
# 参数更新:
# theta_{t+1} = theta_t - lr / (sqrt(v_hat_t) + epsilon) * m_hat_tepsilon = 1e-7  # 一个非常小的常数,用于防止除以零(在Adam分母中)# 一阶矩估计 (m) 和二阶矩估计 (v) 的变量
# 它们需要是全局变量,因为Adam优化器在每次迭代中都会更新它们,并在下一次迭代中使用
m_w = 0  # 权重w梯度的一阶矩估计
m_b = 0  # 偏置b梯度的一阶矩估计
v_w = 0  # 权重w梯度的二阶矩估计
v_b = 0  # 偏置b梯度的二阶矩估计# Adam优化器的超参数
beta1 = 0.9  # 控制一阶矩估计的衰减率
beta2 = 0.999  # 控制二阶矩估计的衰减率def Adam(points, w, b, t, lr, batch_size=1):"""Adam优化器函数。Args:points (np.array): 训练数据集。w (float): 当前权重。b (float): 当前偏置。t (int): 当前迭代次数(用于偏差修正)。lr (float): 学习率。batch_size (int): 每次迭代使用的样本数量(批大小)。Returns:tuple: 更新后的 (w, b)。"""# 声明这些变量是全局变量,以便在函数内部修改它们的值,而不是创建局部变量global m_w, m_b, v_w, v_b, beta1, beta2, epsilon# 第一步:打乱数据顺序# 随机打乱数据集的行,这对于SGD/Mini-batch SGD非常重要,可以避免训练过程陷入局部最优或震荡。np.random.shuffle(points)# 第二步: mini-batch 的概念# 遍历数据集,每次取一个批次 (batch_size) 的数据进行训练。# 当batch_size等于数据总数时,就是批量梯度下降 (Batch Gradient Descent)。# 当batch_size等于1时,就是随机梯度下降 (Stochastic Gradient Descent)。# 介于两者之间是 Mini-batch 梯度下降。for num_batch in range(0, len(points), batch_size):# 提取当前批次的数据batch_points = points[num_batch:num_batch + batch_size, :]# 分离当前批次的特征和标签batch_x = batch_points[:, 0]batch_y = batch_points[:, 1]# 计算梯度# 对当前批次数据进行前向传播batch_pre_y = w * batch_x + b# 计算损失函数对权重 w 的梯度 (dw)# 损失函数 L = 1/N * sum((w*x + b - y)^2)# dL/dw = 1/N * sum(2 * (w*x + b - y) * x)dw = np.mean(2 * (batch_pre_y - batch_y) * batch_x)# 计算损失函数对偏置 b 的梯度 (db)# dL/db = 1/N * sum(2 * (w*x + b - y) * 1)db = np.mean(2 * (batch_pre_y - batch_y))# 更新Adam参数# step1: 计算一阶矩估计 (梯度的指数加权平均)m_w = beta1 * m_w + (1 - beta1) * dwm_b = beta1 * m_b + (1 - beta1) * db# step2: 计算二阶矩估计 (梯度平方的指数加权平均)v_w = beta2 * v_w + (1 - beta2) * dw * dwv_b = beta2 * v_b + (1 - beta2) * db * db# step3: 偏差修正 (Bias Correction)# 为了抵消m_w, m_b, v_w, v_b在训练初期因m0, v0为零而产生的偏差m_w_hat = m_w / (1 - beta1 ** t)m_b_hat = m_b / (1 - beta1 ** t)v_w_hat = v_w / (1 - beta2 ** t)v_b_hat = v_b / (1 - beta2 ** t)# step4: 参数更新# w = w - learning_rate * m_hat / (sqrt(v_hat) + epsilon)w -= lr / (np.sqrt(v_w_hat) + epsilon) * m_w_hatb -= lr / (np.sqrt(v_b_hat) + epsilon) * m_b_hatreturn w, b# 6. 绘制和显示准备
# 构建网格点,用于绘制损失函数的三维表面图和等高线图
w_values = np.linspace(-20, 80, 100)  # 在-20到80之间生成100个w值
b_values = np.linspace(-20, 80, 100)  # 在-20到80之间生成100个b值
# 创建二维网格,W和B分别存储了w_values和b_values的网格化版本
W, B = np.meshgrid(w_values, b_values)# 计算网格中每个 (w, b) 组合对应的损失值
# 将损失值存入和W一样大小的矩阵中
loss_values = np.zeros_like(W)
for i, w_val in enumerate(w_values):for j, b_val in enumerate(b_values):# 注意:这里loss_func需要使用全局的X和Y,计算的是整个数据集的损失# 且loss_func参数需要当前网格点的w_val和b_valloss_values[j][i] = loss_func(X, w_val, b_val)# 构建图像的对象
fig = plt.figure(figsize=(12, 6))  # 创建一个尺寸为12x6英寸的图表# 创建画布布局
# gridspec.GridSpec 将整个图像窗口切分为一个2x2的网格
gs = gridspec.GridSpec(2, 2)# 定义各个子图的位置
ax1 = fig.add_subplot(gs[0, 0])  # 左上角的格子:用于显示数据点和回归线
ax2 = fig.add_subplot(gs[1, 0])  # 左下角的格子:用于显示损失函数的等高线图和优化路径
ax3 = fig.add_subplot(gs[:, 1], projection="3d")  # 整个右侧格子:用于显示损失函数的三维表面图和优化路径# 绘制3D损失表面图
# cmap='viridis' 设置颜色映射,alpha=0.8 设置透明度
ax3.plot_surface(W, B, loss_values, cmap='viridis', alpha=0.8)
ax3.set_xlabel('Weight (w)')  # 设置X轴标签
ax3.set_ylabel('Bias (b)')  # 设置Y轴标签
ax3.set_zlabel('Loss')  # 设置Z轴标签
ax3.set_title('3D Loss Surface')  # 设置标题# 存储梯度下降路径 (w, b) 和对应的损失值,用于可视化优化过程
gd_path = []  # 存储每次迭代的 (w, b) 参数对
gd_path_value = []  # 存储每次迭代的损失值# 5. 开始迭代(训练过程)
epoches = 1000  # 训练的总轮数(一个epoch表示遍历整个数据集一次)
bs = 2  # 批大小 (Batch Size),每次更新参数使用的样本数量for epoch in range(1, epoches + 1):# 调用Adam优化器更新权重 w 和偏置 b# 将当前的epoch作为t传入Adam,用于偏差修正w, b = Adam(points, w, b, epoch, lr, batch_size=bs)# 记录当前的 (w, b) 和损失值,以便后续绘制优化路径gd_path.append((w, b))gd_path_value.append(loss_func(X, w, b))  # 使用当前w, b和原始X计算损失# 6. 设置显示频率# 每隔20个epoch或者在第1个epoch时进行打印和绘图更新,以便观察训练过程if epoch == 1 or epoch % 20 == 0:current_loss = loss_func(X, w, b)print(f"Epoch {epoch}: Loss = {current_loss:.4f}, w = {w:.4f}, b = {b:.4f}")# 7. 绘制和显示训练过程的可视化# 绘制左上角的图 (ax1: 数据点和回归线)ax1.cla()  # 清除当前轴的内容,以便重新绘制ax1.scatter(X, Y, c='r', label='Original Data')  # 绘制原始数据点,红色# 绘制当前学习到的回归直线x_line = np.linspace(np.min(X) - 0.5, np.max(X) + 0.5, 100)  # 生成用于绘制直线的X值范围y_line = np.dot(x_line, w) + b  # 计算对应的Y值ax1.plot(x_line, y_line, c='g', label='Regression Line')  # 绘制绿色回归直线ax1.set_title(f'Epoch {epoch} - Line Fit')  # 设置标题ax1.set_xlabel('X')ax1.set_ylabel('Y')ax1.legend()  # 显示图例# 绘制左下角的图 (ax2: 损失等高线图和优化路径)ax2.cla()  # 清除当前轴的内容# 绘制损失函数的等高线图ax2.contourf(W, B, loss_values, levels=50, cmap='viridis', alpha=0.8)  # 填充等高线# 绘制优化器当前的 (w, b) 点ax2.scatter(w, b, c="black", s=50, marker='*', label='Current (w,b)')  # 当前点,黑色星形# 提取并绘制优化路径gd_w, gd_b = zip(*gd_path)  # 将 (w, b) 对列表解包成两个单独的列表ax2.plot(gd_w, gd_b, c='red', linewidth=1, label='Optimization Path')  # 绘制优化路径,红色ax2.set_title('Loss Contour & Path')  # 设置标题ax2.set_xlabel('Weight (w)')ax2.set_ylabel('Bias (b)')ax2.legend()# 绘制右侧的图 (ax3: 3D损失表面图和优化路径)# 由于3D图每次都会重新绘制整个表面会很慢,我们只更新路径点和当前点# 这里需要重新绘制表面,否则路径会消失ax3.cla()  # 清除当前3D轴的内容ax3.plot_surface(W, B, loss_values, cmap='viridis', alpha=0.8)  # 重新绘制3D表面ax3.set_xlabel('Weight (w)')ax3.set_ylabel('Bias (b)')ax3.set_zlabel('Loss')ax3.set_title('3D Loss Surface & Path')# 绘制当前 (w, b) 对应的损失点ax3.scatter(w, b, current_loss, c='black', s=50, marker='*', label='Current (w,b)')  # 当前点,黑色星形# 绘制优化路径# 注意:为了在3D图中绘制线,需要将w, b, loss_value列表传入ax3.plot(gd_w, gd_b, gd_path_value, c='red', linewidth=1, label='Optimization Path')  # 绘制优化路径,红色ax3.legend()plt.tight_layout()  # 调整子图布局,防止重叠plt.pause(0.1)  # 暂停0.1秒,以便观察动画效果。如果希望动画更快或更慢,可以调整这个值。# 训练结束后,显示最终的图表,不退出程序
plt.show()

二、神经网络的可解释性

1.1 定义与挑战

  • 定义: 神经网络的可解释性是指理解神经网络中每个决策或预测是如何产生的。
  • 挑战: 神经网络通常被视为“黑箱模型”,其内部决策过程复杂且难以直接理解,这使得对预测结果的解释成为一个重要问题。

1.2 重要性与作用

  • 理解与发现: 有助于深入理解神经网络的行为模式,并发现网络中可能存在的问题。
  • 提升可靠性与可信度: 改进网络的性能、可靠性与可信度。
  • 建立用户信任: 帮助构建用户信任的神经网络系统,增强用户对系统预测结果的信心,从而推动其在实际应用中的落地和推广。

1.3 提升可解释性的常用方法

  • 全连接层(Fully Connected Layer): 在输出层前加入,辅助模型解析和呈现决策逻辑。
  • Dropout 层: 通过随机失活神经元抑制过拟合,间接增强模型的泛化能力,有助于更稳健地理解特征作用。
  • 归一化层(Normalization Layer): 标准化输入数据分布,缓解梯度消失/爆炸问题,并有助于提升模型对特征的学习效率和可解释性。
  • 注意力机制(Attention Mechanism): 使模型能够聚焦于输入中的关键特征,直观地展现输入与预测结果之间的关联权重,是提升可解释性的直接且有效方法。

神经网络的可解释性对其应用至关重要,只有具备可解释性,才能增强人们对预测结果的信任并推动实际应用。目前神经网络发展尚不能解释,有待读者的继续完善。


好的,以下是对“欠拟合”概念的整理:


三、欠拟合(Underfitting)

定义:
欠拟合指模型在训练集上未能有效学习数据中的复杂关系与模式,导致模型对训练数据和新数据(测试数据)的预测能力都较差。其典型表现是训练误差和测试误差均较高

常见原因:

  1. 数据量不足: 训练数据样本过少,模型无法从中学习到足够丰富和具有代表性的特征与模式。
  2. 模型复杂度过低: 模型结构过于简单(例如使用线性模型去拟合非线性数据),无法捕捉数据中固有的复杂关系。
  3. 正则化过度: 施加了过强的正则化(如L1/L2正则化),过度限制了模型参数的取值范围,迫使模型过于简化,从而忽略了数据中的重要特征。

示例:
模型因未能充分学习数据特征或模式,导致其识别、分类或预测结果不准确,是欠拟合的典型表现。例如,在图像识别中,模型无法准确识别不同类别的物体,因为它没有学到足够的区分性特征。
在这里插入图片描述

解决方案:
欠拟合相对来说比较容易解决,核心思路是增强模型的学习能力或提供更多有效信息。常见方法包括:

  • 增加数据量: 收集更多训练数据,或者通过数据增强(Data Augmentation)技术扩充现有数据集。
  • 提高模型复杂度: 采用更复杂的模型结构,例如使用非线性模型、增加神经网络的层数或神经元数量,以提高模型的拟合能力。
  • 减少正则化强度: 如果模型存在过度正则化,则应降低正则化参数,允许模型学习更多特征。
  • 选择更合适的特征: 确保输入模型的特征是有效且充分的,有时需要进行特征工程来提取更有区分度的特征。

代码示例:

# 神经网络可解释性和欠拟合 (Neural Network Interpretability and Underfitting)
# 这个例子展示了一个简单的神经网络如何对二维数据进行分类,并通过动态绘图可视化决策边界。
# 通过观察决策边界的变化,可以初步理解神经网络的学习过程以及可能的欠拟合(如果模型过于简单或训练不足)现象。import numpy as np  # 导入NumPy库,用于进行数值计算,特别是处理数组数据。
import torch  # 导入PyTorch库,用于构建和训练神经网络。
import torch.nn as nn  # 导入PyTorch神经网络模块,包含各种神经网络层(如全连接层)。
import torch.optim as optim  # 导入PyTorch优化器模块,包含各种优化算法(如Adam)。
import matplotlib.pyplot as plt  # 导入Matplotlib库,用于绘图和数据可视化。# 1. 散点输入数据准备
# 定义类别1的散点数据。这些点代表一个分类。
class1_points = np.array([[-2.8, 0.1], [0.4, 2.8], [-0.1, 1.9], [-2.0, 1.5], [-0.4, 0.4], [2.1, -0.0],[-0.8, 0.6], [0.4, -2.5], [0.1, -2.0], [-0.6, 1.6], [-1.5, 2.1], [-0.9, 1.2],[1.3, 1.7], [0.2, -0.0], [0.2, -0.1], [-2.1, -0.8], [-0.7, 1.6], [1.4, -1.2],[-0.8, -1.6], [-1.3, 1.1], [-1.2, -0.1], [-2.9, -0.4], [2.4, -1.2], [-2.7, -1.3],[-1.1, -1.9], [1.4, 0.5], [-1.0, 1.8], [2.2, 0.8], [0.9, 1.9], [1.8, -0.9],[1.4, -0.6], [-0.2, -0.7], [-0.3, -2.4], [-1.5, 0.4], [1.2, -0.5], [-1.8, 1.2],[-0.8, -1.7], [-1.7, -2.3], [-0.6, -0.4], [0.3, 1.8], [-0.9, -1.9], [1.6, -1.6],[0.8, -2.6], [-2.6, 1.2], [1.8, -1.0], [0.2, -0.9], [-0.4, -2.5], [1.5, 1.5],[2.2, -1.3], [-1.4, 1.2], [-0.4, 1.3], [-1.3, -1.2], [-2.2, 0.4], [-0.1, 2.9],[1.5, -2.4], [1.1, 2.3], [0.4, 2.8], [-0.8, -1.2], [-2.7, 0.4], [2.3, 1.1],[0.9, 1.0], [0.9, 0.7], [-1.8, 0.3], [-1.7, -0.4], [1.0, -0.3], [-1.1, -0.6],[-2.4, -0.4], [2.6, -1.4], [1.3, -0.7], [-0.0, 1.0], [-1.1, -2.4], [2.0, -0.5],[0.3, 2.3], [-0.6, -2.8], [0.6, -1.8], [0.9, -0.4], [1.0, -1.3], [-0.4, 0.2],[2.3, 0.1], [2.2, 0.5], [2.5, 0.2], [-2.1, -1.3], [-1.1, 1.0], [1.7, 1.5],[0.9, -1.0], [1.1, -2.2], [-0.2, -2.4], [0.7, -1.1], [-0.4, 0.3], [-0.0, -2.6],[-0.3, -0.1], [-1.8, -1.6], [-0.8, 2.5], [-1.9, -1.4], [-2.5, 1.2], [-2.3, -0.6],[-1.6, 0.1], [-1.9, 0.0], [1.1, -0.6], [-0.2, 2.7]])
# 定义类别2的散点数据。这些点代表另一个分类。
class2_points = np.array([[5.1, 1.6], [-5.1, 1.2], [-4.6, 3.0], [-5.7, 0.5], [5.4, 2.5], [-4.5, -2.5],[4.9, -0.4], [1.4, 5.5], [1.4, 5.7], [4.2, -1.0], [-1.3, -4.8], [-4.4, -2.9],[-3.6, 3.4], [-3.4, -4.1], [-5.8, -0.1], [4.7, 3.0], [-1.4, 4.4], [2.5, -4.7],[2.7, 5.3], [4.1, -2.8], [-4.0, -2.5], [5.0, 0.5], [-4.0, 4.3], [5.0, 1.3],[3.3, 3.4], [2.2, 4.6], [2.8, 4.8], [4.0, -3.5], [4.6, -3.1], [0.5, 5.5],[-4.7, 3.1], [-5.7, 1.0], [2.8, -5.1], [-1.3, 4.9], [2.7, 5.2], [-4.9, 1.3],[4.1, -2.9], [-4.9, -3.3], [-4.6, 2.8], [-4.6, 3.1], [-1.8, 4.8], [-2.4, 5.3],[-5.2, 3.0], [-3.7, -4.4], [1.5, -5.0], [4.8, 1.1], [-0.6, -5.8], [0.7, -4.9],[0.2, 5.7], [5.8, -0.5], [-2.0, -4.0], [3.9, -3.1], [0.2, 5.1], [4.5, 1.5],[-1.4, -5.3], [5.0, -1.4], [5.1, 0.7], [5.0, -3.0], [-0.7, -5.1], [5.2, -1.5],[-0.7, 4.5], [2.1, 3.9], [-2.4, 5.0], [-0.8, 4.9], [-5.1, 0.3], [3.3, 3.6],[-0.4, -5.1], [3.8, 4.6], [-5.3, -2.5], [-5.5, -1.2], [0.6, 5.5], [-5.8, 0.8],[-5.3, 1.2], [2.0, -5.5], [5.7, 0.7], [-1.1, -4.7], [-0.0, 5.7], [-3.0, -4.8],[-3.5, -4.0], [4.9, 1.8], [-1.1, -5.5], [-2.7, 4.2], [-4.9, -1.6], [-0.2, -5.2],[2.5, 5.1], [-0.0, 5.4], [3.9, 4.4], [3.5, 4.8], [4.8, -1.9], [5.4, -1.0],[3.7, 4.6], [1.8, 5.2], [4.7, 3.4], [4.1, 2.3], [0.6, -5.4], [1.4, 5.5],[-5.5, 1.3], [4.2, 2.5], [-2.2, 3.9], [5.9, -0.4]])
# 将两个类别的散点数据在行方向上连接起来,形成总的输入数据矩阵。
points = np.concatenate((class1_points, class2_points), axis=0)
# 为合并后的数据创建对应的标签。类别1标记为1,类别2标记为0。
labels = np.concatenate((np.ones(len(class1_points)), np.zeros(len(class2_points))))# 2. 定义神经网络模型
class Model(nn.Module):# 模型的初始化方法def __init__(self):super(Model, self).__init__()  # 调用父类nn.Module的构造函数# 定义第一个全连接层 (Linear Layer)。# 输入特征维度是2(每个点有x,y两个坐标),输出特征维度是4。self.layer1 = nn.Linear(2, 4)# 定义第二个全连接层。# 输入特征维度是4(来自第一个隐藏层),输出特征维度是2(对应两个类别的logit值)。self.layer2 = nn.Linear(4, 2)# 模型的前向传播方法def forward(self, x):# 数据通过第一个全连接层,然后应用Sigmoid激活函数。# Sigmoid函数将输出值压缩到0到1之间,引入非线性,使得模型能够学习更复杂的决策边界。x = torch.sigmoid(self.layer1(x))# 数据通过第二个全连接层。# 注意:这里没有应用激活函数,因为CrossEntropyLoss通常期望接收原始的logit值。x = self.layer2(x)return x  # 返回模型的预测输出(logit)。# 7. 创建网格点以绘制等高线图(决策边界)
# 使用np.meshgrid创建二维网格点,用于覆盖整个绘图区域。
# 从-6.5到6.5的范围内生成100个均匀间隔的x和y坐标。
xx, yy = np.meshgrid(np.linspace(-6.5, 6.5, 100), np.linspace(-6.5, 6.5, 100))
# 将网格点的x和y坐标展平,并按列堆叠,形成N x 2的数组,N是网格点的总数。
grid_points = np.c_[xx.ravel(), yy.ravel()]# 实例化之前定义的神经网络模型。
model = Model()# 3. 定义优化器与损失函数
lr = 0.05  # 定义学习率(Learning Rate),控制模型参数更新的步长。
cri = nn.CrossEntropyLoss()  # 定义交叉熵损失函数(Cross Entropy Loss)。
# CrossEntropyLoss常用于多分类问题,它包含了softmax和负对数似然损失。
# 它可以直接接收模型的原始logit输出和类别标签。
optimizer = optim.Adam(model.parameters(), lr=lr)  # 定义Adam优化器。
# Adam是一种自适应学习率优化算法,model.parameters()获取模型中所有可训练的参数。# 4. 迭代训练过程
epoches = 500  # 定义训练的迭代次数(epoch)。一个epoch表示遍历整个数据集一次。
for epoch in range(1, epoches + 1):# 将NumPy数组格式的输入数据(points)转换为PyTorch张量。# dtype=torch.float32表示浮点类型,神经网络通常使用浮点数进行计算。input_data = torch.tensor(points, dtype=torch.float32)# 将NumPy数组格式的标签数据(labels)转换为PyTorch张量。# dtype=torch.long表示长整型,CrossEntropyLoss要求标签是长整型。label_data = torch.tensor(labels, dtype=torch.long)# 执行模型的前向传播,获取预测输出。output = model(input_data)# 根据模型的输出和真实标签计算损失值。loss = cri(output, label_data)# 反向传播与优化optimizer.zero_grad()  # 清空之前计算的梯度。在每次反向传播之前,必须清零梯度,否则梯度会累积。loss.backward()  # 执行反向传播,计算损失相对于模型所有可训练参数的梯度。optimizer.step()  # 根据计算出的梯度更新模型参数(权重和偏置)。# 打印当前迭代的损失值。# .detach()用于将张量从计算图中分离,阻止其梯度被跟踪。# .cpu()用于将张量移动到CPU内存(如果它在GPU上)。# .item()用于将单元素张量转换为Python标量。print(loss.detach().cpu().item())# 5. 显示频率设置和动态绘图# 在第一个epoch和之后每隔20个epoch进行绘图和信息打印。if epoch == 1 or epoch % 20 == 0:print(f"epoch:{epoch},loss:{loss}")  # 打印当前epoch数和损失值。# 将用于绘图的网格点数据转换为PyTorch张量。grid_points_tensor = torch.tensor(grid_points, dtype=torch.float32)# 使用当前训练的模型对网格点进行预测。# .detach().numpy():将预测结果从计算图中分离并转换为NumPy数组。pre_result = model(grid_points_tensor).detach().numpy()# 获取模型对类别1的预测概率(或logit)。# pre_result的形状是 N x 2,其中第二列(索引1)通常对应于类别1的logit。# 然后将其重塑回xx(或yy)的原始网格形状,以便与meshgrid生成的坐标匹配。one_lables_prob = pre_result[:, 1].reshape(xx.shape)plt.cla()  # 清空当前图形中的所有内容,准备绘制新的帧,实现动态更新。# 绘制类别1的散点图,颜色为红色。plt.scatter(class1_points[:, 0], class1_points[:, 1], c='r', label='Class 1')# 绘制类别2的散点图,颜色为蓝色。plt.scatter(class2_points[:, 0], class2_points[:, 1], c='b', label='Class 0')# 绘制决策边界等高线。# levels=[0.5]表示绘制类别1概率为0.5的等值线。# 对于二分类,这条线就是模型区分两个类别的分界线。plt.contour(xx, yy, one_lables_prob, levels=[0.5], colors='k', linestyles='--')plt.title(f'Epoch {epoch}, Loss: {loss.item():.4f}')  # 添加标题显示当前epoch和损失plt.xlabel('X-axis')  # 添加X轴标签plt.ylabel('Y-axis')  # 添加Y轴标签plt.legend()  # 显示图例plt.pause(0.3)  # 暂停0.3秒,以便观察动态训练过程,太快可能看不到变化。# print('s') # 这行代码似乎是一个调试输出,可以删除或注释掉。# 6. 最终绘图显示
plt.show()  # 显示最终的绘图结果。在训练循环结束后,会显示最后一个epoch的决策边界。

总结

本文深入探讨了深度学习的三大核心概念:优化器、神经网络可解释性及欠拟合问题。优化器部分详细介绍了梯度下降及其变体(BGD、SGD、MBGD),并逐步演进至更高效的算法如SGD_Momentum、AdaGrad、RMSProp和Adam,阐述了各自的原理、优缺点及应用场景。神经网络可解释性部分定义了其重要性,讨论了“黑箱”挑战,并提出了提升可解释性的常用方法,如全连接层、Dropout、归一化层和注意力机制。欠拟合部分则明确了其定义(训练与测试误差均高)、常见原因(数据不足、模型复杂度低、正则化过度)及有效解决方案(增加数据、提高复杂度、减少正则化、选择特征),并通过实例代码展示了相关概念,旨在帮助读者更全面地理解深度学习模型的训练过程与行为表现。

http://www.xdnf.cn/news/964063.html

相关文章:

  • 【无标题新手学习期权从买入看涨期权开始】
  • OpenCV 图像像素值统计
  • Python入门手册:常用的Python标准库
  • C++初阶-list的模拟实现(难度较高)
  • C++学习-入门到精通【17】自定义的模板化数据结构
  • ParcelJS:零配置极速前端构建工具全解析
  • React 中的TypeScript开发范式
  • 存储设备应用指导
  • C++ 手写实现 unordered_map 和 unordered_set:深入解析与源码实战
  • 光伏功率预测 | BP神经网络多变量单步光伏功率预测(Matlab完整源码和数据)
  • word嵌入图片显示不全-error记
  • 高考志愿填报,如何查询高校历年录取分数线?
  • Vue 2.0 + C# + OnlyOffice 开发
  • Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
  • K8S容器介绍
  • ubuntu24安装cuda12.6+cudnn9.6
  • 国产具身大模型首入汽车工厂,全场景验证开启工业智能新阶段
  • Vue3 watch使用
  • 路由器欧盟EN 18031网络安全认证详细解读
  • Css实现悬浮对角线边框动效
  • 【Trace32专栏】使用trace32 定位分析log_buf问题
  • 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
  • ESP8266自动浇水系统
  • 边缘计算医疗风险自查APP开发方案
  • i++与++i的区别
  • 光影魔术手 4.7.1 | 经典照片美化软件
  • Java八股文——JVM「类加载篇」
  • 论文分类打榜赛Baseline(2):InternLM昇腾硬件微调实践
  • React---day12
  • 【QT】自动更新库QSimpleUpdater使用实例封装