人工智能基础3-神经网络的学习
[!NOTE] 参考说明
本笔记参考《深度学习入门:基于Python的理论和实现》(第4章)。
核心定义:这里所说的“学习”是指从训练数据中自动获取最优权重参数的过程。
核心指标:引入损失函数 (Loss Function)。学习的目的是找到使损失函数值最小的权重参数。
核心方法:利用梯度法 (Gradient Method),通过函数斜率来寻找最小值。
一. 从数据中学习
神经网络最大的特征就是可以“从数据中学习”,即由数据自动决定权重参数的值,从而避免了人工介入参数设计的繁琐。
1.1 数据驱动的方法演变
我们以识别手写数字“5”为例:
| 方法 | 描述 | 特点 |
|---|---|---|
| 以人为主 | 编写特定的逻辑规则(如:这里有个圆弧,那里有个横线) | 极其困难,很难总结出普适规律。 |
| 机器学习 (Machine Learning) | 人工设计特征量 (SIFT, HOG等) |
特征量仍需人工设计,机器只学习分类模式。 |
| 深度学习 (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。
神经网络的学习过程是依赖导数(梯度)来指引方向的。我们通过计算“如果稍微改变权重,指标会如何变化”来更新参数。
识别精度的缺陷(离散、不连续):
- 识别精度对微小的参数变化不敏感。
- 假设现在的精度是 32%,如果我们微调权重,精度通常依然保持 32%,或者突然跳变到 33%。
- 这种离散的跳变意味着,在绝大多数地方,参数的导数都等于 0。
- 如果导数为 0,权重就不会更新,学习过程就会“卡死”。
损失函数的优势(连续):
- 损失函数是连续变化的。
- 即使参数发生极其微小的变化,损失函数的值也会发生相应的微小变化(例如从 0.92543 变为 0.92542)。
- 这意味着导数在绝大多数地方不为 0,神经网络可以持续地获得更新参数的方向和幅度。
三. 梯度 (Gradient)
3.1 概念
- 数学定义:由全部变量的偏导数汇总而成的向量称为梯度(Gradient)。
- 物理意义:梯度指示了函数值增加最快的方向(上坡最陡的方向)。
- 决策方向:因为我们的目标是减小损失函数(找谷底),所以我们需要沿着梯度的反方向(负梯度方向)前进。
3.2 梯度下降法
这是神经网络学习的核心算法。为了找到损失函数的最小值,我们沿着梯度的反方向,一步一步地更新参数。
更新公式详解:
参数含义对照表:
| 符号 | 读音 | 名称 | 详细含义与作用 |
|---|---|---|---|
| Theta | 参数 (权重/偏置) | 这里指需要更新的权重 ( |
|
| - | 减号 (关键) | 为什么要减? 因为梯度指向“增加”的方向。为了让损失函数“减小”,必须向梯度的反方向移动。 |
|
| Eta | 学习率 | 决定了在梯度方向上一步走多远。这是一个超参数(详见下节)。 | |
| - | 损失函数 | 即 Loss Function(如均方误差或交叉熵误差)。我们需要最小化它。 | |
| - | 梯度 (偏导数) | 表示“如果 它指引了更新的方向和强弱。 |
执行步骤:
- 计算梯度:算出当前位置下,所有参数的梯度。
- 更新参数:将每个参数减去“学习率
梯度”。 - 重复迭代:重复步骤 1 和 2,直到损失函数的值不再下降(收敛)。
3.3 学习率
定义:
学习率 (
- 普通参数(如权重
、偏置 ):由神经网络在训练过程中自动学习和调整。 - 超参数:无法由机器自动学习,必须由人工在训练前手动设定的参数。
学习率的影响:
想象一个人被蒙住双眼下山(寻找损失函数最小值):
- **学习率过大 :
- 表现:步子迈得太大。
- 后果:可能会直接跨过谷底,跑到对面的山上去了。导致损失函数的值发散(越来越大),无法收敛。
- 学习率过小 (
Small): - 表现:像蚂蚁一样挪动。
- 后果:虽然能保证稳步下降,但收敛速度极慢,训练需要耗费大量时间,且容易困在局部最优解(小坑)里出不来。
四. 学习算法的实现
4.1 神经网络学习的 4 个步骤
- Mini-batch:随机选取一部分数据。
- 计算梯度:计算损失函数关于各个权重参数的梯度。
- 更新参数:将权重沿梯度反方向微调(乘以学习率)。
- 重复:重复步骤 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定义
Batch (批次)
- 定义:一次模型更新(梯度下降)所使用的样本数量。
- 例子:MNIST 中
batch_size = 100,意味着每次看 100 张图就更新一次权重。
Iteration (迭代)
- 定义:模型更新参数的次数。
- 关系:1 次 Iteration = 处理 1 个 Batch。
- 如果总共迭代 10,000 次,说明参数被更新了 10,000 次。
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。
- 计算: