Softmax回归

目录

一、Softmax回归关键思想

1、回归问题和分类问题的区别

2、Softmax回归模型

3、Softmax函数

4、交叉熵损失函数

二、图像分类数据集

1、读取数据集

2、读取小批量数据

3、整合所有组件

三、Softmax回归的从零开始实现

1、读取数据集

2、初始化模型参数

3、定义Softmax操作

4、定义模型

5、定义损失函数

6、分类精度(分类正确率)

7、训练

8、预测

四、Softmax回归的简洁实现

1、读取数据集

2、定义模型

3、初始化模型参数

4、定义损失函数

5、定义优化算法

6、训练

7、疑问

参考文献


一、Softmax回归关键思想

1、回归问题和分类问题的区别

       Softmax回归虽然叫“回归”,但是它本质是一个分类问题。回归是估计一个连续值,而分类是预测一个离散类别。

2、Softmax回归模型

       Softmax回归跟线性回归一样将输入特征与权重做线性叠加。与线性回归的一个主要不同在于,Softmax回归的输出值个数等于标签里的类别数。比如一共有4种特征和3种输出动物类别(猫、狗、猪),则权重包含12个标量(带下标的$w$),偏差包含3个标量(带下标的$b$),且对每个输入计算$ O_1,O_2,O_3 $这三个输出:

$ \begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{12} + x_3 w_{13} + x_4 w_{14} + b_1,\\ o_2 &= x_1 w_{21} + x_2 w_{22} + x_3 w_{23} + x_4 w_{24} + b_2,\\ o_3 &= x_1 w_{31} + x_2 w_{32} + x_3 w_{33} + x_4 w_{34} + b_3. \end{aligned} $

最后,再对这些输出值进行Softmax函数运算

       softmax回归同线性回归一样,也是一个单层神经网络。由于每个输出$ O_1,O_2,O_3 $的计算都要依赖于所有的输入$ X_1,X_2,X_3,X_4 $,所以softmax回归的输出层也是一个全连接层。

3、Softmax函数

       Softmax用于多分类过程中,它将多个神经元的输出(比如$ O_1,O_2,O_3 $)映射到(0,1)区间内,可以看成概率来理解,从而来进行多分类!它通过下式将输出值变换成值为正且和为1的概率分布:

$\widehat{y_1},\widehat{y_2},\widehat{y_3} = \mathrm{softmax}(o_1,o_2,o_3)$

其中:

$ \widehat{y}_j=\frac{\exp \left( o_1 \right)}{\sum\limits_{i=1}^3{\exp \left( o_i \right)}} $, $ \widehat{y}_j=\frac{\exp \left( o_2 \right)}{\sum\limits_{i=1}^3{\exp \left( o_i \right)}} $, $ \widehat{y}_j=\frac{\exp \left( o_3 \right)}{\sum\limits_{i=1}^3{\exp \left( o_i \right)}} $

       容易看出 $ \widehat{y_1}+\widehat{y_2}+\widehat{y_3}=1 $ 且 $ \widehat{y_1}+\widehat{y_2}+\widehat{y_3}=1 $,因此 $ \widehat{y_1},\widehat{y_2},\widehat{y_3} $ 是一个合法的概率分布。此外,我们注意到:

$ arg\max\text{\ }o_i=arg\max\text{\ }\widehat{y_i} $

 因此softmax运算不改变预测类别输出。

       下图可以更好的理解Softmax函数,其实就是取自然常数e的指数相加后算比例,由于自然常数的指数($ e^x $)在$ \left( -\infty ,+\infty \right) $单调递增,因此softmax运算不改变预测类别输出。

4、交叉熵损失函数

       假设我们希望根据图片动物的轮廓、颜色等特征,来预测动物的类别,有三种可预测类别:猫、狗、猪。假设我们当前有两个模型(参数不同),这两个模型都是通过sigmoid/softmax的方式得到对于每个预测结果的概率值:

模型1:

模型1
预测真实是否正确
0.30.30.4001正确
0.30.40.3010正确
0.10.20.7100错误

       模型评价:模型1对于样本1和样本2以非常微弱的优势判断正确,对于样本3的判断则彻底错误。

模型2:

模型2
预测真实是否正确
0.10.20.7001正确
0.10.70.2010正确
0.30.40.3100错误

       模型评价:模型2对于样本1和样本2判断非常准确,对于样本3判断错误,但是相对来说没有错得太离谱。

       好了,有了模型之后,我们需要通过定义损失函数来判断模型在样本上的表现了,那么我们可以定义哪些损失函数呢?我们可以先尝试使用以下几种损失函数,然后讨论哪种效果更好。

(1)Classification Error(分类错误率)

       最为直接的损失函数定义为:

$ classification\ error=\frac{count\ of\ error\ items}{count\ of\ all\ items} $

模型1:$ classification\ error=\frac{1}{3} $

模型2:$ classification\ error=\frac{2}{3} $

       我们知道,模型1模型2虽然都是预测错了1个,但是相对来说模型2表现得更好,损失函数值照理来说应该更小,但是,很遗憾的是,classification error 并不能判断出来,所以这种损失函数虽然好理解,但表现不太好。

(2)Mean Squared Error(均方误差MSE)

       均方误差损失也是一种比较常见的损失函数,其定义为:

$ MSE=\frac{1}{n}\sum_i^n{\left( \widehat{y_i}-y_i \right) ^2} $

模型1:

对所有样本的loss求平均:

模型2:

对所有样本的loss求平均:

       我们发现,MSE能够判断出来模型2优于模型1,那为什么不采样这种损失函数呢?主要原因是在分类问题中,使用sigmoid/softmx得到概率,配合MSE损失函数时,采用梯度下降法进行学习时,会出现模型一开始训练时,学习速率非常慢的情况(损失函数 | Mean-Squared Loss - 知乎)。

       有了上面的直观分析,我们可以清楚的看到,对于分类问题的损失函数来说,分类错误率和均方误差损失都不是很好的损失函数,下面我们来看一下交叉熵损失函数的表现情况。

(3)Cross Entropy Loss Function(交叉熵损失函数)

其中:

$M$:类别的数量

$ y_{ic} $:符号函数(0或1),如果样本 i 的真实类别等于 c 取 1,否则取 0

$ p_{ic} $:观测样本 i 属于类别 c 的预测概率

$N$:样本的数量

$log$:其实是$ln$

现在我们利用这个表达式计算上面例子中的损失函数值:

模型1

对所有样本的loss求平均(相当于是求一个batch的平均):

模型2:

对所有样本的loss求平均(相当于是求一个batch的平均):

       可以发现,交叉熵损失函数可以捕捉到模型1和模型2预测效果的差异,因此对于Softmax回归问题我们常用交叉熵损失函数。

      下面两图可以很清晰的反应整个Softmax回归算法的流程:

二、图像分类数据集

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

       在这里我们定义一些函数用于数据的读取与显示,这些函数已经在Python包d2l中定义好了,但为了便于大家理解,这里没有直接调用d2l中的函数。

1、读取数据集

       我们可以通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。

# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,
# 并除以255使得所有像素的数值均在0~1之间
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(
    root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
    root="../data", train=False, transform=trans, download=True)

       Fashion-MNIST由10个类别的图像组成,每个类别由训练数据集(train dataset)中的6000张图像和测试数据集(test dataset)中的1000张图像组成。因此,训练集和测试集分别包含60000和10000张图像。测试数据集不会用于训练,只用于评估模型性能。

print(len(mnist_train), len(mnist_test))
60000 10000

       每个输入图像的高度和宽度均为28像素。数据集由灰度图像组成,其通道数为1。为了简洁起见,本书将高度$h$像素、宽度$w$像素图像的形状记为$h \times w$($h$,$w$)。接下来我们可以打印一下mnist_train的类型和mnist_train的第一个元素。

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

       可以看出mnist_train的类型为<class 'torchvision.datasets.mnist.FashionMNIST'>。mnist_train的第一个元素的类型是<class 'tuple'>,是一个元组,元组第一个元素是转化为tensor后的灰度值,第二个元素是图像所属类别index,这里是9。因为是灰度图,因此channel数量为1,图片长和宽都是28,因此形状是(1,28,28)。

       Fashion-MNIST中包含的10个类别,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)

       以下函数用于在数字标签索引及其文本名称之间进行转换。

def get_fashion_mnist_labels(labels):   # labels:mnist_train和mnist_test里面图像的类别index(数字)
    """返回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]    # 根据index返回文本标签列表('t-shirt', 'trouser'...)

       我们现在可以创建一个函数来可视化这些样本。

def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
    """绘制图像列表"""
    """
    imgs: tensor向量
    num_rows: 画图时的行数
    num_cols: 画图时的列数
    titles: 每张图片的标题
    scales: 因为要将num_rows*num_cols张图片画到一张图上,并且还要添加一些文字,
    因此需要对大图进行一定的缩放才能保证每张小图之间的间隙
    """
    figsize = (num_cols * scale, num_rows * scale)
    # figsize = (num_cols, num_rows)
    _, 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])
    return axes

       以下是训练数据集中前18个样本的图像及其相应的标签。

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))

2、读取小批量数据

       为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器,而不是从零开始创建。在每次迭代中,数据加载器每次都会读取一小批量数据,大小为`batch_size`。通过内置数据迭代器,我们可以随机打乱所有样本,从而无偏见地读取小批量。

batch_size = 256

def get_dataloader_workers():    # 写这一个是因为windows早期版本不支持多进程读取
    """使用4个进程来读取数据"""
    return 4

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

3、整合所有组件

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

def load_data_fashion_mnist(batch_size, resize=None):
    """下载Fashion-MNIST数据集,然后将其加载到内存中"""
    trans = [transforms.ToTensor()]    # 此时的trans是一个列表
    if resize:
        trans.insert(0, transforms.Resize(resize))    # 如果提供了resize参数,则在转换链中插入Resize操作
    trans = transforms.Compose(trans)    # 将一系列的图像转换操作组合成一个转换链。
    # trans是一个由多个图像转换操作组成的列表。它按照列表中的顺序依次应用这些转换操作。
    # 这样可以将多个转换操作组合在一起,以便在加载数据时一次性应用它们。
    mnist_train = torchvision.datasets.FashionMNIST(
        root="../data", train=True, transform=trans, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(
        root="../data", train=False, transform=trans, download=True)
    return (data.DataLoader(mnist_train, batch_size, shuffle=True,
                            num_workers=get_dataloader_workers()),
            data.DataLoader(mnist_test, batch_size, shuffle=False,
                            num_workers=get_dataloader_workers()))

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

train_iter, test_iter = load_data_fashion_mnist(32, resize=64)
for X, y in train_iter:
    print(X.shape, X.dtype, y.shape, y.dtype)
    break
torch.Size([32, 1, 64, 64]) torch.float32 torch.Size([32]) torch.int64

三、Softmax回归的从零开始实现

       softmax回归也是重要的基础,因此应该知道实现softmax回归的细节。我们将使用刚刚引入的Fashion-MNIST数据集,并设置数据迭代器的批量大小为256。

import torch
from IPython import display
from d2l import torch as d2l

1、读取数据集

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

2、初始化模型参数

       和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。原始数据集中的每个样本都是$28 \times 28$的图像。我们将展平每个图像,把它们看作长度为784的向量。在后面的章节中,我们将讨论能够利用图像空间结构的特征,但现在我们暂时只把每个像素位置看作一个特征。

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

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)

3、定义Softmax操作

       在实现softmax回归模型之前,我们简要回顾一下`sum`运算符如何沿着张量中的特定维度工作。给定一个矩阵`X`,我们可以对所有元素求和(默认情况下)。也可以只求同一个轴上的元素,即同一列(轴0)或同一行(轴1)。如果`X`是一个形状为`(2, 3)`的张量,我们对列进行求和,则结果将是一个具有形状`(3,)`的向量。当调用`sum`运算符时,我们可以指定保持在原始张量的轴数,而不折叠求和的维度。这将产生一个具有形状`(1, 3)`的二维张量。

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

回想一下,实现softmax由三个步骤组成:

  • 对每个项求幂(使用torch.exp());
  • 对每一行求和(小批量中每个样本是一行,行数=batch_size,列数=类别数量10),得到每个样本的规范化常数;
  • 将每一行除以其规范化常数,确保结果的和为1。

表达式为:

$ \mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(\mathbf{X}_{ij})}{\sum_k \exp(\mathbf{X}_{ik})}. $

def softmax(X):
    X_exp = torch.exp(X)    # 对torch X的每个元素都求指数
    partition = X_exp.sum(1, keepdim=True)
    return X_exp / partition  # 这里应用了广播机制

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

下面做一个测试:

X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
print(X_prob)
print(X_prob.sum(1))
tensor([[0.3612, 0.2049, 0.0552, 0.0798, 0.2988],
        [0.0243, 0.1031, 0.7090, 0.0167, 0.1469]])
tensor([1.0000, 1.0000])

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

4、定义模型

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

def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)    # reshape之后的X的shape为(256, 784),因为batch_size=256、W.shape[0]=784

5、定义损失函数

       接下来,我们实现前面引入的交叉熵损失函数。交叉熵损失函数可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题的数量。

       回顾一下,交叉熵采用真实标签的预测概率的负对数似然。这里我们不使用Python的for循环迭代预测(这往往是低效的),而是通过一个运算符选择所有元素。下面,我们创建一个数据样本`y_hat`,其中包含2个样本在3个类别的预测概率,以及它们对应的标签`y`。然后使用`y`作为`y_hat`中概率的列索引,我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。

y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
print(y_hat[[0, 1], y])    # 等同于 y_hat[[0, 1], [0, 2]]
print(y_hat[[0, 1], [0, 2]])    # 其实就是根据tensor的索引来找元素,可以同时获得多个元素 [0, 1]:行索引 [0, 2]:列索引
print(y_hat[0, 0])

现在我们只需一行代码就可以实现交叉熵损失函数。

def cross_entropy(y_hat, y):
    # print(y_hat[range(len(y_hat)), y])
    return -torch.log(y_hat[range(len(y_hat)), y]) # len(y_hat)=2, range(len(y_hat))获得了y_hat的长度, 方便根据索引进行查找, 与前面思路一样

print(cross_entropy(y_hat, y))
tensor([2.3026, 0.6931])

6、分类精度(分类正确率)

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

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

def accuracy(y_hat, y):
    """计算预测正确的数量"""
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:     # 如果y_hat(h, w)中h>1、w>1,
        y_hat = y_hat.argmax(axis=1)    # 使用argmax获得每行中最大元素的索引
    print("y_hat.dtype: {}\ny.dtype: {}".format(y_hat.dtype, y.dtype))
    cmp = y_hat.type(y.dtype) == y      # 保险起见将y_hat的数据类型转换为与y的数据类型一致
    print(cmp)
    return float(cmp.type(y.dtype).sum())

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

accuracy(y_hat, y) / len(y)
y_hat.dtype: torch.int64
y.dtype: torch.int64
tensor([False,  True])
0.5

       同样,对于任意数据迭代器`data_iter`可访问的数据集,我们可以评估在任意模型`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]    # 返回分类正确样本数和总样本数的比例

       这里定义一个实用程序类`Accumulator`,用于对多个变量进行累加。在上面的`evaluate_accuracy`函数中,我们在`Accumulator`实例中创建了2个变量,分别用于存储正确预测的数量和预测的总数量。当我们遍历数据集时,两者都将随着时间的推移而累加。

class Accumulator:
    """在n个变量上累加"""
    def __init__(self, n):
        self.data = [0.0] * n   # self.data本质是一个长为n的列表

    def add(self, *args):       # *args在Python中用于表示可变数量的参数
        self.data = [a + float(b) for a, b in zip(self.data, args)]  # zip()函数用于将self.data和args中的元素分别匹配起来,返回一个迭代器对象

    def reset(self):            # 重置self.data,归零
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):     # 通过索引访问Accumulator对象的data属性
        return self.data[idx]

关于Accumulator类可能存在的疑问:

问题1:[0.0] * n 创建列表

# [0.0] * n 创建列表
"""[0.0] * n 用于创建一个元素为0.0、长度为n的列表"""
l = [0.0] * 5   # 创建了一个包含5个元素的列表,并将元素初始化为0.1
print(l)
[0.0, 0.0, 0.0, 0.0, 0.0]

问题2:*args 可变数量参数

"""当函数定义中使用*args作为参数时,它表示可以接受任意数量的位置参数,并将这些位置参数作为一个元组传递给函数体。
在函数调用时,可以传递任意数量的位置参数,它们将被打包成一个元组,然后传递给*args。"""
def my_function(*args):
    for arg in args:
        print(arg)

my_function(1, 2, 3)  # 输出:1 2 3
1
2
3

问题3:关于zip()函数

# 关于zip()函数
"""zip函数将按照索引位置一一对应地将两个列表中的元素配对在一起"""
list1 = [1.0, 2.0, 3.0]
list2 = [4, 5, 6]
for t in zip(list1, list2): # zip(list1, list2)将返回一个迭代器,依次生成(1.0, 4),(2.0, 5),(3.0, 6)。
    print(t)
(1.0, 4)
(2.0, 5)
(3.0, 6)

问题4:关于__getitem__()

# 关于__getitem__()
"""在Python中,__getitem__()是一个特殊方法(也被称为魔术方法或双下划线方法),用于实现对象的索引访问。
当使用索引操作符[]来访问对象的元素时,Python会调用该对象的__getitem__()方法来处理索引操作。"""
class MyClass:
    def __init__(self, data):
        self.data = data

    def __getitem__(self, idx):
        return self.data[idx]

my_obj = MyClass([1, 2, 3, 4, 5])
print(my_obj[2])  # 输出:3
3

7、训练

       在我们看过线性回归实现后,softmax回归的训练过程代码应该看起来非常眼熟。在这里,我们重构训练过程的实现以使其可重复使用。首先,我们定义一个函数来训练一个迭代周期。请注意,`updater`是更新模型参数的常用函数,它接受批量大小作为参数。它可以是`d2l.sgd`函数,也可以是框架的内置优化函数。

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):
            # 使用PyTorch内置的优化器和损失函数
            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]

       在展示训练函数的实现之前,我们定义一个在动画中绘制数据的实用程序类`Animator`,它能够简化本书其余部分的代码。

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)

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

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,))
    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

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

lr = 0.1

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

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

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

8、预测

       现在训练已经完成,我们的模型已经准备好对图像进行分类预测。给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。

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)

四、Softmax回归的简洁实现

       深度学习框架的高级API也能更方便地实现softmax回归模型。

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

1、读取数据集

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

2、定义模型

# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))

3、初始化模型参数

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

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01) # 如果layer是Linear的话就init成一个均值0(默认)标准差0.01的随机初始化权重

net.apply(init_weights) # 将init_weights()函数apply到net上面去,net上面的每一层都会跑一下这个函数,就完成了初始化

4、定义损失函数

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

5、定义优化算法

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

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

6、训练

       接下来我们调用之前定义的训练函数来训练模型。

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

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

7、疑问

疑问一:为什么简洁实现中没有看到调用Softmax运算?而是直接使用了交叉熵损失函数

答:因为算交叉熵损失函数的时候自带了softmax,这个函数其实是softmax()和交叉熵的结合体。

详情见下面链接:

torch.nn.CrossEntropyLoss自带了softmax!_nn.crossentropyloss()包含softmax吗-CSDN博客文章浏览阅读4k次,点赞2次,收藏13次。刚刚看一篇代码,注释说了最后有一个softmax,但是代码中却没有用到F.softmax查阅资料,原来是算交叉熵损失函数的时候自带了softmaxx[class]是gt类别的预测概率,x[j]是各个类别的预测概率,由于交叉熵损失只计算gt类别的损失,如下图x_i是gt类别,p(x_i)只有gt类别时=1,所以实际上只计算gt类别的预测置信度加个log,算个softmax就是第一张图片那样..._nn.crossentropyloss()包含softmax吗https://blog.csdn.net/lt1103725556/article/details/110483525【pytorch】pytorch 计算 CrossEntropyLoss 需要先经 softmax 层激活吗_pytorch 不需要接softmax吗-CSDN博客文章浏览阅读8.9k次,点赞24次,收藏36次。答案是不需要。碰到一个坑,之前用pytorch实现自己的网络时,如果使用CrossEntropyLoss我总是将网路输出经softmax激活层后再计算交叉熵损失。刚刚查文档时发现自己大错特错了。考虑样本空间的类集合为{0,1,2},网络最后一层有3个神经元(每个神经元激活值代表对不同类的响应强度),某个样本送入网络后的输出记为net_out: [1,2,3], 该样本的真..._pytorch 不需要接softmax吗https://blog.csdn.net/zkq_1986/article/details/100668648Pytorch踩坑记之交叉熵(nn.CrossEntropy,nn.NLLLoss,nn.BCELoss的区别和使用)_pytorch 交叉熵-CSDN博客文章浏览阅读3.8w次,点赞100次,收藏239次。目录nn.Softmax和nn.LogSoftmaxnn.NLLLossnn.CrossEntropynn.BCELoss总结在Pytorch中的交叉熵函数的血泪史要从nn.CrossEntropyLoss()这个损失函数开始讲起。从表面意义上看,这个函数好像是普通的交叉熵函数,但是如果你看过一些Pytorch的资料,会告诉你这个函数其实是softmax()和交叉熵的结合体..._pytorch 交叉熵https://blog.csdn.net/watermelon1123/article/details/91044856

参考文献

[1]  损失函数|交叉熵损失函数

[2]  深度学习模型系列一——多分类模型——Softmax 回归-CSDN博客

[3]  Softmax 回归_哔哩哔哩_bilibili

  • 11
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
softmax回归从零开始的实现可以分为以下几个步骤: 1. 获取并读取数据:首先,我们需要获取训练数据集和测试数据集。然后,我们可以通过数据加载器将数据集转换为可供模型使用的张量格式。 2. 初始化模型参数:我们需要定义模型的参数,其中包括权重矩阵w和偏置向量b,并将它们初始化为随机值。 3. 定义模型:softmax回归的模型可以表示为线性变换和softmax操作的组合。我们可以使用矩阵乘法和加法运算来实现线性变换,并使用softmax函数将输出转换为概率分布。 4. 定义损失函数:softmax回归使用交叉熵损失函数来衡量预测结果与真实标签之间的差异。交叉熵损失函数可以通过计算预测概率分布和真实标签的对数似然来得到。 5. 定义优化算法:我们可以使用梯度下降算法来最小化损失函数。梯度下降算法的核心思想是通过计算损失函数关于模型参数的梯度来更新参数的值。 6. 训练模型:在训练过程中,我们需要将输入数据传递给模型,计算预测结果,并根据损失函数的值来更新模型参数。这个过程可以通过多次迭代来完成。 以下是一个伪代码示例: ``` # 步骤1:获取并读取数据 data_loader = DataLoader(...) train_data, test_data = data_loader.load_data(...) # 步骤2:初始化模型参数 w = torch.randn(...) b = torch.zeros(...) # 步骤3:定义模型 def model(X): return softmax(torch.matmul(X, w) + b) # 步骤4:定义损失函数 def loss(y_hat, y): return cross_entropy(y_hat, y) # 步骤5:定义优化算法 def optimize(params, lr): params -= lr * params.grad # 步骤6:训练模型 for epoch in range(num_epochs): for X, y in train_data: # 前向传播 y_hat = model(X) # 计算损失 l = loss(y_hat, y) # 反向传播 l.backward() # 更新参数 optimize([w, b], lr) # 清零梯度 w.grad.zero_() b.grad.zero_() # 相关问题:

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值