念念不忘,必有回响

家乐的深度学习笔记「4」 - softmax回归

softmax回归

线性回归模型适用于输出为连续值的情景,而softmax回归的输出单元由一个变成了多个,且引入了softmax运算输出类别的概率分布,使输出更适合离散值的预测与训练,模型输出可以是一个像图像类别这样的离散值,其是一个单层神经网络,输出个数等于分类问题中的类别个数。

分类问题

考虑一个简单的图像分类问题,其输入图像的宽和高均为2像素,且色彩为灰度。这样每个像素值都可以用一个标量表示。将图像中的4像素分别记为,假设训练数据集中图像的真实标签为🐶、🐱或🐔,这些标签分别对应离散值
通常使用离散的数值来表示类别,例如。如此,一张图像的标签为1、2和3这3个数值中的一个。虽然仍可以使用回归模型来进行建模,并将预测值就近定点化到1、2和3这3个离散值之一,但这种连续值到离散值的转换通常会影响到分类质量。因此一般使用更加适合离散值输出的模型来解决分类问题。

softmax回归模型

softmax与线性回归一样将输入特征与权重做线性叠加。其输出值个数等于标签里的类别数。由于每个输出的计算都要依赖于所有的输入,所以其输出层也是一个全连接层。

softmax运算

假如将输出值当作预测类别是的置信度,并将值最大的输出所对应的类作为预测输出,即输出,会存在两个问题:其一,由于输出层的输出值的范围不确定,很难直观上判断这些值的意义;另一方面,由于真实标签为离散值,这些离散值与不确定范围的输出值之间的误差难以衡量。
于是我们拥有了softmax运算,将输出值变换成值为正且和为1的概率分布:

其中:

这样输出值就是一个合法的概率分布,其值还可以直接代表该类别的概率,并且不改变预测类别输出。

矢量表达式

单样本分类的矢量计算表达式

可以将原图像样本视为一整个行向量,即:

则权重项和偏差参数分别为:

输出层略(为一行向量),则softmax回归对样本分类的矢量计算表达式为:

小批量样本分类的矢量计算表达式

为了进一步提升效率,可以从把多个单样本组成小批量数据。广义上讲,给定一个小批量样本,其批量大小为,输入个数(特征数)为,输出个数(类别数)为。设批量特征为。假设softmax回归的权重和偏差参数分别为。softmax回归的矢量计算表达式为:

其中加法运算使用了广播机制,且这两个矩阵的第行分别为样本的输出和概率分布

交叉熵损失函数

softmax运算将输出变换成一个合法的类别预测分布,而真实标签也可以用类别分布表达:对于样本,我们构造向量为前文提到的输出个数(类别数)),使其第个(样本i类别的离散数值)个元素为1,其余为0。这样训练目标就可以设为使预测概率分布尽可能接近真实的标签概率分布
家乐:即构造的向量也是一个合法的概率分布,只不过只存在一个尖峰,下面的对比差异就容易理解了。
可以像线性回归那样使用平方损失函数(L2损失),然而,想要预测分类结果正确,其实并不需要预测概率完全等于标签概率。如在图像分类问题中,只需要某类别预测值大于其他所有预测值即可,而平方损失就会过于严格。
于是可以使用更适合衡量两个概率分布差异的测量函数。其中,交叉熵(cross entropy)是一个常用的衡量方法:

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

假设训练数据集的样本数为交叉熵损失函数定义为:

其中代表模型参数。同样地,如果每个样本只有一个标签,那么交叉熵损失可以简写成。另一个角度看,最小化等价于最大化,即最小化交叉熵损失函数等价于最大化训练数据集所有标签类别的联合预测概率。
家乐:这里除以是因为需要衡量整个模型的质量,如果是小批量过程中,是除以,自己概念绕混了,已更新在前一篇的损失函数章节上。

练习一:最小化交叉熵损失函数与最大似然估计(MLE):最大似然估计的目的是利用已知的样本结果,反推最有可能(最大概率)导致这样结果的参数值,两者同样可以用来调整模型参数值,不过MLE假设的前提是训练样本的分布能够代表样本的真实分布,每个样本集中的样本都是所谓独立同分布的随机变量 (iid条件),且有充分的训练样本,其将概率密度的估计转化为参数估计问题。

模型预测及评价

在训练好softmax回归模型后,给定一样本特征,就可以预测每个输出类别的概率,通常使用准确率(accuracy)来评价模型的表现,其为正确预测数量与总预测数量之比。

图像分类数据集(Fashion-MNIST)

Fashion-MNIST,顾名思义,时尚的MNIST,那MNIST是什么呢?MNIST数据集来自美国国家标准与技术研究所,由来自 250 个不同人手写的数字构成,全都是压缩过的手写数字图片,由于已经被玩烂了(大部分模型都可以做到95%以上的准确率),所以新的入门数据集被推了出来,即Fashion-MNIST。

Fashion-MNIST一共包括十个类别,分别为't-shirt', 'trouser', 'pullover', 'dress', 'coat', 'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot',即T恤、裤子、套衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包和短靴,由于存储空间原因,标签仅记录了数值标签0-9。

获取数据集

必要的包

其中d2lzh是“Dive into deep learning”书作者沐神写的,可以直接用pip安装,以后不表。

from mxnet.gluon import data as gdata
import d2lzh as d2l
import time
import sys

训练与测试数据集

现在的框架基本上都带了常见数据集的一键下载,方便管理与组织。

mnist_train = gdata.vision.FashionMNIST(train=True)
mnist_test = gdata.vision.FashionMNIST(train=False)

练习三、四:gluon.data.vision里面还包括了常见的MNIST、CIFAT10、CIFAR100和自定义ImageRecordDataSet、ImageFolderDataSet(遍历文件夹的图片)等;transforms里还提供了各种随机化实例,用于扩充训练集。

训练数据集样本数

len(mnist_train), len(mnist_test)

可以查看训练集与测试集所含样本数,每个类别分别为6000和1000种。

(60000, 10000)

查看样本

feature, label = mnist_train[0]
print(feature.shape, feature.dtype)
print(label, type(label), label.dtype)

特征为宽高均为28像素的图像,每个像素的值为0到255之间8位无符号整数(uint8),使用三维NDArray存储。

(28, 28, 1) <class 'numpy.uint8'>
2 <class 'numpy.int32'> int32

获取标签

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 = d2l.plt.subplots(1, len(images), figsize=(12, 12))
    for f, img, lbl in zip(figs, images, labels):
        f.imshow(img.reshape((28, 28)).asnumpy())
        f.set_title(lbl)
        f.axes.get_xaxis().set_visible(False)
        f.axes.get_yaxis().set_visible(False)
 
X, y = mnist_train[:9]
show_fashion_mnist(X, get_fashion_mnist_labels(y))

output_6_0.svg

读取小批量

虽然可以使用模仿上节线性回归,使用yield来做生成器,但为了代码简洁,直接创建了DataLoader实例。
书后练习一:减小batch_size(如到1)会影响读取性能吗?答:会严重影响性能,4线程时,读取速度达到了惊人的152.24秒!

batch_size = 256
transformer = gdata.vision.transforms.ToTensor()
if sys.platform.startswith('win'):
    num_workers = 0
else:
    num_workers = 80

train_iter = gdata.DataLoader(mnist_train.transform_first(transformer),
                             batch_size=batch_size, shuffle=True,
                             num_workers=num_workers)
test_iter = gdata.DataLoader(mnist_test.transform_first(transformer),
                            batch_size=batch_size, shuffle=False,
                            num_workers=num_workers)

上面的num_worker是激活多线程来实现加速数据读取(暂不支持win),因为数据读取经常是训练的性能瓶颈,特别是当模型较简单或计算硬件性能较高时。另,使用ToTensor实例将图像数据从uint8格式转换为32位浮点数格式,并除以255使得所有像素的数值均在0到1之间,这一步叫作归一化;其还将图像通道从最后一维移到最前一维来方便之后介绍的卷积神经网络计算。通过transform_first函数,使ToTensor变换应用在每个数据样本(图像和标签)的第一个元素,即图像之上。

start = time.time()
for X, y in train_iter:
    continue
'%.2f sec' % (time.time() - start)

练习二:测试一下批量读取的时间,这里因为使用了40核的服务器,线程调到了80个,可以看到速度的明显提升,对比4线程时为1.72秒,40线程时为0.86秒。

'0.74 sec'

全部代码

图像分类数据集(Fashion-MNIST).html

softmax回归的从零实现

绝大多数深度学习模型的训练都有着类似的步骤:获取并读取数据、定义模型和损失函数并使用优化算法训练模型。

必要的包

前文已有基础,不多做介绍。

from mxnet import autograd, gpu, nd
import d2lzh as d2l

改一下使用的gpu。

ctx = gpu(3)

读取数据集

这里批大小设置为512,其实这个数据集本身就挺小的,感觉完全可以直接全扔显存。

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

初始化模型参数

把每个样本拉长为一行长向量(28*28即784),作为参与运算即可。

num_inputs = 28 * 28
num_outputs = 10

W = nd.random.normal(scale=0.01, shape=(num_inputs, num_outputs), ctx=ctx)
b = nd.zeros(num_outputs, ctx=ctx)

W.attach_grad()
b.attach_grad()

实现softmax运算

前面介绍过数学公式定义,这里利用NDArray实现上述操作。

X = nd.array([[1, 2, 3], [3, 4, 5]], ctx=ctx)
print(X.sum(axis=0, keepdims=True), X.sum(axis=1, keepdims=True))
print(X.sum(axis=0), X.sum(axis=1, keepdims=False))

可以看到,如果不加保留维度选项,输出会变回一维向量。

[[4. 6. 8.]]
<NDArray 1x3 @gpu(3)> 
[[ 6.]
 [12.]]
<NDArray 2x1 @gpu(3)>

[4. 6. 8.]
<NDArray 3 @gpu(3)> 
[ 6. 12.]
<NDArray 2 @gpu(3)>

将预测值转化为一个合法的概率分布。

def softmax(X):
    X_exp = X.exp()
    partition = X_exp.sum(axis=1, keepdims=True)
    return X_exp / partition

测试一下这个函数:

softmax(X)
[[0.09003057 0.24472848 0.66524094]
 [0.09003057 0.24472846 0.66524094]]
<NDArray 2x3 @gpu(3)>

另外,这个只利用数学定义实现的softmax存在问题:因为指数函数非线性增大,如计算exp(50),结果值就会变得非常大,难以维护数值稳定性。

a = nd.array([50], ctx=ctx)
a.exp()
[5.1847055e+21]
<NDArray 1 @gpu(3)>

对一个随机的输入,验证一下softmax运算会将其变为非负数,且每行和为1。

X = nd.random.normal(shape=(2, 5), ctx=ctx)
X_prob = softmax(X)
print(X)
print(X_prob, X_prob.sum(axis=1))
[[-1.1795309   1.7976178   1.52335    -1.3275213  -0.21527036]
 [ 0.35299432  1.0368916  -1.1053166   0.08840044 -1.0292935 ]]
<NDArray 2x5 @gpu(3)>

[[0.02561495 0.5028665  0.38224316 0.02209134 0.0671841 ]
 [0.23625386 0.46815717 0.05495946 0.18132897 0.05930058]]
<NDArray 2x5 @gpu(3)> 
[1. 1.]
<NDArray 2 @gpu(3)>

定义模型

在这一步把图形数据拉长为长向量。

def net(X):
    return softmax(nd.dot(X.reshape((-1, num_inputs)).as_in_context(ctx), W) + b)

定义损失函数

首先介绍一下nd.pick()方法,会按照索引从一个数组中挑出元素。

y_hat = nd.array([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = nd.array([0, 2], dtype='int32')
nd.pick(y_hat, y)
[0.1 0.5]
<NDArray 2 @cpu(0)>

按照数学定义的交叉熵损失函数即为预测值与真实值之差的负对数,这样损失函数对模型的惩罚非线性增大,但对数函数定义域为,这里输入参数的值域为(不取闭的原因是一般情况下不会出现,除非刚开始初始化时网络权重全0),所以输出值域就会变得很不稳定,如假设我们对于正确类别的预测值太低,会导致极大的loss。

下面(简洁实现)会介绍一种同时包括softmax与交叉熵的函数,其具有更好的数值稳定性。

def cross_entropy(y_hat, y):
    return -nd.pick(y_hat, y).log()

这里演示了有可能遇到的极限情况时,两者的差值。

a = nd.array([0.000001, 0.999999], ctx=ctx)
print(a[1] / a[0])
[999999.]
<NDArray 1 @gpu(3)>

计算分类准确率

给定一个预测概率分布,将预测概率最大的类别作为输出类别。分类准确率即正确预测数量与总预测数量之比。由于标签为整数,需要将其转化为32位浮点数再进行比较。

def accuracy(y_hat, y):
    return (y_hat.argmax(axis=1) == y.astype('float32')).mean().asscalar()

测试一下上面定义的变量的准确性,可以看出来,每行代表一个样本,每列代表一个样本的预测概率分布。

accuracy(y_hat, y)
0.5

同理,可以对整个模型计算分类准确率。

def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        y = y.astype('float32').as_in_context(ctx)
        acc_sum += (net(X).argmax(axis=1) == y).sum().asscalar()
        n += y.size
    return acc_sum / n

由于模型权重为随机初始化,所以现在对理论准确率为10%。

evaluate_accuracy(test_iter, net)
0.0608

训练模型

这里超参数的设置可以自己随便调调观察效果。

num_epochs, lr = 5, 0.2

def train(net, train_iter, test_iter, loss, num_epochs,
          batch_size, params=None, lr=None, trainer=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:
            with autograd.record():
                y_hat = net(X)
                l = loss(y_hat, y.as_in_context(ctx)).sum()
            l.backward()
            if trainer is None:
                d2l.sgd(params, lr, batch_size)
            else:
                traniner.step(batch_size)
            y = y.astype('float32')
            train_l_sum += l.asscalar()
            train_acc_sum += (y_hat.argmax(axis=1) == y.as_in_context(ctx)).sum().asscalar()
            n += y.size
        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))

train(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)
epoch 1, loss 0.9251, train acc 0.703, test acc 0.778
epoch 2, loss 0.6221, train acc 0.789, test acc 0.805
epoch 3, loss 0.5600, train acc 0.810, test acc 0.825
epoch 4, loss 0.5325, train acc 0.819, test acc 0.826
epoch 5, loss 0.5085, train acc 0.827, test acc 0.821

预测效果

这里演示一下如何对图像进行分类,可以看出五次迭代即达到了80%的准确度。

count = 0
for X, y in test_iter:
    if count == 233:
        break
    count += 1

true_labels = d2l.get_fashion_mnist_labels(y.asnumpy())
pred_labels = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1).asnumpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]

d2l.show_fashion_mnist(X[:10], titles[:10])

output_18_0.svg

全部代码

softmax回归从零实现.html

softmax简洁实现

同样适用gluon自带的库。

必要的包

from mxnet.gluon import loss as gloss, nn
from mxnet import autograd, gluon, init, gpu
import d2lzh as d2l

切换下gpu。

ctx = gpu(3)

读取数据集

同上。

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

定义和初始化模型

同上篇文章。

net = nn.Sequential()
net.add(nn.Dense(10))
net.initialize(init.Normal(sigma=0.01), ctx=ctx)

softmax和交叉熵损失函数

使用gluon提供的混合包,可以获得更好的数值稳定性,参见上面的损失函数部分遇到的问题。

loss = gloss.SoftmaxCrossEntropyLoss()

训练模型

trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.2})

这里因为需要使用gpu训练,重新定义了准确度函数。

def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        X = X.as_in_context(ctx)
        y = y.astype('float32').as_in_context(ctx)
        acc_sum += (net(X).argmax(axis=1) == y).sum().asscalar()
        n += y.size
    return acc_sum / n

训练模型,一回生二回熟。

num_epochs = 5

def train(net, train_iter, test_iter, loss, num_epochs, batch_size, trainer):
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            with autograd.record():
                y_hat = net(X.as_in_context(ctx))
                l = loss(y_hat, y.as_in_context(ctx)).sum()
            l.backward()
            trainer.step(batch_size)
            y = y.astype('float32')
            train_l_sum += l.asscalar()
            train_acc_sum += (y_hat.argmax(axis=1) == y.as_in_context(ctx)).sum().asscalar()
            n += y.size
        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))
        
train(net, train_iter, test_iter, loss, num_epochs, batch_size, trainer)

可以看出来准确度还是不错的:84%,省下的测试略,见上例。

epoch 1, loss 0.9137, train acc 0.703, test acc 0.788
epoch 2, loss 0.6413, train acc 0.784, test acc 0.820
epoch 3, loss 0.5698, train acc 0.805, test acc 0.816
epoch 4, loss 0.5274, train acc 0.822, test acc 0.833
epoch 5, loss 0.5265, train acc 0.819, test acc 0.840

全部代码

softmax回归简洁实现.html

另:Batch_size的选择

批量的选择会影响模型最终训练的准确度,采用全量训练更有助于确定梯度要优化的方向,但因为迭代次数更少实际上容易陷入极小点而出不来;另一方面采用过小的批量,如1(在线学习),又会导致模型难以收敛;所以,批量作为超参数之一,也需要合适选择来确保模型训练迭代速度更快、准确度更好,同时适当的批量引入的噪声会使模型具有更大的不确定性来逃离极小点,最终到达最小点的可能性会增加。

参考资料

极大似然估计详解
哈?你还认为似然函数跟交叉熵是一个意思呀?
机器学习算法原理、实现与实践——机器学习的三要素
深度学习中的batch的大小对学习效果有何影响?
Asynchronous Parallel Stochastic Gradient for Nonconvex Optimization∗

posted on 2020-03-31 00:33  licsber  阅读(1446)  评论(0编辑  收藏  举报