[!NOTE] 参考说明

本笔记参考《深度学习入门:基于Python的理论和实现》(第4章)。

核心定义:这里所说的“学习”是指从训练数据中自动获取最优权重参数的过程。
核心指标:引入损失函数 (Loss Function)。学习的目的是找到使损失函数值最小的权重参数。
核心方法:利用梯度法 (Gradient Method),通过函数斜率来寻找最小值。

一. 从数据中学习

神经网络最大的特征就是可以“从数据中学习”,即由数据自动决定权重参数的值,从而避免了人工介入参数设计的繁琐。

1.1 数据驱动的方法演变

我们以识别手写数字“5”为例:

方法 描述 特点
以人为主 编写特定的逻辑规则(如:这里有个圆弧,那里有个横线) 极其困难,很难总结出普适规律。
机器学习 (Machine Learning) 人工设计特征量 (SIFT, HOG等) 转换为向量 SVM/KNN 学习 特征量仍需人工设计,机器只学习分类模式。
深度学习 (Deep Learning) 端到端 (End-to-End) 的学习。直接输入原始图像 输出结果 连特征量的提取也由机器自动完成,完全没有人为干预。

1.2 训练数据与测试数据

为了正确评价模型的泛化能力(即处理未见过数据的能力),必须将数据分为两部分:

  • 训练数据 (Training Data):用于寻找最优参数。
  • 测试数据 (Test Data):用于评价模型实际能力。

[!WARNING] 过拟合 (Overfitting)

如果模型只在训练数据上表现好,而在测试数据上表现差,这种状态称为“过拟合”。避免过拟合是机器学习的重要课题。

二. 损失函数

损失函数是表示神经网络性能“恶劣程度”的指标。我们的目标是让它越小越好。

2.1 均方误差 (Mean Squared Error, MSE)

最常用的损失函数。

  • :神经网络的输出
  • :监督数据(真实标签)

Python 实现:

import numpy as np

def mean_squared_error(y, t):
return 0.5 * np.sum((y - t) ** 2)

# 示例
t = np.array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0]) # 正确解是 "2"

# 情况1: 预测 "2" 的概率最高 (0.6) -> 误差小
y1 = np.array([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0])
print(mean_squared_error(y1, t)) # 0.0975

# 情况2: 预测 "7" 的概率最高 (0.6) -> 误差大
y2 = np.array([0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0])
print(mean_squared_error(y2, t)) # 0.5975

2.2 交叉熵误差 (Cross Entropy Error, CEE)

分类问题中推荐使用的损失函数。

  • 由于 是 One-hot 向量(只有正确解为1,其余为0),该公式实际上只计算正确解标签对应的自然对数
  • 例如:正确解是索引2,模型预测索引2的概率 ,则
  • 直观理解:正确解的概率 越接近 1, 越接近 0(误差越小)。

Python 实现:

def cross_entropy_error(y, t):
delta = 1e-7 # 微小值,防止 np.log(0) 导致 -inf
return -np.sum(t * np.log(y + delta))

# 接上文示例
print(cross_entropy_error(y1, t)) # 0.51... (误差小)
print(cross_entropy_error(y2, t)) # 2.30... (误差大)

2.3 Mini-batch 学习

神经网络学习的目标是最小化所有训练数据的损失函数总和。但是,当数据量巨大(如 MNIST 有 60000 张,ImageNet 有上百万张)时,每次更新参数都计算所有数据的梯度是不现实的。

Mini-batch 的思想源于统计学中的大数定律: 如果我们从数据集中随机抽取一部分样本(例如 100 个),只要样本量足够,这 100 个样本的平均梯度,就可以作为总体梯度的一个很好的近似值

2.4 核心疑问:为什么不用“识别精度”作为指标?

既然我们的最终目标是提高识别精度(Accuracy),为什么不直接用识别精度作为指标来优化参数,反而要引入一个“损失函数”呢?

根本原因:为了让导数(梯度)不为 0。

神经网络的学习过程是依赖导数(梯度)来指引方向的。我们通过计算“如果稍微改变权重,指标会如何变化”来更新参数。

  1. 识别精度的缺陷(离散、不连续):

    • 识别精度对微小的参数变化不敏感
    • 假设现在的精度是 32%,如果我们微调权重,精度通常依然保持 32%,或者突然跳变到 33%。
    • 这种离散的跳变意味着,在绝大多数地方,参数的导数都等于 0
    • 如果导数为 0,权重就不会更新,学习过程就会“卡死”。
  2. 损失函数的优势(连续):

    • 损失函数是连续变化的。
    • 即使参数发生极其微小的变化,损失函数的值也会发生相应的微小变化(例如从 0.92543 变为 0.92542)。
    • 这意味着导数在绝大多数地方不为 0,神经网络可以持续地获得更新参数的方向和幅度。

三. 梯度 (Gradient)

3.1 概念

  • 数学定义:由全部变量的偏导数汇总而成的向量称为梯度(Gradient)。
  • 物理意义:梯度指示了函数值增加最快的方向(上坡最陡的方向)。
  • 决策方向:因为我们的目标是减小损失函数(找谷底),所以我们需要沿着梯度的反方向(负梯度方向)前进。

3.2 梯度下降法

这是神经网络学习的核心算法。为了找到损失函数的最小值,我们沿着梯度的反方向,一步一步地更新参数。

更新公式详解:

参数含义对照表:

符号 读音 名称 详细含义与作用
Theta 参数 (权重/偏置) 这里指需要更新的权重 () 或偏置 ()。下标 表示这是第 个参数。
- 减号 (关键) 为什么要减?
因为梯度指向“增加”的方向。为了让损失函数“减小”,必须向梯度的反方向移动。
Eta 学习率 决定了在梯度方向上一步走多远。这是一个超参数(详见下节)。
- 损失函数 即 Loss Function(如均方误差或交叉熵误差)。我们需要最小化它。
- 梯度 (偏导数) 表示“如果 稍微变化一点,损失函数 会变化多少”。



它指引了更新的方向和强弱。

执行步骤:

  1. 计算梯度:算出当前位置下,所有参数的梯度。
  2. 更新参数:将每个参数减去“学习率 梯度”。
  3. 重复迭代:重复步骤 1 和 2,直到损失函数的值不再下降(收敛)。

3.3 学习率

定义:
学习率 () 是一个超参数。

  • 普通参数(如权重 、偏置 ):由神经网络在训练过程中自动学习和调整。
  • 超参数:无法由机器自动学习,必须由人工在训练前手动设定的参数。

学习率的影响:
想象一个人被蒙住双眼下山(寻找损失函数最小值):

  • **学习率过大 :
    • 表现:步子迈得太大。
    • 后果:可能会直接跨过谷底,跑到对面的山上去了。导致损失函数的值发散(越来越大),无法收敛。
  • 学习率过小 ( Small)
    • 表现:像蚂蚁一样挪动。
    • 后果:虽然能保证稳步下降,但收敛速度极慢,训练需要耗费大量时间,且容易困在局部最优解(小坑)里出不来。

四. 学习算法的实现

4.1 神经网络学习的 4 个步骤

  1. Mini-batch:随机选取一部分数据。
  2. 计算梯度:计算损失函数关于各个权重参数的梯度。
  3. 更新参数:将权重沿梯度反方向微调(乘以学习率)。
  4. 重复:重复步骤 1-3。

4.2 TwoLayerNet 类实现

这是一个包含 1 个隐藏层的神经网络类。

import sys, os
import numpy as np # 导入 NumPy 库,用于高效的矩阵和数组运算
# 导入自定义函数:sigmoid(激活函数)、softmax(输出层函数)、cross_entropy_error(损失函数)
from common.functions import sigmoid, softmax, cross_entropy_error
# 导入用于计算梯度的函数(这里使用数值微分,计算较慢但易于理解)
from common.gradient import numerical_gradient

class TwoLayerNet:
"""
一个包含 1 个隐藏层的全连接神经网络类

网络结构:
输入层 -> 隐藏层 (Sigmoid 激活) -> 输出层 (Softmax 激活)
"""

def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
"""
初始化网络的参数(权重 W 和 偏置 b)

参数:
input_size: 输入层的神经元数量(例如 MNIST 图像展平后是 784)
hidden_size: 隐藏层的神经元数量(例如 50 或 100)
output_size: 输出层的神经元数量(例如 MNIST 分类是 10)
weight_init_std: 权重初始化的标准差
"""

# 用于存储所有权重和偏置的字典
self.params = {}

# --- 第 1 层参数 (输入层 -> 隐藏层) ---
# W1: 权重矩阵。维度为 (输入神经元数, 隐藏神经元数)。
# 使用随机数初始化,并乘以 weight_init_std 确保初始值较小。
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
# b1: 偏置向量。初始化为 0。维度为 (隐藏神经元数, )。
self.params['b1'] = np.zeros(hidden_size)

# --- 第 2 层参数 (隐藏层 -> 输出层) ---
# W2: 权重矩阵。维度为 (隐藏神经元数, 输出神经元数)。
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
# b2: 偏置向量。初始化为 0。维度为 (输出神经元数, )。
self.params['b2'] = np.zeros(output_size)

def predict(self, x):
"""
执行前向传播(Forward Propagation),进行推理或预测
参数 x: 输入数据(图像等)
返回值 y: 预测结果(概率分布)
"""
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']

# --- 第 1 层运算 ---
# 矩阵乘法:输入 x 乘以权重 W1,加上偏置 b1
a1 = np.dot(x, W1) + b1
# 激活函数:经过 Sigmoid 激活函数处理
z1 = sigmoid(a1)

# --- 第 2 层运算(输出层)---
# 矩阵乘法:上一层输出 z1 乘以权重 W2,加上偏置 b2
a2 = np.dot(z1, W2) + b2
# 输出层激活函数:使用 Softmax 将结果转换为概率分布
y = softmax(a2)

return y

def loss(self, x, t):
"""
计算损失函数的值
参数 x: 输入数据, t: 监督数据(正确标签)
"""
# 1. 首先进行预测
y = self.predict(x)
# 2. 计算交叉熵误差(分类问题常用的损失函数)
return cross_entropy_error(y, t)

def accuracy(self, x, t):
"""
计算识别精度(准确率)
"""
# 1. 预测结果(概率)
y = self.predict(x)

# 2. 确定模型的最终预测结果(将概率转换为数字标签)
# np.argmax(y, axis=1) 找出每一行(每个样本)中概率最大的那个元素的索引
y = np.argmax(y, axis=1)

# 3. 如果监督数据 t 是 One-hot 编码,也需要转换回数字标签
if t.ndim != 1:
t = np.argmax(t, axis=1)

# 4. 计算准确率:(预测正确的数量 / 总样本数)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy

def numerical_gradient(self, x, t):
"""
计算所有参数(W1, b1, W2, b2)的梯度
这里使用数值微分(numerical_gradient)进行计算
"""
# 定义一个 Lambda 函数:计算当前参数 W 下的损失值
# 这是一个“闭包”,它能记住 x 和 t 的值,并只接受 W 作为可变参数
loss_W = lambda W: self.loss(x, t)

# 存储梯度的字典
grads = {}

# 使用外部导入的 numerical_gradient 函数计算各个参数的梯度
# 梯度的形状与对应参数的形状相同
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])

return grads

4.3 训练循环 (Training Loop)

结合 Mini-batch 和 梯度下降进行训练,并定期评价泛化能力。

import numpy as np
from dataset.mnist import load_mnist
# 假设 TwoLayerNet 保存在 two_layer_net.py 中
# from two_layer_net import TwoLayerNet

# 1. 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

train_loss_list = []
train_acc_list = []
test_acc_list = []

# 2. 超参数设定
iters_num = 10000 # 迭代次数
train_size = x_train.shape[0] # 60000
batch_size = 100
learning_rate = 0.1

# 计算 epoch (一轮) 需要的迭代次数
# 60000 / 100 = 600 次
iter_per_epoch = max(train_size / batch_size, 1)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

# 3. 训练循环
for i in range(iters_num):
# A. 获取 mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

# B. 计算梯度
grad = network.numerical_gradient(x_batch, t_batch)
# 注意:实际工程中通常使用误差反向传播法 (backward) 计算梯度,速度更快

# C. 更新参数
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]

# D. 记录损失
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)

# E. 每个 epoch 记录一次准确率 (评价泛化能力)
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print(f"epoch {int(i/iter_per_epoch)} | train acc: {train_acc:.4f}, test acc: {test_acc:.4f}")

五. 核心概念辨析:Batch, Epoch, Iteration

5.1定义

  1. Batch (批次)

    • 定义:一次模型更新(梯度下降)所使用的样本数量。
    • 例子:MNIST 中 batch_size = 100,意味着每次看 100 张图就更新一次权重。
  2. Iteration (迭代)

    • 定义:模型更新参数的次数。
    • 关系:1 次 Iteration = 处理 1 个 Batch。
    • 如果总共迭代 10,000 次,说明参数被更新了 10,000 次。
  3. Epoch (轮次)

    • 定义:所有训练数据被完整“看过”一遍的过程。
    • 关系:1 Epoch = 总样本数 / Batch Size。

5.2计算实例

假设:训练数据 10,000 个Batch Size = 100

  • 问题 1:完成 1 个 Epoch 需要多少次 Iteration

    • 计算: 次。
    • 含义:机器迭代 100 次后,刚好把这 10000 个数据都过了一遍。
  • 问题 2:如果要求训练 10 个 Epoch,总共需要多少次 Iteration

    • 计算: 次。
  • 问题 3:如果代码中写了 iters_num = 1000 (总迭代 1000 次),这相当于训练了多少个 Epoch?

    • 计算: 个 Epoch。