从零实现Softmax回归 - d2l-zh项目详解
引言:为什么需要Softmax回归?
在机器学习实践中,我们经常面临分类问题:不是预测"多少",而是预测"哪一个"。比如判断邮件是否为垃圾邮件、识别图像中的物体类别、预测用户是否会订阅服务等。虽然线性回归擅长处理连续值预测,但对于分类任务,我们需要一种能够输出概率分布的模型——这就是Softmax回归的用武之地。
Softmax回归是多分类问题的基石,它能够将线性模型的输出转换为概率分布,为每个类别分配一个0到1之间的概率值,且所有类别的概率之和为1。本文将带你从零开始实现Softmax回归,深入理解其数学原理和实现细节。
Softmax回归的数学基础
核心公式
Softmax函数定义为:
$$ \mathrm{softmax}(\mathbf{o})j = \frac{\exp(o_j)}{\sum{k=1}^q \exp(o_k)} $$
其中$\mathbf{o}$是未规范化的预测(logits),$q$是类别数量。这个公式确保:
- 所有输出都是非负数
- 所有输出的和为1
- 保持原始logits的大小顺序
交叉熵损失函数
对于分类问题,我们使用交叉熵损失函数:
$$ l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j $$
其中$\mathbf{y}$是真实标签的独热编码,$\hat{\mathbf{y}}$是预测的概率分布。
环境设置与数据准备
多框架支持实现
d2l-zh项目支持多种深度学习框架,以下是各框架的初始化代码:
# MXNet
from d2l import mxnet as d2l
from mxnet import autograd, np, npx, gluon
npx.set_np()
# PyTorch
from d2l import torch as d2l
import torch
# TensorFlow
from d2l import tensorflow as d2l
import tensorflow as tf
# PaddlePaddle
from d2l import paddle as d2l
import paddle
数据加载
使用Fashion-MNIST数据集,批量大小为256:
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
从零实现Softmax回归
1. 初始化模型参数
num_inputs = 784 # 28x28图像展平
num_outputs = 10 # 10个类别
# 权重和偏置初始化
W = np.random.normal(0, 0.01, (num_inputs, num_outputs))
b = np.zeros(num_outputs)
W.attach_grad()
b.attach_grad()
2. 实现Softmax函数
def softmax(X):
X_exp = d2l.exp(X)
partition = d2l.reduce_sum(X_exp, 1, keepdims=True)
return X_exp / partition # 广播机制
3. 定义模型
def net(X):
return softmax(d2l.matmul(d2l.reshape(X, (-1, W.shape[0])), W) + b)
4. 实现交叉熵损失函数
def cross_entropy(y_hat, y):
return - d2l.log(y_hat[range(len(y_hat)), y])
5. 计算分类精度
def accuracy(y_hat, y):
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = d2l.argmax(y_hat, axis=1)
cmp = d2l.astype(y_hat, y.dtype) == y
return float(d2l.reduce_sum(d2l.astype(cmp, y.dtype)))
6. 训练过程
def train_epoch_ch3(net, train_iter, loss, updater):
"""训练模型一个迭代周期"""
metric = Accumulator(3) # 训练损失、训练精度、样本数
for X, y in train_iter:
with autograd.record():
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.size)
return metric[0] / metric[2], metric[1] / metric[2]
数值稳定性考虑
数值溢出问题
原始Softmax实现可能存在数值稳定性问题:
改进方案:Log-Softmax
为了解决数值稳定性问题,可以使用Log-Softmax技巧:
def log_softmax(X):
X_max = d2l.reduce_max(X, axis=1, keepdims=True)
X_shifted = X - X_max
log_sum_exp = d2l.log(d2l.reduce_sum(d2l.exp(X_shifted), axis=1, keepdims=True))
return X_shifted - log_sum_exp
性能优化技巧
1. 向量化计算
# 小批量矢量化计算
def softmax_vectorized(O):
"""按行执行softmax"""
O_exp = d2l.exp(O)
partition = d2l.reduce_sum(O_exp, axis=1, keepdims=True)
return O_exp / partition
2. 内存优化
# 使用原地操作减少内存分配
def softmax_inplace(X):
X_exp = d2l.exp(X)
d2l.reduce_sum(X_exp, axis=1, keepdims=True, out=partition)
d2l.divide(X_exp, partition, out=X_exp)
return X_exp
实战:训练与评估
训练配置
lr = 0.1
num_epochs = 10
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
训练循环
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
animator = Animator(xlabel='epoch', xlim=[1, num_epochs],
ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch + 1, train_metrics + (test_acc,))
return train_metrics
性能评估
训练完成后,我们可以评估模型在测试集上的表现:
# 评估测试精度
test_accuracy = evaluate_accuracy(net, test_iter)
print(f'测试精度: {test_accuracy:.3f}')
# 可视化预测结果
def predict_ch3(net, test_iter, n=6):
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(d2l.argmax(net(X), axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(d2l.reshape(X[0:n], (n, 28, 28)), 1, n, titles=titles[0:n])
常见问题与解决方案
1. 梯度消失/爆炸
2. 过拟合处理
| 技术 | 描述 | 适用场景 |
|---|---|---|
| L2正则化 | 添加权重惩罚项 | 所有场景 |
| Dropout | 随机丢弃神经元 | 深层网络 |
| 早停 | 监控验证集性能 | 训练时间有限 |
| 数据增强 | 增加训练数据多样性 | 数据量不足 |
3. 类别不平衡
对于类别不平衡问题,可以采用加权交叉熵损失:
def weighted_cross_entropy(y_hat, y, class_weights):
"""加权交叉熵损失"""
loss = -d2l.log(y_hat[range(len(y_hat)), y])
weights = class_weights[y]
return d2l.reduce_mean(loss * weights)
进阶主题
1. 自定义Softmax温度参数
def softmax_with_temperature(X, temperature=1.0):
"""带温度参数的Softmax"""
X = X / temperature
X_exp = d2l.exp(X)
partition = d2l.reduce_sum(X_exp, axis=1, keepdims=True)
return X_exp / partition
2. 混合精度训练
def mixed_precision_softmax(X):
"""混合精度Softmax"""
X = X.astype('float16')
X_max = d2l.reduce_max(X, axis=1, keepdims=True)
X_shifted = X - X_max
X_exp = d2l.exp(X_shifted)
partition = d2l.reduce_sum(X_exp, axis=1, keepdims=True)
return (X_exp / partition).astype('float32')
总结与最佳实践
通过本文的从零实现,我们深入理解了Softmax回归的:
- 数学原理:Softmax函数如何将logits转换为概率分布
- 损失函数:交叉熵损失的理论基础和实践实现
- 数值稳定性:处理数值溢出和下溢的技巧
- 性能优化:向量化计算和内存优化策略
- 实战应用:完整的训练、评估和预测流程
关键要点
- 🎯 Softmax回归是多分类问题的基本模型
- 🔢 注意数值稳定性,使用Log-Softmax技巧
- ⚡ 利用向量化计算提升性能
- 📊 监控训练过程,防止过拟合
- 🧪 在不同框架下保持一致的实现
下一步学习
掌握了Softmax回归的基础后,建议继续学习:
- 多层感知机(MLP)和深度神经网络
- 卷积神经网络(CNN)用于图像分类
- 循环神经网络(RNN)用于序列数据
- 注意力机制和Transformer架构
Softmax回归作为深度学习的入门模型,为你后续学习更复杂的神经网络架构奠定了坚实的基础。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



