动手学深度学习3.7 softmax回归的简洁实现-笔记&练习(PyTorch)

以下内容为结合李沐老师的课程和教材补充的学习笔记,以及对课后练习的一些思考,自留回顾,也供同学之人交流参考。

本节课程地址:Softmax 回归简洁实现_哔哩哔哩_bilibili

本节教材地址:3.7. softmax回归的简洁实现 — 动手学深度学习 2.0.0 documentation (d2l.ai)

本节开源代码:...>d2l-zh>pytorch>chapter_linear-networks>softmax-regression-concise.ipynb


softmax回归的简洁实现

3.3节 中, 我们发现通过深度学习框架的高级API能够使实现线性回归变得更加容易。 同样,通过深度学习框架的高级API也能更方便地实现softmax回归模型。 本节如在 3.6节 中一样, 继续使用Fashion-MNIST数据集,并保持批量大小为256。

import torch
from torch import nn
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

初始化模型参数

如我们在 3.4节 所述, softmax回归的输出层是一个全连接层。 因此,为了实现我们的模型, 我们只需在Sequential中添加一个带有10个输出的全连接层。 同样,在这里Sequential并不是必要的, 但它是实现深度模型的基础。 我们仍然以均值0和标准差0.01随机初始化权重。

# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
# 图像的第一维(channel)保留,其余维度(像素)展平成一维

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)
# 初始化权重值
net.apply(init_weights);

;

重新审视Softmax的实现

在前面 3.6节 的例子中, 我们计算了模型的输出,然后将此输出送入交叉熵损失。 从数学上讲,这是一件完全合理的事情。 然而,从计算角度来看,指数可能会造成数值稳定性问题。

回想一下,softmax函数 \hat y_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)} , 其中 \hat y_j 是预测的概率分布。 \hat y_j 是未规范化的预测 𝑜 的第 𝑗 个元素。 如果 o_k 中的一些数值非常大, 那么 \exp(o_k) 可能大于数据类型容许的最大数字,即上溢(overflow)。 这将使分母或分子变为inf(无穷大), 最后得到的是0、infnan(不是数字)的 \hat y_j 。 在这些情况下,我们无法得到一个明确定义的交叉熵值。

解决这个问题的一个技巧是: 在继续softmax计算之前,先从所有 o_k 中减去 \max(o_k)。 这里可以看到每个 o_k 按常数进行的移动不会改变softmax的返回值:

\begin{aligned} \hat y_j & = \frac{\exp(o_j - \max(o_k))\exp(\max(o_k))}{\sum_k \exp(o_k - \max(o_k))\exp(\max(o_k))} \\ & = \frac{\exp(o_j - \max(o_k))}{\sum_k \exp(o_k - \max(o_k))}. \end{aligned}

在减法和规范化步骤之后,可能有些 o_j - \max(o_k) 具有较大的负值。 由于精度受限, \exp(o_j - \max(o_k)) 将有接近零的值,即下溢(underflow)。 这些值可能会四舍五入为零,使 \hat y_j 为零, 并且使得 \log(\hat y_j) 的值为-inf。 反向传播几步后,我们可能会发现自己面对一屏幕可怕的nan结果。

尽管我们要计算指数函数,但我们最终在计算交叉熵损失时会取它们的对数。 通过将softmax和交叉熵结合在一起,可以避免反向传播过程中可能会困扰我们的数值稳定性问题。 如下面的等式所示,我们避免计算 \exp(o_j - \max(o_k)) , 而可以直接使用 o_j - \max(o_k) ,因为 \log(\exp(\cdot)) 被抵消了。

\begin{aligned} \log{(\hat y_j)} & = \log\left( \frac{\exp(o_j - \max(o_k))}{\sum_k \exp(o_k - \max(o_k))}\right) \\ & = \log{(\exp(o_j - \max(o_k)))}-\log{\left( \sum_k \exp(o_k - \max(o_k)) \right)} \\ & = o_j - \max(o_k) -\log{\left( \sum_k \exp(o_k - \max(o_k)) \right)}. \end{aligned}

我们也希望保留传统的softmax函数,以备我们需要评估通过模型输出的概率。 但是,我们没有将softmax概率传递到损失函数中, 而是[在交叉熵损失函数中传递未规范化的预测,并同时计算softmax及其对数], 这是一种类似"LogSumExp技巧"的聪明方式。

loss = nn.CrossEntropyLoss(reduction='none')

优化算法

在这里,我们使用学习率为0.1的小批量随机梯度下降作为优化算法。 这与我们在线性回归例子中的相同,这说明了优化器的普适性。

trainer = torch.optim.SGD(net.parameters(), lr=0.1)

训练

接下来我们调用 3.6节 中定义的训练函数来训练模型。

num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

和以前一样,这个算法使结果收敛到一个相当高的精度,而且这次的代码比之前更精简了。

小结

  • 使用深度学习框架的高级API,我们可以更简洁地实现softmax回归。
  • 从计算的角度来看,实现softmax回归比较复杂。在许多情况下,深度学习框架在这些著名的技巧之外采取了额外的预防措施,来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。

练习

1. 尝试调整超参数,例如批量大小、迭代周期数和学习率,并查看结果。

解:
1)批量大小:较大的批量大小可以提高训练过程中的计算效率,因为可以利用并行计算的能力,同时还可以降低梯度的方差,增加梯度的稳定性。但是,较大的批量大小可能导致模型陷入局部最小值,因为每次更新梯度的方向都是基于一批样本的平均梯度。另外,较大的批量大小可能需要更多的内存,限制了可用的批量大小。因此,选择适当的批量大小需要在计算效率和模型性能之间进行权衡。
2)迭代周期数:迭代周期数指的是完整遍历训练数据集的次数。较多的迭代周期数可以增加模型的训练时长,允许模型更好地学习数据的模式和特征,从而提高测试精度。然而,如果迭代周期数过大或过拟合现象较为严重,模型可能在训练数据上表现很好,但在测试数据上表现较差。因此,选择适当的迭代周期数需要在模型性能和训练时间之间进行平衡。
3)学习率:较大的学习率可以加快模型收敛速度,但可能导致优化过程不稳定,甚至无法收敛;而较小的学习率可以提高模型稳定性,但可能需要更多的迭代周期数才能收敛。选择合适的学习率通常需要进行调试和调优,可以使用学习率调度策略(如学习率衰减)来进一步优化学习率的选择。适当的学习率可以帮助模型达到更好的测试精度,同时控制训练时间。
以下是设置不同超参数时,测试精度和运行时间的比较试验:

import torch
from torch import nn
from d2l import torch as d2l

import itertools
import time
import matplotlib.pyplot as plt



# 定义超参数的搜索范围
batch_sizes = [64, 256, 512]
num_epochs = [1, 10, 50]
lrs = [0.01, 0.1, 1]

# 定义测试精度相关参数
accuracies = []
accuracy = 0
best_accuracy = 0
best_accuracy_iteration = 0

# 定义运行时间相关参数
run_times = []
run_time = 0
best_runtime = float('inf')
best_runtime_iteration = 0

#定义参数和迭代次数相关参数
hyperparameters = []
best_hyperparameters = {}
Iterations = []

# 使用嵌套循环遍历所有超参数组合
for i, (batch, epochs, lr) in enumerate(itertools.product(
    batch_sizes, num_epochs, lrs)):

    # 记录迭代次数
    Iterations.append(i+1)

    # 记录开始时间
    start_time = time.time()

    train_iter, test_iter = d2l.load_data_fashion_mnist(batch)

    net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
    net.apply(init_weights)
    updater = torch.optim.SGD(net.parameters(), lr=lr)
    loss = nn.CrossEntropyLoss()

    # 在训练集上进行训练
    for epoch in range(epochs):
        d2l.train_epoch_ch3(net, train_iter, loss, updater)
        # 在验证集上评估模型
    accuracy = d2l.evaluate_accuracy(net, test_iter)
    accuracies.append(accuracy)

    # 记录结束时间
    end_time = time.time()

    # 计算运行时间
    run_time = end_time - start_time
    run_times.append(run_time)

    hyperparameters.append({'batch_size': batch,
                            'num_epochs': epochs,
                            'learning_rate': lr})

    # 更新最高的 accuracy 和对应的迭代条件和迭代次数
    if accuracy > best_accuracy:
        best_accuracy = accuracy
        best_accuracy_hyperparameters = {'batch_size': batch,
                                         'num_epochs': epochs,
                                         'learning_rate': lr}
        best_accuracy_iteration = i

    # 更新最短的 runtime 和对应的迭代条件和迭代次数
    if run_time < best_runtime:
        best_runtime = run_time
        best_runtime_hyperparameters = {'batch_size': batch,
                                        'num_epochs': epochs,
                                        'learning_rate': lr}
        best_runtime_iteration = i

# 绘制 Accuracy 柱状图
plt.figure(figsize=(10, 5))
plt.barh(range(len(accuracies)), accuracies)
plt.yticks(range(len(Iterations)), Iterations)
plt.xlabel('Accuracy')
plt.ylabel('Iterations')
plt.title('Accuracy per Iteration')
plt.show()

print("Best accuracy: {:.2f}".format(best_accuracy))
print("Hyperparameters achieving the best accuracy:", best_accuracy_hyperparameters, 
     "in Iteration ", best_accuracy_iteration)

# 绘制 Runtime 柱状图
plt.figure(figsize=(10, 5))
plt.barh(range(len(run_times)), run_times)
plt.yticks(range(len(Iterations)), Iterations)
plt.xlabel('Runtime (seconds)')
plt.ylabel('Iterations')

plt.title('Runtime per Iteration')
plt.show()

print("Shortest runtime: {:.2f} seconds".format(best_runtime))
print("Hyperparameters achieving the shortest runtime:", best_runtime_hyperparameters, 
     "in Iteration ", best_runtime_iteration)

Best accuracy: 0.84
Hyperparameters achieving the best accuracy: {'batch_size': 256, 'num_epochs': 50, 'learning_rate': 0.1} in Iteration  16

Shortest runtime: 5.54 seconds
Hyperparameters achieving the shortest runtime: {'batch_size': 512, 'num_epochs': 1, 'learning_rate': 1} in Iteration  20

2. 增加迭代周期的数量。为什么测试精度会在一段时间后降低?我们怎么解决这个问题?

解:
因为样本复杂度小于模型复杂度,出现过拟合导致的。
随着迭代周期的数量增加,模型不断去接近样本规律,但到了某一迭代次数后,模型表达能力过剩,会去学习一些只能满足训练样本的非共性特征,即过拟合,从而降低模型的泛化能力,导致测试精度降低。
可以通过以下方法解决:
1)数据增强(Data Augmentation):通过对训练数据进行各种随机变换(如旋转、平移、缩放、翻转等),扩增训练数据的多样性,可以降低过拟合风险。
2)正则化(Regularization):通过在损失函数中引入正则化项(如L1正则化、L2正则化),限制模型参数的大小,防止模型过于复杂而出现过拟合。
3)早停(Early Stopping):在训练过程中监控验证集的性能,当验证集性能不再提升时,停止训练,避免过拟合。
4)Dropout:通过在训练过程中随机将一部分神经元的输出置为0,来减少神经元之间的依赖关系,降低过拟合。
5)模型复杂度调整:减少模型的复杂度,可以通过减少网络层数、减少神经元个数等方式,降低过拟合风险。
6)数据集分割:合理划分训练集、验证集和测试集,用于模型的训练、调参和评估,以确保模型在未知数据上的泛化能力。

  • 46
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

scdifsn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值