零基础-动手学深度学习-3.6softmax回归的从零开始实现

鄙人生医转码,道行浅薄,请多谅解~

感觉这章的内容超量,代码和详解都非常长,细嚼慢咽ing~

首先导入需要的库和上一章讲的训练和测试集MNIST(相比于原码我多加了一个库后面用)

import torch
import matplotlib.pyplot as plt
from IPython import display
from d2l import torch as d2l


batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

一、初始化模型参数

num_inputs = 784#输入一个拉长的向量
num_outputs = 10#模型输出维度为10
#拉长向量会损失很多空间信息,这就是卷积神经网络要考虑的事情了
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)#定义权重
b = torch.zeros(num_outputs, requires_grad=True)#偏移长度同理

这一步和线性回归操作一样,但是我们需要明确w,b矩阵的大小关系,不然容易搞混,比如说w为了在和x相乘后得出的数据与分类模型的类别数相同,我们要其列数为10也就是output

二、定义softmax操作

#仅是回顾一下沿着特定维度求和的矩阵操作
#X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
#X.sum(0, keepdim=True), X.sum(1, keepdim=True)

回顾后我们来写softmax的代码实现:

def softmax(X):
    X_exp = torch.exp(X)#输入x中每个元素的指数函数
    partition = X_exp.sum(1, keepdim=True)#关键,计算出每个样本的所有类别分数指数和
    #参数 keepdim=True 表示在加和操作后保持维度,结果仍然是二维张量,这在后面的广播机制中起作用
    return X_exp / partition  # 这里应用了广播机制
    #由于 partition 是 [n, 1] 形状的张量,而 X_exp 是 [n, m] 形状的张量(n 是样本数量,m 是类别数量)
    #广播机制会自动将 partition 扩展为 [n, m] 的形状,使得每行的每个元素都被对应的 partition 值除

#我感觉这里有一个比较容易搞混的地方?
#就是为什么sum函数的维度是1而不是0是因为类别分数的归一化是针对每个样本单独进行的
#而不是将所有样本的类别分数一起归一化

这里也再次强调了输出矩阵的格式,我感觉我第一次看李沐的时候没有太关注代码和这些具体实现,二刷的时候就很懵,我现在浅薄的想法就是,输入的样本数作为行数,然后和w相乘后以类别数作为列数。

三、定义模型

定义模型就很简单了直接导入上面的softmax操作:

def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
#reshape 是为了确保输入的形状与权重矩阵 W 的形状兼容,从而进行正确的矩阵运算

这里确保了输入的形状兼容性,但我寻思之前我们写w的正态分布不是已经规定好了吗?不懂

四、定义损失函数

需要先理解一个格式:

#利用y作为y_hat概率中的索引
#y = torch.tensor([0, 2])
#y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
#y_hat[[0, 1], y]

 然后再写交叉熵损失函数:

#实现交叉熵损失函数
def cross_entropy(y_hat, y):
    return - torch.log(y_hat[range(len(y_hat)), y])
#y_hat 是模型的输出概率分布,通常是经过 Softmax 处理后的张量,形状为 (num_samples, num_classes)
#y 是真实标签,通常是一个包含每个样本对应类别索引的一维张量,形状为 (num_samples,)
#这行代码通过索引选择 y_hat 中每个样本对应类别的预测概率。例如,如果 y 中的某个值是 2,则会选择 y_hat 中第 i 行的第 2 列的值

五、分类精度

#将预测类别和真实y元素进行比较
def accuracy(y_hat, y):  #@save
    """计算预测正确的数量"""
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
    #这行代码检查 y_hat 的形状。如果 y_hat 是一个二维张量(表示有多个类别的预测概率),则需要将其转换为类标签
        y_hat = y_hat.argmax(axis=1)
        #argmax(axis=1) 计算 y_hat 每行(每个样本)最大值的索引,即选择预测概率最高的类别。这一步将概率分布转化为具体的类别标签
    cmp = y_hat.type(y.dtype) == y
    #这里将 y_hat 转换为与真实标签 y 相同的数据类型,然后进行逐元素比较,得到一个布尔张量 cmp,其中每个元素表示该样本的预测是否正确
    return float(cmp.type(y.dtype).sum())
    #cmp.type(y.dtype) 将布尔张量转换为相同类型的数值(通常是 0 和 1),然后 sum() 计算正确预测的样本数量

这里的注释我写了非常多,因为我确实对概念理解不全,对softmax操作的目的不清,当我们运用softmax去处理了输入向量后,得到的是一个样本的在每个不同类别的概率,这个时候它是要划分导不同类别去的,而不是直接进行概率运算,所以这里分类精度就是进行了一个划分的判断操作,当判断为这个二分类及以上问题时,我们把最大概率类别的索引取出来,然后和真实标签进行布尔操作,返回正确预测的样本数量。

单独的这个算法是不够的,还需要适配于我们的回归分类问题当中:

#给我一个模型,我算一算在这个数据迭代器上面的精度
#标准说法是:计算给定模型 net 在指定的数据集 data_iter 上的精度
def evaluate_accuracy(net, data_iter):  #@save
    """计算在指定数据集上模型的精度"""
    if isinstance(net, torch.nn.Module):#检查 net 是否是 PyTorch 的 torch.nn.Module 类型(即一个神经网络模型)
        net.eval()  # 如果 net 是一个 PyTorch 神经网络模型将模型设置为评估模式
    metric = Accumulator(2)  # 创建一个 Accumulator 对象来记录评估中的累积结果:正确预测数、预测总数
    with torch.no_grad():#因为在评估模式下,我们只需要前向传播得到模型的预测,不需要计算梯度,节省内存并加速推理过程
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())#对每个批次的数据,调用模型 net(X) 来预测输出
            #accuracy(net(X), y):计算模型在该批次上的预测准确数(即预测正确的样本数量)
            #y.numel():计算标签 y 中的元素总数(即该批次中样本的总数)
            #metric.add() 会将正确预测的数量和样本总数累积起来
    return metric[0] / metric[1]
    #这里 metric[0] 表示累积的正确预测数量,metric[1] 表示累积的总样本数量

具体的注释写的很清楚了,就是在指定数据集中测量精度的一个函数~

于此同时注意这里我们写了一个class:

class Accumulator:  #@save
    """在n个变量上初始化、累加、重置、访问"""
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):#*args 表示该方法可以接收任意数量的参数(类似于元组)
        self.data = [a + float(b) for a, b in zip(self.data, args)]
    #add 方法用于将传入的参数累加到 self.data 中
    #zip(self.data, args) 将 self.data 和传入的 args 参数进行一一配对,a 是 self.data 中的当前值,b 是传入的参数
    #对于每个配对的元素,将 a + float(b),将结果存储回 self.data 中。也就是说,add 方法将传入的每个参数累加到对应的 self.data 位置上
    def reset(self):
        self.data = [0.0] * len(self.data)
    #reset 方法用于将所有累加变量重新设置为 0.0。通过将 self.data 重置为与之前相同长度的全 0 列表来实现
    def __getitem__(self, idx):
        return self.data[idx]
    #这是一个特殊方法,用于支持通过索引访问对象中的数据(类似于列表的访问方式)
    #__getitem__ 使得 Accumulator 对象可以像列表一样通过 [] 来访问其中的元素。例如,acc[0] 将返回 self.data 中第一个累加的值

#class是 Python 中用于创建用户定义数据类型的关键字,允许你将数据和功能封装在一个结构中
#list 是 Python 中的一种内置数据结构,表示可变的、有序的元素集合

 因为我是真的小白,三年大学时光都在搞流体力学仿真,单片机,sw建模等乱七八糟的东西(很符合我对生医的印象),所以这里还稍微查了一下class和list的差别,就是class可以设定一些功能,属于更大的封装体系,而list就是简单的c语言的元胞(没说错吧,c艹也不记得多少了)

举个例子这个class干什么的更加直观:

#acc = Accumulator(3)  # 创建一个累加器,用于累加3个变量
#acc.add(1, 2, 3)      # 累加 [1, 2, 3] -> self.data = [1.0, 2.0, 3.0]
#acc.add(4, 5, 6)      # 累加 [4, 5, 6] -> self.data = [5.0, 7.0, 9.0]
#print(acc[0])         # 输出:5.0
#print(acc[1])         # 输出:7.0
#acc.reset()           # 重置为 [0.0, 0.0, 0.0]

六、训练

剩下的就是训练了:

def train_epoch_ch3(net, train_iter, loss, updater):  #@save
    """训练模型一个迭代周期(定义见第3章)"""
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()#训练模式会激活某些层(如 Dropout 和 BatchNorm)的特定行为,与评估模式不同
    # 训练损失总和、训练准确度总和、样本数
    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内置的优化器和损失函数,要自己清除grad
            updater.zero_grad()#先把梯度设为0
            l.mean().backward()#计算损失的平均值并反向传播
            updater.step()#执行优化器的梯度更新
        else:
            # 如果是使用自定义的优化器和损失函数
            l.sum().backward()
            updater(X.shape[0])
            #计算的是损失总和(l.sum())并反向传播,接着通过自定义的 updater 来进行参数更新
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
        #metric.add() 将当前批次的损失总和、准确度和样本总数累加到 Accumulator 中
    # 返回训练损失和训练精度
    return metric[0] / metric[2], metric[1] / metric[2]

        这里没有见过的时这个if对优化器的判断,一个是内置优化器,一个时自定义优化器,这里我有点问题:为什么自定义的优化器是计算损失的总和并反向传播而内置优化器是损失均值,其次是updater(X.shape[0]) 里面还要写X.shape[0]?
1. 损失总和 vs. 损失均值:
        内置优化器通常期望的是对每个样本的损失求平均值来进行梯度计算(l.mean().backward())
这样可以使得损失的量级与批次的大小(batch size)无关,确保模型的更新步长不会因为批次大小的不同而发生较大变化,这就是为什么在使用 PyTorch 内置的优化器时,常常会对损失取平均值。

        自定义优化器可能根据某种特殊需求来设计,在一些场景下,计算损失的总和会更符合需求如果损失的总和用于梯度计算,意味着每个样本的损失都会对梯度更新有直接的影响,这样在小批次或变化的批次大小的情况下,模型的参数更新量与样本数直接相关,这种方式在某些优化场景中,可能需要累积更大的梯度效果,或者与更新的自定义策略结合更为适合。
        但是实际上选择损失总和还是平均值,通常取决于具体优化器的设计和所需的更新策略。

2. 为什么 updater(X.shape[0]) 需要传入 X.shape[0]?
        updater 是一个自定义优化器,它可能是基于批次大小来调整模型的参数更新,自定义的优化器可能会根据当前批次的大小动态调整参数的更新幅度。比如,如果批次比较大,可能会选择更小的更新步长;而批次较小时,步长可以稍微增大,自定义的优化器可能会通过批次大小进行某种形式的归一化或缩放,使得损失总和和梯度之间的比例适当。
        PyTorch 自带的优化器(如 SGD, Adam, RMSprop 等)是高度优化和通用的,它们能够满足绝大多数标准机器学习和深度学习任务的需求

剩下好像没啥好讲的操作了,写了一点关于断言的注释其他的没啥了,真复杂啊不是我说。

evaluate_data = []#写一个列表来存数据
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
    """训练模型(定义见第3章)"""
    #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)#训练误差拿下来
        train_loss, train_acc = train_metrics
        test_acc = evaluate_accuracy(net, test_iter)#评估测试精度
        #animator.add(epoch + 1, train_loss + train_acc + test_acc)
        #print(f'epoch {epoch + 1}, loss {float(test_acc):f}')既然不能自动画图那就可以手动导出数据
        evaluate_data.append((test_acc, train_acc, train_loss))
    assert train_loss < 0.5, train_loss
    assert train_acc <= 1 and train_acc > 0.7, train_acc
    assert test_acc <= 1 and test_acc > 0.7, test_acc

#从 train_metrics 中提取训练损失和训练准确度,并进行一系列断言
#这些断言确保模型在训练过程中表现良好,符合预期的性能标准
#assert 语句用于在程序中检查某个条件是否为真。如果条件为假,程序会抛出 AssertionError,并可以附加一个错误信息

lr = 0.1
#小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1
def updater(batch_size):
    return d2l.sgd([W, b], lr, batch_size)

num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)


column1 = [row[0] for row in evaluate_data]
column2 = [row[1] for row in evaluate_data]
column3 = [row[2] for row in evaluate_data]
x = range(len(evaluate_data))
plt.plot(x, column1, label='test_acc', marker='o')
plt.plot(x, column2, label='train_acc', marker='s')
plt.plot(x, column3, label='train_loss', marker='^')
plt.title('Data Plot')
plt.xlabel('epoch')
plt.ylabel('acc%loss')
plt.legend()
plt.show()

#预测操作
def predict_ch3(net, test_iter, n=6):  #@save
    """预测标签(定义见第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])

predict_ch3(net, test_iter)

七、一点补充

实际上就是一个在jupyter上能运行的一个动画绘画的一个plot class,我自己改了改用了python的绘图,不是动画的,但不知道咋的出了点多线程运行问题,反正最终图没画出来,大家还是用notebook的原代码吧····我只是自己想操作一下 

#定义一个在动画中绘制数据的实用程序类, 它能够简化本书其余部分的代码
class Animator:  #@save
    """在动画中绘制数据"""
    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()
        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()
        display.display(self.fig)#显示图形并清除之前的输出,以便实现动画效果
        display.clear_output(wait=True)#但注意这些功能是为 Jupyter Notebook 的交互式环境设计的,可以在 Notebook 中实现图像的动态更新
    #所以在python中用不了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值