softmax回归、交叉熵损失函数——对28*28灰度图像进行多元分类

1 intro

线性回归模型适用于输出为连续值的情景。在另一类情景中,模型输出可以是一个像图像类别这样的离散值。对于这样的离散值预测问题,我们可以使用诸如softmax回归在内的分类模型。和线性回归不同,softmax回归的输出单元从一个变成了多个,且引入了softmax运算使输出更适合离散值的预测和训练。本节以softmax回归模型为例,介绍神经网络中的分类模型。

2 softmax

softmax回归跟线性回归一样将输入特征与权重做线性叠加。与线性回归的一个主要不同在于,softmax回归的输出值个数等于标签里的类别数。因为一共有4种特征和3种输出动物类别,所以权重包含12个标量(带下标的w)、偏差包含3个标量(带下标的b),且对每个输入计算o1,o2,o3这3个输出:
o 1 = x 1 w 11 + x 2 w 21 + x 3 w 31 + x 4 w 41 + b 1 \begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{21} + x_3 w_{31} + x_4 w_{41} + b_1\end{aligned}
o 2 = x 1 w 12 + x 2 w 22 + x 3 w 32 + x 4 w 42 + b 2 \begin{aligned} o_2 &= x_1 w_{12} + x_2 w_{22} + x_3 w_{32} + x_4 w_{42} + b_2\end{aligned}
o 3 = x 1 w 13 + x 2 w 23 + x 3 w 33 + x 4 w 43 + b 3 . \begin{aligned}o_3 &= x_1 w_{13} + x_2 w_{23} + x_3 w_{33} + x_4 w_{43} + b_3. \end{aligned}
图3.2用神经网络图描绘了上面的计算。softmax回归同线性回归一样,也是一个单层神经网络。由于每个输出 o 1 , o 2 , o 3 o_1, o_2, o_3 的计算都要依赖于所有的输入 x 1 , x 2 , x 3 , x 4 x_1, x_2, x_3, x_4 ,softmax回归的输出层也是一个全连接层。
在这里插入图片描述
既然分类问题需要得到离散的预测输出,一个简单的办法是将输出值 o i o_i 当作预测类别是 i i 的置信度,并将值最大的输出所对应的类作为预测输出,即输出 arg max i o i \underset{i}{\arg\max} o_i 。例如,如果 o 1 , o 2 , o 3 o_1,o_2,o_3 分别为 0.1 , 10 , 0.1 0.1,10,0.1 ,由于 o 2 o_2 最大,那么预测类别为2,其代表猫。

然而,直接使用输出层的输出有两个问题。一方面,由于输出层的输出值的范围不确定,我们难以直观上判断这些值的意义。例如,刚才举的例子中的输出值10表示“很置信”图像类别为猫,因为该输出值是其他两类的输出值的100倍。但如果 o 1 = o 3 = 1 0 3 o_1=o_3=10^3 ,那么输出值10却又表示图像类别为猫的概率很低。另一方面,由于真实标签是离散值,这些离散值与不确定范围的输出值之间的误差难以衡量。

softmax运算符(softmax operator)解决了以上两个问题。它通过下式将输出值变换成值为正且和为1的概率分布:

y ^ 1 , y ^ 2 , y ^ 3 = softmax ( o 1 , o 2 , o 3 ) \hat{y}_1, \hat{y}_2, \hat{y}_3 = \text{softmax}(o_1, o_2, o_3)

其中

y ^ 1 = exp ( o 1 ) i = 1 3 exp ( o i ) , y ^ 2 = exp ( o 2 ) i = 1 3 exp ( o i ) , y ^ 3 = exp ( o 3 ) i = 1 3 exp ( o i ) . \hat{y}1 = \frac{ \exp(o_1)}{\sum{i=1}^3 \exp(o_i)},\quad \hat{y}2 = \frac{ \exp(o_2)}{\sum{i=1}^3 \exp(o_i)},\quad \hat{y}3 = \frac{ \exp(o_3)}{\sum{i=1}^3 \exp(o_i)}.

容易看出 y ^ 1 + y ^ 2 + y ^ 3 = 1 \hat{y}_1 + \hat{y}_2 + \hat{y}_3 = 1 0 y ^ 1 , y ^ 2 , y ^ 3 1 0 \leq \hat{y}_1, \hat{y}_2, \hat{y}_3 \leq 1 ,因此 y ^ 1 , y ^ 2 , y ^ 3 \hat{y}_1, \hat{y}_2, \hat{y}_3 是一个合法的概率分布。这时候,如果 y ^ 2 = 0.8 \hat{y}_2=0.8 ,不管 y ^ 1 \hat{y}_1 y ^ 3 \hat{y}_3 的值是多少,我们都知道图像类别为猫的概率是80%。此外,我们注意到

arg max i o i = arg max i y ^ i \underset{i}{\arg\max} o_i = \underset{i}{\arg\max} \hat{y}_i

扫描二维码关注公众号,回复: 8622891 查看本文章

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

3 交叉熵损失函数

前面提到,使用softmax运算后可以更方便地与离散标签计算误差。我们已经知道,softmax运算将输出变换成一个合法的类别预测分布。实际上,真实标签也可以用类别分布表达:对于样本 i i ,我们构造向量 y ( i ) R q \boldsymbol{y}^{(i)}\in \mathbb{R}^{q} ,使其第 y ( i ) y^{(i)} (样本 i i 类别的离散数值)个元素为1,其余为0。这样我们的训练目标可以设为使预测概率分布 y ^ ( i ) \boldsymbol{\hat y}^{(i)} 尽可能接近真实的标签概率分布 y ( i ) \boldsymbol{y}^{(i)}

我们可以像线性回归那样使用平方损失函数 y ^ ( i ) y ( i ) 2 / 2 |\boldsymbol{\hat y}^{(i)}-\boldsymbol{y}^{(i)}|^2/2 。然而,想要预测分类结果正确,我们其实并不需要预测概率完全等于标签概率。例如,在图像分类的例子里,如果 y ( i ) = 3 y^{(i)}=3 ,那么我们只需要 y ^ 3 ( i ) \hat{y}^{(i)}_3 比其他两个预测值 y ^ 1 ( i ) \hat{y}^{(i)}_1 y ^ 2 ( i ) \hat{y}^{(i)}_2 大就行了。即使 y ^ 3 ( i ) \hat{y}^{(i)}_3 值为0.6,不管其他两个预测值为多少,类别预测均正确。而平方损失则过于严格,例如 y ^ 1 ( i ) = y ^ 2 ( i ) = 0.2 \hat y^{(i)}_1=\hat y^{(i)}_2=0.2 y ^ 1 ( i ) = 0 , y ^ 2 ( i ) = 0.4 \hat y^{(i)}_1=0, \hat y^{(i)}_2=0.4 的损失要小很多,虽然两者都有同样正确的分类预测结果。

改善上述问题的一个方法是使用更适合衡量两个概率分布差异的测量函数。其中,交叉熵(cross entropy)是一个常用的衡量方法:

H ( y ( i ) , y ^ ( i ) ) = j = 1 q y j ( i ) log y ^ j ( i ) , H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ) = -\sum_{j=1}^q y_j^{(i)} \log \hat y_j^{(i)},

其中带下标的 y j ( i ) y_j^{(i)} 是向量 y ( i ) \boldsymbol y^{(i)} 中非0即1的元素,需要注意将它与样本 i i 类别的离散数值,即不带下标的 y ( i ) y^{(i)} 区分。在上式中,我们知道向量 y ( i ) \boldsymbol y^{(i)} 中只有第 y ( i ) y^{(i)} 个元素 y ( i ) y ( i ) y^{(i)}{y^{(i)}} 为1,其余全为0,于是 H ( y ( i ) , y ^ ( i ) ) = log y ^ y ( i ) ( i ) H(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}) = -\log \hat y{y^{(i)}}^{(i)} 。也就是说,交叉熵只关心对正确类别的预测概率,因为只要其值足够大,就可以确保分类结果正确。当然,遇到一个样本有多个标签时,例如图像里含有不止一个物体时,我们并不能做这一步简化。但即便对于这种情况,交叉熵同样只关心对图像中出现的物体类别的预测概率。

假设训练数据集的样本数为 n n ,交叉熵损失函数定义为 ( Θ ) = 1 n i = 1 n H ( y ( i ) , y ^ ( i ) ) , \ell(\boldsymbol{\Theta}) = \frac{1}{n} \sum_{i=1}^n H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ),

其中 Θ \boldsymbol{\Theta} 代表模型参数。同样地,如果每个样本只有一个标签,那么交叉熵损失可以简写成 ( Θ ) = ( 1 / n ) i = 1 n log y ^ y ( i ) ( i ) \ell(\boldsymbol{\Theta}) = -(1/n) \sum_{i=1}^n \log \hat y_{y^{(i)}}^{(i)} 。从另一个角度来看,我们知道最小化 ( Θ ) \ell(\boldsymbol{\Theta}) 等价于最大化 exp ( n ( Θ ) ) = i = 1 n y ^ y ( i ) ( i ) \exp(-n\ell(\boldsymbol{\Theta}))=\prod_{i=1}^n \hat y_{y^{(i)}}^{(i)} ,即最小化交叉熵损失函数等价于最大化训练数据集所有标签类别的联合预测概率。

4 代码

# funs.py
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import sys


def sgd(params, lr, batch_size):
    for param in params:
        param.data -= lr * param.grad / batch_size


'''
mnist_train: [
                []: <img>[],
                int: label
             ]
'''
def load_data_fashion_mnist(batch_size):
    mnist_train = torchvision.datasets.FashionMNIST(root='./Datasets/FashionMNIST', train=True, download=True,
                                                    transform=transforms.ToTensor())
    mnist_test = torchvision.datasets.FashionMNIST(root='./Datasets/FashionMNIST', train=False, download=True,
                                                   transform=transforms.ToTensor())
    if sys.platform.startswith('win'):
        num_workers = 0  # 0表示不用额外的进程来加速读取数据
    else:
        num_workers = 4
    train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
    return train_iter, test_iter


def get_fashion_mnist_labels(labels):
    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_fashion_mnist(images, labels):
    # d2l.use_svg_display()
    # 这里的_表示我们忽略(不使用)的变量
    _, figs = plt.subplots(1, len(images), figsize=(12, 12))
    for f, img, lbl in zip(figs, images, labels):
        f.imshow(img.view((28, 28)).numpy())
        f.set_title(lbl)
        f.axes.get_xaxis().set_visible(False)
        f.axes.get_yaxis().set_visible(False)
    plt.show()


def Evaluate_Accuracy(data_iter, net):
    right_sum, n = 0.0, 0
    for X, y in data_iter:
        right_sum += (net(X).argmax(dim=1) == y).float().sum().item()
        n += y.shape[0]
    return right_sum / n


def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, optimizer=None):
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            y_hat = net(X)
            l = loss(y_hat, y).sum()
            if optimizer is not None:
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()
            l.backward()
            if optimizer is None:
                sgd(params, lr, batch_size)
            else:
                optimizer.step()
            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
        test_acc = Evaluate_Accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
              % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
# main.py
'''
train_iter: [
                img[]: <img>[],
                label[]: <int>[]
             ]
'''
import torch
import torchvision
import numpy as np
import funs as d2l


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

num_inputs = 784
num_outputs = 10


def Soft_Max(X):
    x_exp = X.exp()
    return x_exp / x_exp.sum(dim=1, keepdim=True)


def Net(X):
    return Soft_Max(torch.mm(X.view(-1, num_inputs), W) + b)


'''
y_hat: [
    第1个样本的输出行向量,
    第2个样本的输出行向量,
    第3个样本的输出行向量,
]
y: 真实值
return: k*1(k == batch_size)
'''
def Corss_Entropy(y_hat, y):
    # print(y)
    return - torch.log(y_hat.gather(1, y.view(-1, 1)))



if __name__ == '__main__':
    W = torch.from_numpy(np.random.normal(0, 0.1, (num_inputs, num_outputs))).float()
    b = torch.zeros(num_outputs, dtype=torch.float)

    W.requires_grad_(requires_grad=True)
    b.requires_grad_(requires_grad=True)

    num_epochs, lr = 5, 0.1

    d2l.train_ch3(Net, train_iter, test_iter, Corss_Entropy, num_epochs, BATCH_SIZE, [W, b], lr)

    X, y = iter(test_iter).next()
	
	# 预测
    true_labels = d2l.get_fashion_mnist_labels(y.numpy())
    pred_labels = d2l.get_fashion_mnist_labels(Net(X).argmax(dim=1).numpy())
    titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]

    d2l.show_fashion_mnist(X[0:20], titles[0:20])

5 解读

5.1 train_iter

是一个一维数组,且只有两个元素,train_iter[0]是图片的一维数组长度为k,包含k个img,其中k是batch_size,img是28*28的灰度图片;train_iter[1]是一个一维数组,包含k个label,其中label是int类型,表示图片里是哪种衣服。

5.2 Net函数和loss函数

5.2.1 Net(假设函数)

先梳理一下,我们要干什么,其实和线性回归很像,28*28的灰度图片说明有784个像素点,每个像素点的值在0-255之间,如果我们视每一个像素点为一个变量x,则对于一张图片有:
o1 = x1*w1-1 + x2*w1-2 + … + x784*w1-784,o1是模型将该图片X预测为label=1的置信度。
那么同理,将该图片预测为label=2的置信度o2就等于:
o2 = x1*w2-1 + x2*w2-2 + … + x784*w2-784
比如我们已知这个图片集的分类共有10种,那么就有10个这样的output。

所以其实我们要做的还是y1*10 = X1*784W784*10 + b1*10,只不过y并不是连续的值了,而是某些特定的分类的概率,例如本例中有10种服饰,则输出为一个长度为10的数组,其中每一个数值yi代表,将图片X分类为i的置信度,其中最大的,就是我们要输出的label

我们设k = batch_size,为了发挥矩阵运算的速度优势,我们当然一次要批量处理k个数据,因此有 y k 10 = X   k 784   W   784 10   + b   k 10   yk*10 = X~k*784~W~784*10~ + b~k*10~

因此,softmax最终输出的y就是一个k*10的矩阵,我们每一行都是一个图片训练出来的概率集且对于任意i,有(现在的y就是代码中的y_hat):
s u m ( y [ i ] ) = = 1 sum(y[i]) == 1

5.2.2 loss(损失函数)

而且我们之前说过,交叉熵只关心对正确类别的预测概率,这里我解释一下什么叫正确类别,我细化一下上面的公式
在这里插入图片描述
在这里插入图片描述
在图一中,你的模型分类正确,我们取计算后其中的最大值,即你的损失函数的值为0.4;假如你分类错误,那么不要紧,我们做相同的动作,取其中的最大值,即你的损失函数的值为0.05。那么你图二对应的损失函数算出来的值要比图一的更劣势,代表你这个分类不好,在下一次梯度下降时,将有更大的下降空间。

这里要注意,上面的案例是没有log的,由于交叉熵损失函数,所有的概率被约束到0~1之间,log以后即为负数,前面再取负号,则置信度越高,最后得出的值反而更低,符合梯度下降的理念。

怎么硕呢,感觉难点就没什么了,其他的还是蛮好理解的

发布了113 篇原创文章 · 获赞 6 · 访问量 7115

猜你喜欢

转载自blog.csdn.net/swallowblank/article/details/102608321