动手学深度学习笔记--三种常见损失函数、从零实现softmax
一、三种常见的回归任务损失函数
损失函数用于量化模型预测值与真实标签的误差,是神经网络反向传播、参数更新的核心依据。
1、L2 Loss 平方损失
公式:

下图中蓝色代表损失函数,黄色代表损失函数梯度,绿色代表似然函数

损失函数:抛物线形态,误差越大损失增长越快,对大误差的惩罚力度呈平方级上升。
梯度函数:随误差线性增大,误差越大梯度越大,大误差下更新步长会急剧增加。
2、L1 Loss 绝对值误差
公式:


损失函数:两条斜率为 ±1 的直线,损失随误差线性增长,惩罚力度恒定。
梯度函数:除原点外梯度恒为 ±1,梯度大小不随误差变化,原点处梯度突变、不可导。
3、Huber Loss
公式:


损失函数:误差较小时为抛物线,误差较大时为直线。
梯度函数:小误差下梯度随误差线性减小(L2 特性),大误差下梯度被截断为恒定值(L1 特性),全程平滑可导。
核心总结
L2 Loss、L1 Loss、Huber Loss 是回归任务的三类经典损失函数,核心差异在于对预测误差的惩罚力度与梯度特性不同,最终在可导性、异常值鲁棒性、收敛速度三个维度形成明确的取舍。
L2 Loss误差小时效果较好,误差较大的少量数据会导致训练不稳定,适用于干净数据情况,如经典线性回归、数据噪声极低的回归任务。
L1 Loss即使误差较大也能平稳的进行更新,对异常值有较好处理,但在接近最优解时会使更新步长过大,适用于数据异常值多、对精度要求低于稳定性的回归任务。
Huber Loss结合了以上两种方式,是通用回归任务的均衡优选方案,工业界应用最广,典型如目标检测的边界框回归。
二、从零实现softmax回归
概念介绍
Softmax回归是单层线性网络,输出维度等于互斥类别数 x。其输出通过 指数归一化 将线性得分 ai 映射为概率分布(和为1),适用于多分类。

以节点x1到节点y1为例,通过参数w11和b1计算得 a1 = w1*x1 + b1
在经过激活函数 y1 = softmax(a1)
softmax公式:
下图exp(ak) = e的ak次方

分母为所有y的和,所以 y1+y2+y3 = 1,yk可以看作输出类别为类别k的概率,这也正是softmax适合分类任务的原因。
直接按公式计算 Softmax 存在严重的数值溢出风险,通过指数函数和数学运算的特性,将yk分子分母同时除以相同的数,在通过指数函数特性将其放到指数函数中

C‘设为ai中的最大值即可解决问题
代码实现
我们基于 Fashion-MNIST 数据集实现完整的 Softmax 回归,覆盖数据加载、模型定义、损失函数、训练验证全流程。
1. 环境依赖导入
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
2. 数据集加载
使用 Fashion-MNIST 服饰分类数据集:包含 10 类服饰图像,单通道 28×28 像素,训练集 60000 张,测试集 10000 张。
def load_data_fashion_mnist(batch_size, resize=None):
"""加载 Fashion-MNIST 数据集,返回训练集和测试集的DataLoader
Args:
batch_size: 小批量样本数
resize: 可选,将图像缩放到指定尺寸
Returns:
train_iter: 训练数据迭代器
test_iter: 测试数据迭代器
"""
# 定义数据预处理流水线
trans = [transforms.ToTensor()] # PIL图像转张量,像素值归一化到[0,1]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
# 加载数据集(首次运行会自动下载)
mnist_train = torchvision.datasets.FashionMNIST(
root="./data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="./data", train=False, transform=trans, download=True)
# 构建DataLoader:训练集打乱顺序,测试集不打乱
return (data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=2),
data.DataLoader(mnist_test, batch_size, shuffle=False, num_workers=2))
3. 评估与训练辅助工具
(1)准确率计算函数
def accuracy(y_hat, y):
"""计算批量中预测正确的样本数量
Args:
y_hat: 预测概率矩阵,形状 (batch_size, num_classes)
y: 真实类别索引,形状 (batch_size,)
Returns:
正确预测的样本总数
"""
# 多分类场景:取每行概率最大值的索引作为预测类别
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
# 数据类型对齐后比较,统计正确样本数
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())
(2)指标累加器
用于遍历批量时累计损失、正确样本数、总样本数等指标,避免频繁创建变量。
class Accumulator:
"""多变量累加器,用于训练/评估时批量统计指标"""
def __init__(self, n):
self.data = [0.0] * n # n个待累加的统计变量
def add(self, *args):
"""按位置累加对应数值"""
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
"""重置所有累加值为0"""
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
"""按索引获取累加值"""
return self.data[idx]
(3)数据集精度评估
def evaluate_accuracy(net, data_iter):
"""计算模型在指定数据集上的分类准确率
Args:
net: 模型函数/网络
data_iter: 数据迭代器
Returns:
整体准确率(正确数/总样本数)
"""
if isinstance(net, torch.nn.Module):
net.eval() # 切换评估模式,禁用Dropout、BN等训练专属层
metric = Accumulator(2) # 两个统计量:预测正确数、总样本数
with torch.no_grad(): # 关闭梯度计算,节省显存与算力
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
4. Softmax 函数与网络前向传播
(1)数值稳定版 Softmax 实现
def softmax(X):
"""数值稳定的Softmax函数
Args:
X: 原始得分矩阵,形状 (batch_size, num_classes)
Returns:
概率矩阵,每行和为1,形状与输入一致
"""
# 取每行最大值,保持维度以便广播运算
X_max = X.max(dim=1, keepdim=True)[0]
X_stable = X - X_max # 减去行最大值,避免指数上溢
X_exp = torch.exp(X_stable)
partition = X_exp.sum(dim=1, keepdim=True) # 归一化分母
return X_exp / partition
(2)Softmax 回归前向传播
def net(X):
"""Softmax回归前向传播逻辑
Args:
X: 输入图像批次,形状 (batch_size, 1, 28, 28)
Returns:
类别概率矩阵,形状 (batch_size, 10)
"""
# 图像展平:将[batch, 1, 28, 28] reshape为[batch, 784]
# -1表示自动计算该维度大小,等价于batch_size
X_flat = X.reshape(-1, W.shape[0])
# 线性变换:矩阵乘法 + 偏置广播
linear_output = torch.matmul(X_flat, W) + b
# 经Softmax输出概率分布
return softmax(linear_output)
5. 交叉熵损失实现
def cross_entropy(y_hat, y):
"""交叉熵损失函数
Args:
y_hat: 预测概率矩阵,形状 (batch_size, num_classes)
y: 真实类别索引,形状 (batch_size,)
Returns:
每个样本的损失值,形状 (batch_size,)
"""
# 高级索引:批量取出每个样本对应真实类别的预测概率
# range(len(y_hat)) 生成行索引(0到batch_size-1),y为列索引
# 等价于依次取 y_hat[0][y[0]], y_hat[1][y[1]], ..., y_hat[n-1][y[n-1]]
return -torch.log(y_hat[range(len(y_hat)), y])
y_hat[range(len(y_hat)), y]可以拆解成 y_hat[a,b], a = range(len(y_hat)), b=y
y_hat: 预测概率矩阵 y: 真实类别索引假设本次进行三分类任务,batch_size = 3,y_hat.shape = (3,3)
y_hat=[ [0.2, 0.3, 0.5] [0.4, 0.1, 0.5] [0.8, 0.1, 0.1] ]第一行[0.2,0.3,0.5]是这一批中第一个数据点分别是第一类,第二类,第三类的概率预测,其他两行同理
y=[2, 2, 0]第一个数据就是第三类,第二个也是第三类,第三个是第一类
len(y_hat) = 批次 = 3, a=range(len(y_hat))会生成从0到2,同时b=y[a],会依次取出数据点真实标签所以y_hat[range(len(y_hat)), y] 分别回取出y_hat[0][2] y_hat[1][2] y_hat[2][0], 正是真实标签在预测中的概率而真实预测的概率越大,损失越小,所以最终返回加了个负号为 -torch.log(y_hat[range(len(y_hat)), y])
6. 完整训练流程
(1)单轮训练(一个 Epoch)
def train_epoch_ch3(net, train_iter, loss, updater):
"""训练模型一个epoch(完整遍历一次训练集)
Args:
net: 模型
train_iter: 训练数据迭代器
loss: 损失函数
updater: 优化器
Returns:
平均训练损失、平均训练准确率
"""
if isinstance(net, torch.nn.Module):
net.train() # 切换训练模式
metric = Accumulator(3) # 累计:损失总和、正确样本数、总样本数
for X, y in train_iter:
y_hat = net(X) # 前向传播:计算预测概率
l = loss(y_hat, y) # 计算每个样本的损失
if isinstance(updater, torch.optim.Optimizer):
# PyTorch内置优化器标准流程
updater.zero_grad() # 清零上一轮梯度
l.mean().backward() # 损失均值反向传播(数值更稳定)
updater.step() # 更新参数
else:
# 自定义优化器兼容流程
l.sum().backward()
updater(X.shape[0])
# 累计本轮统计指标
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回本轮平均损失和平均准确率
return metric[0] / metric[2], metric[1] / metric[2]
(2)全周期训练循环
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
"""完整训练流程,每轮输出训练与测试指标"""
for epoch in range(num_epochs):
train_loss, train_acc = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
print(f"epoch {epoch+1:2d} | 训练损失: {train_loss:.4f} | "
f"训练准确率: {train_acc:.4f} | 测试准确率: {test_acc:.4f}")
7. 预测可视化辅助
def get_fashion_mnist_labels(labels):
"""将类别索引转换为中文文本标签"""
text_labels = ['T恤', '裤子', '套头衫', '连衣裙', '外套',
'凉鞋', '衬衫', '运动鞋', '包', '短靴']
return [text_labels[int(i)] for i in labels]
def predict_ch3(net, test_iter, n=6):
"""取一批测试样本,输出真实标签与预测标签对比"""
for X, y in test_iter:
break # 只取第一批数据
trues = get_fashion_mnist_labels(y)
preds = get_fashion_mnist_labels(net(X).argmax(axis=1))
print("\n预测示例:")
print(f"真实标签: {trues[:n]}")
print(f"预测标签: {preds[:n]}")
8. 主程序:参数初始化与运行
if __name__ == '__main__':
# ========== 超参数设置 ==========
batch_size = 32 # 小批量样本数
num_epochs = 10 # 训练总轮数
learning_rate = 0.1 # 学习率
# ========== 加载数据集 ==========
train_iter, test_iter = load_data_fashion_mnist(batch_size)
# ========== 模型参数初始化 ==========
num_inputs = 28 * 28 # 输入特征维度:28*28=784
num_outputs = 10 # 输出类别数:10类服饰
# 权重:正态分布初始化,均值0,标准差0.01,开启梯度追踪
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
# 偏置:初始化为0,开启梯度追踪
b = torch.zeros(num_outputs, requires_grad=True)
# ========== 定义损失与优化器 ==========
loss = cross_entropy
updater = torch.optim.SGD([W, b], lr=learning_rate)
# ========== 启动训练 ==========
print("开始训练Softmax回归...")
train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
# ========== 测试预测效果 ==========
predict_ch3(net, test_iter)
更多推荐




所有评论(0)