深度学习-线性神经网络5-Softmax回归的从零开始实现

2025-07-31  本文已影响0人  R7_Perfect

1. softmax回归的基本概念与实现

在深入代码实现之前,我们先了解softmax回归的基本概念。softmax回归是一种用于分类问题的模型,通过把输入映射到每个类别的概率上来进行预测。它适用于多类别分类任务,例如图片分类中的手写数字识别。

2. 初始化模型参数

和线性回归类似,softmax回归同样需要初始化权重参数与偏置项。在本实现中,每个图像被展平为长度为 784的向量(因为Fashion-MNIST中的图片大小为 28×28=784)。以下代码展示了如何初始化参数 W 和 b,其中权重参数使用均值为 0、标准差为 0.01 的正态分布随机生成,而偏置初始化为 0。

import torch

batch_size = 256  # 小批量大小
num_inputs = 784  # 输入数据维度
num_outputs = 10  # 输出数据维度

# 初始化权重w和偏置b
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

3. 定义模型

3.1 定义softmax运算

softmax函数的核心在于它将输入的分数转换为概率分布。对于一个向量 z的每一个分量 zj,softmax的定义如下:


1753933674567.png

这个公式的作用是:先将所有类别的原始分数通过指数函数转换为正数,再通过总和归一化,以确保所有概率之和为 1。这样一来,模型的输出就是一个概率分布,可以用来做分类预测。

def softmax(X):
#对输入张量 X 的每个元素计算指数。torch.exp 是 PyTorch 中的一个函数,用于逐元素计算指数。
    X_exp = torch.exp(X)
#计算 X_exp 在指定维度上的和。dim=1 表示沿着第一个维度(通常是特征维度)进行求和。keepdim=True 确保输出张量在求和维度上保持原来的维度(即不减少维度),这对于后续的广播操作是必要的。
    partition = X_exp.sum(dim=1, keepdim=True)
#将 X_exp 中的每个元素除以 partition 中对应的元素,从而得到 softmax 的输出。这个操作将每个元素缩放到 0 到 1 之间,并且所有元素的和为 1,形成一个概率分布。
    return X_exp / partition
x = torch.tensor([[1.0, 2.0, 3.0],
                  [4.0, 5.0, 6.0]])
#计算了 x 中每一行的元素和。dim=1 表示沿着第一个维度(列)进行求和。结果是一个一维张量,其中每个元素对应于 x 中每一行的和。
sum_result = x.sum(dim=1)
print(sum_result)  # tensor([ 6., 15.])

3.2 定义模型

定义softmax运算后,我们可以实现 softmax回归模型。下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape函数将每张原始图像展平为向量。

def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
  1. X.reshape((-1, W.shape[0])):
    这行代码将输入张量 X 重塑为一个二维张量,其中行数为 -1(自动计算),列数为 W.shape[0]。W.shape[0] 表示权重矩阵 W 的行数,这通常对应于输入特征的数量。

  2. torch.matmul(X.reshape((-1, W.shape[0])), W):
    这行代码计算 X 和权重矩阵 W 的矩阵乘法。torch.matmul 是 PyTorch 中用于矩阵乘法的函数。结果是一个新的张量,其中每一行是输入样本经过线性变换后的输出。

  3. +b:
    这行代码将偏置向量 b 加到矩阵乘法的结果中。偏置 b 通常用于调整线性变换的输出。

  4. softmax(...):
    最后,经过线性变换和加偏置后的结果通过 softmax 函数进行处理。softmax 函数将线性变换的输出转换为概率分布。

4. 交叉熵损失函数

对于多分类问题,交叉熵损失是softmax回归中常用的损失函数。假设模型预测的概率分布为 y^,真实类别的独热向量为 y,则交叉熵损失定义为:


1753934095380.png

在实现中,通常使用PyTorch内置的 cross_entropy 函数,这个函数包含了softmax和交叉熵的组合,简化了计算。

def cross_entropy(y_hat, y):
    return -torch.log(y_hat[range(len(y_hat)), y])

对于每个样本,提取其真实类别对应的预测概率 ^yj,然后对其取对数,再计算平均值。

交叉熵损失函数代码详解

# 模拟的 logits (未经 softmax 的输出),和真实标签
logits = torch.tensor([[2.0, 0.5, 1.0], [0.1, 2.5, 0.3]], requires_grad=True)
true_labels = torch.tensor([0, 1])  # 样本 1 属于类别 0,样本 2 属于类别 1

# Step 1: 对 logits 应用 softmax 以得到每个类别的概率
softmax_probs = torch.exp(logits) / torch.exp(logits).sum(dim=1, keepdim=True)
print(f"softmax_probs: \n{softmax_probs}")
"""
softmax_probs: 
tensor([[0.6285, 0.1402, 0.2312],
        [0.0755, 0.8323, 0.0922]], grad_fn=<DivBackward0>)
"""

# Step 2: 选择正确类别对应的概率
selected_probs = softmax_probs[range(len(true_labels)), true_labels]
print(f"selected_probs: \n{selected_probs}")
"""
selected_probs: 
tensor([0.6285, 0.8323], grad_fn=<IndexBackward0>)
"""

# Step 3: 计算负对数似然,即对正确类别的概率取负对数并求平均
manual_cross_entropy_loss = -torch.log(selected_probs).mean()

print("手动计算的 Cross Entropy 损失值:", manual_cross_entropy_loss.item())
"""
手动计算的 Cross Entropy 损失值: 0.3239785134792328
"""

5. 精度评估函数

在训练过程中,我们需要一种方法来评估模型的精度。通常,对于每一个样本,取预测概率最大的类别作为预测结果,并与真实标签进行比较。

d2l.py

def accuracy(y_hat, y):
 # 检查预测结果是否为二维张量,并且第二维的大小大于1
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
 # 如果是,则使用 argmax 找到每个样本的预测类别
        y_hat = y_hat.argmax(dim=1)
   # 比较预测类别与真实类别
    cmp = y_hat.type(y.dtype) == y
   # 计算正确预测的数量并返回准确率
    return float(cmp.type(y.dtype).sum())

evaluate_accuracy函数用于在测试集上评估模型精度:

d2l.py

def evaluate_accuracy(net, data_iter):
    """计算在指定数据集上模型的精度"""
    if isinstance(net, torch.nn.Module):
        net.eval() # 将模型设置为评估模式
    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] # 返回准确率

6. 训练模型

接下来,我们定义训练循环。这个循环将完成以下步骤:

  1. 将输入数据传入模型,计算输出。
  2. 计算损失值。
  3. 反向传播更新模型参数。
  4. 记录每个周期的损失和精度。

Accumulator 是一个辅助类,用于记录训练过程中的损失和精度.

d2l.py

class Accumulator:
    """在n个变量上累加"""

    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, item):
        return self.data[item]
def train_epoch(net, train_iter, loss, updater):
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()
    metric = d2l.Accumulator(3)  # 创建一个累加器,用于存储损失总和、正确预测的数量和样本总数
    for X, y in train_iter:  # 遍历训练数据集
        y_hat = net(X)  # 计算模型的预测结果
        l = loss(y_hat, y)  # 计算损失
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()  # 清除梯度
            l.mean().backward()  # 计算梯度
            updater.step()  # 更新模型参数
        else:
            l.sum().backward()  # 计算梯度
            updater(batch_size)  # 更新模型参数
        # 累加损失、正确预测的数量和样本总数
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    # 返回平均损失和准确率
    return metric[0] / metric[2], metric[1] / metric[2]
  1. 输入参数:
    net: 要训练的模型。
    train_iter: 训练数据迭代器。
    loss: 损失函数,用于计算预测结果与真实标签之间的差异。
    updater: 更新器,用于更新模型参数,可以是 torch.optim.Optimizer 的实例或自定义的更新函数。
  2. 模型训练模式:
    如果 net 是一个 torch.nn.Module 实例,则将其设置为训练模式(net.train()),以确保在训练时使用 dropout 和 batch normalization 的训练行为。
  3. 累加器:
    d2l.Accumulator(3) 用于存储损失总和、正确预测的数量和样本总数。
  4. 遍历训练数据集:
    对于每个批次,计算模型的预测结果 y_hat。
    使用损失函数计算损失 l。
    如果 updater 是 torch.optim.Optimizer 的实例,使用优化器的标准步骤更新模型参数。
    否则,假设 updater 是一个自定义函数,直接调用它更新参数。
  5. 累加指标:
    使用 metric.add() 将损失、正确预测的数量和样本总数累加到累加器中。
  6. 返回结果:
    返回平均损失和准确率,分别为损失总和除以样本总数,以及正确预测的数量除以样本总数。

7. 模型训练主函数

在这里,我们定义训练的主函数train_softmax。该函数将在多个epoch中迭代,并在每个epoch后记录并输出训练损失和精度。

def train_softmax(net, train_iter, test_iter, loss, num_epochs, updater):
    for epoch in range(num_epochs): # 遍历每个训练周期
        train_metrics = train_epoch(net, train_iter, loss, updater) # 训练一个周期并获取训练损失和准确率
        test_acc = d2l.evaluate_accuracy(net, test_iter)  # 评估模型在测试集上的准确率
        print(f'epoch {epoch + 1}, train loss {train_metrics[0]:.3f}, '
              f'train acc {train_metrics[1]:.3f}, test acc {test_acc:.3f}')

8. 训练模型

我们使用前面文章中定义的 小批量随机梯度下降 来优化模型的损失函数,设置学习率为0.1。

d2l.py

def sgd(params, lr, batch_size):
    """小批量随机梯度下降"""
    with torch.no_grad():  # 禁用梯度计算
        for param in params:  # 遍历每个参数
            param -= lr * param.grad / batch_size # 更新参数
            param.grad.zero_() # 将参数的梯度清零

可视化训练过程

我们定义一个在动画中绘制数据的实用程序类 Animator

d2l.py

class Animator:
    """在动画中绘制数据"""

#xlabel, ylabel: x轴和y轴的标签。
#legend: 图例,用于标识不同的数据线。
#xlim, ylim: x轴和y轴的范围。
#xscale, yscale: x轴和y轴的刻度类型(线性或对数)。
#fmts: 数据线的格式(颜色和样式)。
#nrows, ncols: 图表的行数和列数。
#figsize: 图表的大小。
    def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
                 ylim=None, xscale='linear', yscale='linear',
                 fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
                 figsize=(3.5, 2.5)):
        # 增量地绘制多条线
        if legend is None:
            legend = []
        d2l.use_svg_display()#设置图形显示为 SVG 格式。
        self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)#创建图表
        if nrows * ncols == 1:
            self.axes = [self.axes, ]
        # 使用lambda函数捕获参数
        self.config_axes = lambda: d2l.set_axes(
            self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)  #配置图表的轴。
        self.X, self.Y, self.fmts = None, None, fmts

    def add(self, x, y):
        # 向图表中添加多个数据点
        if not hasattr(y, "__len__"):
            y = [y]
        n = len(y)
        if not hasattr(x, "__len__"):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)
        self.axes[0].cla()
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        self.config_axes()

    def show(self):
        # 显示图形
        plt.show()

现在,我们训练模型10个迭代周期。 请注意,迭代周期(num_epochs)和学习率(lr)都是可调节的超参数。 通过更改它们的值,我们可以提高模型的分类精度。

if __name__ == '__main__':

    num_epochs = 10
    # 加载Fashion-MNIST数据集
    train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
    # 初始状态,train acc 和 test acc 都为 1/10,随机的,用于计算初始状态的损失和准确率
    metric = d2l.Accumulator(3)
    # 计算初始状态的损失和准确率
    for X, y in train_iter:
        y_hat = net(X)
        l = cross_entropy(y_hat, y)
        metric.add(float(l.sum()), d2l.accuracy(y_hat, y), y.numel())
    # 计算初始训练损失和准确率
    train_metrics = metric[0] / metric[2], metric[1] / metric[2]
    # 计算初始测试准确率
    test_acc = d2l.evaluate_accuracy(net, test_iter)
    # 打印初始状态的损失和准确率
    print(f'epoch {0}, train loss {train_metrics[0]:.3f}, '
          f'train acc {train_metrics[1]:.3f}, test acc {test_acc:.3f}')
    # 初始化动画器,用于可视化训练过程
    animator = d2l.Animator(xlabel='epoch', xlim=[0, num_epochs], ylim=[0, 1],
                            legend=['train loss', 'train acc', 'test acc'])
    # 添加初始状态的数据点
    animator.add(0, train_metrics + (test_acc,))
    # 训练模型
    train_softmax(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
    # 可视化优化过程
    animator.show()
epoch 0, train loss 2.292, train acc 0.095, test acc 0.093
epoch 1, train loss 0.784, train acc 0.752, test acc 0.793
epoch 2, train loss 0.571, train acc 0.812, test acc 0.813
epoch 3, train loss 0.524, train acc 0.826, test acc 0.816
epoch 4, train loss 0.501, train acc 0.832, test acc 0.826
epoch 5, train loss 0.484, train acc 0.838, test acc 0.829
epoch 6, train loss 0.474, train acc 0.841, test acc 0.829
epoch 7, train loss 0.466, train acc 0.842, test acc 0.833
epoch 8, train loss 0.458, train acc 0.845, test acc 0.833
epoch 9, train loss 0.452, train acc 0.847, test acc 0.833
epoch 10, train loss 0.448, train acc 0.848, test acc 0.832

image.png

9. 预测

现在训练已经完成,我们的模型已经准备好对图像进行分类预测。

d2l.py

def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
    """绘制图像列表"""
    figsize = (num_cols * scale, num_rows * scale)
    _, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
    axes = axes.flatten()
    for i, (ax, img) in enumerate(zip(axes, imgs)):
        if torch.is_tensor(img):
            # 图片张量
            ax.imshow(img.numpy())
        else:
            # PIL图片
            ax.imshow(img)
        ax.axes.get_xaxis().set_visible(False)
        ax.axes.get_yaxis().set_visible(False)
        if titles:
            ax.set_title(titles[i])
    d2l.plt.show()
    return axes


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 = d2l.get_fashion_mnist_labels(y)
    preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
    titles = ["真实:" + true + '\n' + "预测:" + pred for true, pred in zip(trues, preds)]
    d2l.show_images(
        X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n], scale=2.5)
        
predict(net, test_iter)
image.png

总结

本文介绍了softmax回归模型的原理及从零实现步骤。通过映射输入为概率分布,softmax回归用于多分类任务。我们涵盖了参数初始化、softmax和交叉熵损失定义、精度评估,以及小批量随机梯度下降的训练过程,并在Fashion-MNIST数据集上验证了模型的有效性。

import torch

import d2l

batch_size = 256  # 小批量大小
num_inputs = 784  # 输入数据维度
num_outputs = 10  # 输出数据维度

# 初始化权重w和偏置b
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)


def softmax(X):
    X_exp = torch.exp(X)
    partition = X_exp.sum(dim=1, keepdim=True)
    return X_exp / partition


def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)


def cross_entropy(y_hat, y):
    return -torch.log(y_hat[range(len(y_hat)), y])


def train_epoch(net, train_iter, loss, updater):
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()
    metric = d2l.Accumulator(3)
    for X, y in train_iter:
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()
            l.backward()
            updater.step()
        else:
            l.sum().backward()
            updater(batch_size)
        metric.add(float(l.sum()), d2l.accuracy(y_hat, y), y.numel())
    return metric[0] / metric[2], metric[1] / metric[2]


def train_softmax(net, train_iter, test_iter, loss, num_epochs, updater):
    for epoch in range(num_epochs):
        train_metrics = train_epoch(net, train_iter, loss, updater)
        test_acc = d2l.evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc,))
        print(f'epoch {epoch + 1}, train loss {train_metrics[0]:.3f}, '
              f'train acc {train_metrics[1]:.3f}, test acc {test_acc:.3f}')


lr = 0.1


def updater(batch_size):
    return d2l.sgd([W, b], lr, batch_size)


def predict_ch3(net, test_iter, n=6):
    """预测标签(定义见第3章)"""
    for X, y in test_iter:
        break
    trues = d2l.get_fashion_mnist_labels(y)
    preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
    titles = ["真实:" + true + '\n' + "预测:" + pred for true, pred in zip(trues, preds)]
    d2l.show_images(
        X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n], scale=2.5)


if __name__ == '__main__':

    num_epochs = 10

    train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
    # 初始状态,train acc 和 test acc 都为 1/10,随机的
    metric = d2l.Accumulator(3)

    for X, y in train_iter:
        y_hat = net(X)
        l = cross_entropy(y_hat, y)
        metric.add(float(l.sum()), d2l.accuracy(y_hat, y), y.numel())

    train_metrics = metric[0] / metric[2], metric[1] / metric[2]
    test_acc = d2l.evaluate_accuracy(net, test_iter)

    print(f'epoch {0}, train loss {train_metrics[0]:.3f}, '
          f'train acc {train_metrics[1]:.3f}, test acc {test_acc:.3f}')
    animator = d2l.Animator(xlabel='epoch', xlim=[0, num_epochs], ylim=[0, 1],
                            legend=['train loss', 'train acc', 'test acc'])
    animator.add(0, train_metrics + (test_acc,))
    # 训练模型
    train_softmax(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
    # 可视化优化过程
    animator.show()
    # 预测
    predict_ch3(net, test_iter)
上一篇 下一篇

猜你喜欢

热点阅读