3.4 softmax回归

3.4.1 分类问题

独热编码表示类别,独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。例如:(1,0,0)对应于“猫”、 (0,1,0)对应于“鸡”、 (0,0,1)对应于“狗”。

3.4.2 网络架构

为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。 每个输出对应于它自己的仿射函数。

由于我们有4个特征和3个可能的输出类别, 我们将需要12个标量来表示权重(带下标的w), 3个标量来表示偏置(带下标的b)。softmax回归的输出层也是全连接层

特征是X=(x1,x2,x3,x4)。

3.4.3 全连接层的参数开销

3.4.4 softmax运算

通过softmax计算,会将输出的数据变为非负且和为一,符合概率的定义,公式如下:

尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。 因此,softmax回归是一个线性模型。

3.4.5 小批量样本的矢量化

好好理解一下上面各个矩阵的维度为什么是这样,可以参考鱼书。例如:输出概率矩阵是n*q,可以这样理解,要是预测一个样本,那么这个预测的结果是一个一维的概率向量,大小为q,因为一共有q个类别。举个例子,q=3时,预测输出向量为(0.1,0.7,0.2),说明预测结果为第二个类别的概率为70%。

3.4.6 损失函数

3.4.6.1 对数似然

参考概率论数理逻辑的知识。

3.4.6.2 损失函数

分类问题中最常使用的损失函数是交叉熵损失。

3.5 图像分类数据集

MNIST数据集 (LeCun et al., 1998) 是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。 我们将使用类似但更复杂的Fashion-MNIST数据集 (Xiao et al., 2017)。

1.读取数据集:

Fashion-MNIST由10个类别的图像组成, 每个类别由训练数据集(train dataset)中的6000张图像 和测试数据集(test dataset)中的1000张图像组成。 因此,训练集和测试集分别包含60000和10000张图像。 

每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。 

Fashion-MNIST中包含的10个类别,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。 以下函数用于在数字标签索引及其文本名称之间进行转换。

最后一行代码要从右往左看,遍历labels,将每个标签i,先转换为整数。然后从text_labels中获取对应的文本标签。例如:如果输入labels = [0, 2, 5],函数将返回['t-shirt', 'pullover', 'sandal']。这个函数的作用是将 Fashion-MNIST 数据集中的数字标签(0-9)转换为更易理解的文本描述,方便在展示或分析结果时使用。

下面是绘制一个网格来可视化样本:

其中,参数里的值分别表示需要显示的图像列表、图像的行数、图像的列数、标题和图像的缩放因子(控制显示图像的尺寸)。figsize = (num_cols * scale, num_rows * scale)是根据列数、行数和缩放因子计算整个图像区域的大小,结果是一个元组(宽度, 高度)。

使用d2l库中的 matplotlib 接口创建子图网格,num_rows和num_cols指定了子图的行数和列数,figsize为整个图像的大小,返回的是一个包含所有子图坐标轴的数组axes。扁平化坐标轴数组,方便后续通过循环遍历所有子图。

For循环结构:

在循环里首先检查图片的类型,如果图片是张量,先将其转变为numpy类型后再显示,然后是PIL型,则直接显示。接着,隐藏坐标轴。

if titles::检查是否提供了标题列表。如果提供了标题,就为当前子图ax设置标题,标题内容为titles[i]

总结来说,这段代码的作用是:遍历所有图像和对应的子图位置,根据图像类型进行适当转换后显示,隐藏不必要的坐标轴,并在有需要时添加标题,最终形成整洁的图像网格展示。

Tips: 代码里要加import matplotlib.pyplot as plt和plt.show()才能展示图像,如下所示:

以上代码:创建一个数据加载器,从mnist_train(训练数据集)中每次加载 18 个样本。Iter将数据加载器转换为迭代器。next获取迭代器的下一个元素,即一个批次的数据。得到的X是图像特征,y为对应标签。

X.reshape(18, 28, 28):将图像数据重塑为 (18, 28, 28) 的形状,即 18 张 28×28 像素的图像。通过之前定义的get_fashion_mnist_labels函数将数字标签转换为文本标签,作为图像标题。

2.读取小批量

# 测试读取所需时间

当直接把代码放里面,会报错,原因是:

放到main函数里,便可解决,得运行时间:

3.整合所有组件

现在我们定义load_data_fashion_mnist函数,用于获取和读取Fashion-MNIST数据集。 这个函数返回训练集和验证集的数据迭代器。 此外,这个函数还接受一个可选参数resize,用来将图像大小调整为另一种形状。

  • 创建了一个转换列表,首先添加了ToTensor()转换,这个转换会将 PIL 图像或 NumPy 数组转换为 PyTorch 张量,并将像素值从 0-255 归一化到 0-1 之间
  • 如果指定了resize参数,则在ToTensor()之前插入Resize(resize)转换,用于将图像调整为指定大小
  • 使用transforms.Compose()将所有转换组合成一个可调用的转换函数

下面,我们通过指定resize参数来测试load_data_fashion_mnist函数的图像大小调整功能。

3.6 softmax回归的从零开始实现


3.6.1初始化模型参数

原始数据集中的每个样本都是28*28的图像。 本节将展平每个图像,把它们看作长度为784的向量。 在后面的章节中,我们将讨论能够利用图像空间结构的特征但现在我们暂时只把每个像素位置看作一个特征

回想一下,在softmax回归中,我们的输出与类别一样多。 因为我们的数据集有10个类别,所以网络输出维度为10。 因此,权重将构成一个784*10的矩阵, 偏置将构成一个1*10的行向量。 与线性回归一样,我们将使用正态分布初始化我们的权重W,偏置初始化为0。

3.6.2 定义softmax操作

如果X是一个形状为(2, 3)的张量,我们对列进行求和, 则结果将是一个具有形状(3,)的向量。 当调用sum运算符时,我们可以指定保持在原始张量的轴数,而不折叠求和的维度。 这将产生一个具有形状(1, 3)的二维张量。

  • keepdim=True 表示保持原有的维度信息(不压缩维度)

注意以下的代码是对行求和,是因为小批量数据中每个样本是一行。

  1. X_exp = torch.exp(X):对输入张量的每个元素计算指数值,防止出现负数概率
  2. partition = X_exp.sum(1, keepdim=True):沿着第 1 维(行方向)求和,keepdim=True 保持维度以便后续广播
  3. return X_exp / partition:通过广播机制,将每个元素除以所在行的总和,得到归一化的概率分布

对于任何随机输入,我们将每个元素变成一个非负数。 此外,依据概率原理,每行总和为1。如下代码所示:

注意,虽然这在数学上看起来是正确的,但我们在代码实现中有点草率。 矩阵中的非常大或非常小的元素可能造成数值上溢或下溢,但我们没有采取措施来防止这点。

3.6.3 定义模型

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

3.6.4 定义损失函数

交叉熵采用真实标签的预测概率的负对数似然。下面,我们创建一个数据样本y_hat,其中包含2个样本在3个类别的预测概率, 以及它们对应的标签y。 有了y,我们知道在第一个样本中,第一类是正确的预测; 而在第二个样本中,第三类是正确的预测。

代码和结果如下:

下面两段代码中:y_hat[[0, 1], y]和y_hat[range(len(y_hat)), y]的效果一样,y是对应的标签,就是说每个样本的真实标签是什么。像下面的y取两个值0和2,表示第一个样本的标签是0,即第一类。

注:range(len(y_hat)):生成行索引,对应每个样本(例如 y_hat 有 2 个样本时,生成 [0, 1])

3.6.5 分类精度

当预测与标签分类y一致时,即是正确的。 分类精度即正确预测数量与总预测数量之比。 虽然直接优化精度可能很困难(因为精度的计算不可导), 但精度通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总会关注它。

为了计算精度,我们执行以下操作。 首先,如果y_hat是矩阵,那么假定第二个维度存储每个类的预测分数。 我们使用argmax获得每行中最大元素的索引来获得预测类别。 然后我们将预测类别与真实y元素进行比较。 由于等式运算符“==”对数据类型很敏感, 因此我们将y_hat的数据类型转换为与y的数据类型一致。 结果是一个包含0(错)和1(对)的张量。 最后,我们求和会得到正确预测的数量。

我们将继续使用之前定义的变量y_hat和y分别作为预测的概率分布和标签。 可以看到,第一个样本的预测类别是2(该行的最大元素为0.6,索引为2),这与实际标签0不一致。 第二个样本的预测类别是2(该行的最大元素为0.5,索引为2),这与实际标签2一致。 因此,这两个样本的分类精度率为0.5。其实.accuracy函数就是返回正确预测出的数量。

y_hat是一个二维矩阵,其中行表示每个样本,列表示改样本预测为第i类别的概率是多少。

同样,对于任意数据迭代器data_iter可访问的数据集, 我们可以评估在任意模型net的精度。

net:要评估的神经网络模型

data_iter:数据集迭代器(可以是验证集或测试集)

对于 PyTorch 的nn.Module类型模型,调用eval()方法将其设置为评估模式

评估模式会禁用 dropout、批归一化等训练时的特殊行为,确保评估结果的一致性

metric = Accumulator(2) :创建一个包含 2 个元素的累加器,这两个元素分别用于存储:模型预测正确的样本数、总预测样本数。

torch.numel() 是一个内置函数,用于计算张量中的元素总数。

3.6.6 训练

 首先,我们定义一个函数来训练一个迭代周期。 请注意,updater是更新模型参数的常用函数,它接受批量大小作为参数。 它可以是d2l.sgd函数,也可以是框架的内置优化函数。

其中,if isinstance(net, torch.nn.Module): 是判断该网络是不是 PyTorch 内置的 torch.nn.Module 类的实例,是的话调用 net.train() 方法将模型设置为训练模式。第二处是判断优化器 updater 是否为 PyTorch 内置的 torch.optim.Optimizer 类的实例。

如果是内置优化器(如 SGD、Adam 等):先清空之前的梯度,计算损失的平均值并反向传播求梯度,updater.step():根据梯度更新模型参数。

如果是自定义优化器:先计算损失的总和并反向传播求梯度,然后调用自定义优化器,传入批次大小(用于梯度平均)。

问:为什么使用内置优化器时,计算损失的时候要用平均值来去求梯度,而自定义优化器计算损失的时候要用其总和去求梯度?

核心原因:梯度缩放与批次大小的适配

内置优化器使用平均值的原因:默认假设损失已 经过批次大小的平均。而自定义优化器通常手动实现参数更新逻辑,需要显式处理批次大小对梯度的影响。

接下来我们实现一个训练函数, 它会在train_iter访问到的训练数据集上训练一个模型net。 该训练函数将会运行多个迭代周期(由num_epochs指定)。 在每个迭代周期结束时,利用test_iter访问到的测试数据集对模型进行评估。 我们将利用Animator类来可视化训练进度。

  • xlim=[1, num_epochs]:x 轴的显示范围从 1 到总轮数num_epochs。
  • ylim=[0.3, 0.9]:y 轴的显示范围固定在 0.3 到 0.9(适合展示损失或准确率等指标)。
  • legend=['train loss', 'train acc', 'test acc']:图例标签,分别对应 “训练损失”“训练准确率”“测试准确率” 三条曲线。
  • 第二个参数train_metrics + (test_acc,):将训练指标(损失、准确率)和测试准确率合并为一个元组(train_loss, train_acc, test_acc),作为 y 轴数据,对应三条曲线。

其中,断言用于验证训练结果的合理性,避免出现明显异常(如模型未收敛、准确率超出 0-1 范围等)。如果断言失败,会抛出异常并显示具体数值。

接下来,作为一个从零开始的实现,我们使用 3.2节中定义的 小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。

3.6.7 softmax回归的简洁实现

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

# PyTorch不会隐式地调整输入的形状。因此,

# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状

  • init_weights函数:自定义的权重初始化函数
    • 通过type(m) == nn.Linear判断当前层是否为线性层
    • 对线性层使用nn.init.normal_进行初始化,将权重参数初始化为均值为 0、标准差为 0.01 的正态分布
  • net.apply(init_weights):将初始化函数应用到整个网络
    • apply方法会递归地对网络中的所有子模块执行传入的函数
    • 这里只会对网络中的nn.Linear层进行权重初始化,展平层nn.Flatten没有需要初始化的参数

图像分类代码和softmax从零开始实现代码:
 

# 图像分类数据集
import torch
import torchvision
from d2l import torch as d2l
from torchvision import transforms
from torch.utils import data
import numpy as np
import time
import matplotlib.pyplot as plt

class Timer:  #@save
    """记录多次运行时间"""
    def __init__(self):
        self.times = []
        self.start()

    def start(self):
        """启动计时器"""
        self.tik = time.time()

    def stop(self):
        """停止计时器并将时间记录在列表中"""
        self.times.append(time.time() - self.tik)
        return self.times[-1]

    def avg(self):
        """返回平均时间"""
        return sum(self.times) / len(self.times)

    def sum(self):
        """返回时间总和"""
        return sum(self.times)

    def cumsum(self):
        """返回累计时间"""
        return np.array(self.times).cumsum().tolist()


# 读取数据集
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(root='../data',train= True, download=True,transform=trans)
mnist_test = torchvision.datasets.FashionMNIST(root='../data',train= False, download=True,transform=trans)

print(len(mnist_train))
print(len(mnist_test))

print(mnist_train[0].__sizeof__())
print(mnist_train[0][0].shape)

# 用于在数字标签索引及其文本名称之间进行转换
def get_fashion_mnist_labels(labels):  #@save
    """返回Fashion-MNIST数据集的文本标签"""
    text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
                   'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
    return [text_labels[int(i)] for i in labels]

# 可视化样本
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):  #@save
    """绘制图像列表"""
    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])
    plt.show()
    return axes




X,y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
show_images(X.reshape(18,28,28),2,9,titles=get_fashion_mnist_labels(y))


# 读取小批量数据
batch_size = 256
def get_dataloader_workers():  #@save
    """使用4个进程来读取数据"""
    return 4

train_iter = data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=get_dataloader_workers())

# 整合所有组件
def load_data_fashion_mnist(batch_size,resize=None):
    trans = [transforms.ToTensor()]
    if resize is not None:
        trans.insert(0,transforms.Resize(resize))
    trans = transforms.Compose(trans)
    minst_train = torchvision.datasets.FashionMNIST(root="../data",train=True,download=True,transform=trans)
    minst_test = torchvision.datasets.FashionMNIST(root="../data",train=False,download=True,transform=trans)
    return (data.DataLoader(minst_train,batch_size,shuffle=True,num_workers=get_dataloader_workers()),
            data.DataLoader(minst_test,batch_size,shuffle=False,num_workers=get_dataloader_workers()))



# 测试读取所需时间
if __name__ == '__main__':
    timer = d2l.Timer()
    for X, y in train_iter:
        continue
    print(f'{timer.stop():.2f} sec')

    iter_train, iter_test = load_data_fashion_mnist(32,resize=64)
    for X,y in iter_train:
        print(X.shape,X.dtype,y.shape,y.dtype)
        break

import torch
from IPython import display
from d2l import torch as d2l
from d2l.torch import Accumulator

import matplotlib.pyplot as plt
# 解决本地环境图像不弹出、Jupyter 环境图像不嵌入的问题
plt.rcParams['font.sans-serif'] = ['SimHei']  # 解决中文标签乱码(如需要显示中文)
plt.rcParams['axes.unicode_minus'] = False    # 解决负号显示异常


# softmax回归的从零开始实现
# 结合此目录下的trail04代码

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

num_inputs = 784
num_outputs = 10

w = torch.normal(0,0.01,size=(num_inputs,num_outputs),requires_grad=True)
b = torch.zeros(num_outputs,requires_grad=True)

X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print(X.sum(0, keepdim=True), X.sum(1, keepdim=True))

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

x = torch.normal(0,1,(2,4))
y = softmax(x)
print(x)
print(y)
print(y.sum(dim=1,keepdim=True))

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

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 corss_entropy(y_hat,y):
    return - torch.log(y_hat[range(len(y_hat)),y])

ce = corss_entropy(y_hat,y)
print(ce)

#计算分类精度,即正确预测出的结果占总数的比例
def accuracy(y_hat,y):
    if len(y_hat.shape)>1 and y_hat.shape[1]>1:  #当y_hat是二维数组时,且列数大于1时时
        y_hat= y_hat.argmax(axis=1)  #找到每行的最大值,即表示改行样本所预测出的类别
    cmp =  y_hat.type(y.dtype) == y  #将预测值 y_hat 的数据类型转换为与真实标签 y 相同
    return float(cmp.type(y.dtype).sum())  #将布尔数组 cmp 转换为与 y 相同的数据类型(通常会转为数值型)

print(accuracy(y_hat,y)/len(y))

#用来评估指定数据集在任意模型net的精度
def evaluate_accuracy(net,data_iter): #@save
    """计算在指定数据集上模型的精度"""
    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]

# 训练一个周期
def  train_epoch_ch3(net,train_iter,loss,updater):
    # 将模型设置为训练模式
    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):
            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]


# 在动画中绘制图表的类:
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)

# 训练函数
def train_ch3(net,train_iter,test_iter,loss,num_epochs,updater): #@save
    animator = Animator(xlabel='eopch',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,))
    train_loss,train_acc = train_metrics
    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

#@save
def sgd(params, lr, batch_size):  #@save
    """小批量随机梯度下降"""
    for param in params:
        param[:] = param - lr * param.grad / batch_size


lr =0.1
def updater(batch_size):
    return d2l.sgd([w,b],lr,batch_size)


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

# 预测模型
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])

predict_ch3(net,test_iter)

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐