前言
在之前的线性回归中,我们知道了如何读取和操作数据,构造目标函数、损失函数,定义模型,对损失函数求导后利用随机梯度下降来修改参数,最后使得预测的yhat和真实的y误差越来越小。
在Softmax多类逻辑回归中。我们将进行多类分类,和之前的区别在于:输出结点由一个变成了多个,每一个输出表示属于这一类的概率。
举一个关于简单的图像分类问题。输入图像的高和宽均为2个像素,且色彩为灰度,这样,每个像素值都可以用一个标量表示。我们将图像中的4个像素分别记为
x
1
,
x
2
,
x
3
,
x
4
x_1, x_2, x_3, x_4
x1,x2,x3,x4。假设训练数据集中图像的真实标签为狗、猫或鸡(假设可以用4像素表示出这3种动物)。
如果我们用一个输出表示类别,例如𝑦 = 1代表🐶,𝑦 = 2代表🐱,𝑦 = 3代表🐔,如此,一张图像的标签为1、2和3这3个数值中的一个,虽然我们仍然可以使用回归模型来进行建模,并将预测值就近定点化到1、2和3这3个离散值之一,但这种连续值到离散值的转化通常会影响到分类质量。因此我们一般使用更加适合离散值输出的模型来解决分类问题。
Softmax回归跟线性回归一样将输入特征与权重做线性叠加。与线性回归的一个主要不同在于,Softmax回归的输出值个数等于标签里的类别数。因为一共有4种特征和3种输出动物类别,所以权重包含12个标量(带下标的
w
w
w)、偏差包含3个标量(带下标的
b
b
b),且对每个输入计算
o
1
,
o
2
,
o
3
o_1, o_2, o_3
o1,o2,o3这3个输出:
o
1
=
x
1
w
11
+
x
2
w
21
+
x
3
w
31
+
x
4
w
41
+
b
1
,
o
2
=
x
1
w
12
+
x
2
w
22
+
x
3
w
32
+
x
4
w
42
+
b
2
,
o
3
=
x
1
w
13
+
x
2
w
23
+
x
3
w
33
+
x
4
w
43
+
b
3
.
\begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{21} + x_3 w_{31} + x_4 w_{41} + b_1,\\ o_2 &= x_1 w_{12} + x_2 w_{22} + x_3 w_{32} + x_4 w_{42} + b_2,\\ o_3 &= x_1 w_{13} + x_2 w_{23} + x_3 w_{33} + x_4 w_{43} + b_3. \end{aligned}
o1o2o3=x1w11+x2w21+x3w31+x4w41+b1,=x1w12+x2w22+x3w32+x4w42+b2,=x1w13+x2w23+x3w33+x4w43+b3.
可以理解为:每一类的概率计算方式都不相同,都相同的话就是直接算仅仅属于这一类的概率了,计算方式不同才会有3个不同的输出嘛。
o
1
,
o
2
,
o
3
o_1, o_2, o_3
o1,o2,o3就分别表示属于该类别的概率。
Softmax回归同线性回归一样,也是一个单层神经网络。由于每个输出
o
1
,
o
2
,
o
3
o_1, o_2, o_3
o1,o2,o3的计算都要依赖于所有的输入
x
1
,
x
2
,
x
3
,
x
4
x_1, x_2, x_3, x_4
x1,x2,x3,x4,softmax回归的输出层也是一个全连接层。
获取数据
演示这个模型的常见数据集是手写数字识别MNIST,里面包含了上千个数字0—9的手写图像。这里我们用一个稍微复杂点的数据集,内容不再是分类数字,而是服饰。
from mxnet import gluon
from mxnet import nd
def transform(data,label): #要把数据从int8转换成floa32t格式(对这一步的目的不是很清楚)
return data.astype('float32')/255,label.astype('float32')
mnist_train = gluon.data.vision.FashionMNIST(train=True,transform=transform) #作为训练集
mnist_test = gluon.data.vision.FashionMNIST(train=False,transform=transform) #作为测试集
第一次运行时会有下载提示,等待一下即可,若长时间没反应就再运行一下。
data,label = mnist_train[59] #mnist_train是一个list
print('example shape: ',data.shape,' label: ',label)
#data表示了一张像素为28*28,通道为1(黑白)的图片,label为标号,表示是属于哪一类的
#mnist_train[12] #也可以print下mnist_train[i]看看是个啥玩意
example shape: (28, 28, 1) label: 4.0
以下代码可以画出前9个样本的内容和对应的文本标号。(可以略过)
import matplotlib.pyplot as plt
def show_images(images): #画图函数,具体实现没怎么了解
n = images.shape[0]
_,figs = plt.subplots(1,n,figsize=(15,15))
for i in range(n):
figs[i].imshow(images[i].reshape((28,28)).asnumpy())
figs[i].axes.get_xaxis().set_visible(False)
figs[i].axes.get_yaxis().set_visible(False)
plt.show()
def get_text_labels(label):
text_labels=['t-shirt','trouser','pullover','dress','coat',
'sandal','shirt','sneaker','bag','ankle boot']
#0--9的标号表示不同的10样服饰,比如0表示t-shirt,1表示trouser
return [text_labels[int(i)] for i in label]
data,label = mnist_train[0:9]
print(label)
show_images(data)
print(get_text_labels(label))
[2. 9. 6. 0. 3. 4. 4. 5. 4.] #不知道为什么输出后体现不出float类型
[9张对应的图片]
['pullover', 'ankle boot', 'shirt', 't-shirt', 'dress', 'coat', 'coat', 'sandal', 'coat']
读取数据
方便起见,不再用yield来获取批量数据,直接使用gluon.data的DataLoader函数来读取。
batch_size = 256 #每次取256组数据
train_data = gluon.data.DataLoader(mnist_train,batch_size,shuffle=True)
test_data = gluon.data.DataLoader(mnist_test,batch_size,shuffle=False)
从训练数据里读取的是有随机样本组成的批量,但测试数据不需要随机。
初始化模型参数
这里我们将每个样本表示成一个向量。数据是28 * 28大小的图片,所以输入向量的长度是28 * 28 = 784,数据集有10个类型,所以输出应该是长为10的向量。相当于一共有784种特征和10种输出类别,所以权重包含784 * 10个标量,偏差包含10个标量。(不理解可以参考前言内容再思考一下)
num_inputs = 784
num_outputs = 10
w = nd.random_normal(shape=(num_inputs,num_outputs)) #权重是一个784*10的矩阵
b = nd.random_normal(shape=num_outputs)
params = [w,b]
最开始我十分不理解为什么权重是784 * 10的矩阵,明明输入长度也就784。但其实,如果权重是784* 1的矩阵(和线性回归中设置的权重一样),那么最后输出的只是属于某一类的概率,而我们要的是10个类的概率,所以会有10列,相当于权重矩阵的第一列负责得出第一类的概率,第二列负责得出第二类的概率,以此类推(当然真正的概率还要加上b)。
for param in params:
param.attach_grad() #为参数附上梯度不能忘了
定义模型
在线性回归中,我们只需要输出一个yhat让其尽量靠近目标值。在多类分类中,需要属于每个类别的概率,每个概率值为正且加起来等于1。下面,我们首先通过定义一个softmax函数来将任意的输入归一化成合法的概率值。
from mxnet import nd
def softmax(x): #使得x矩阵每个元素为正且每一行行和为1
exp = nd.exp(x) #exp()函数:求e的x次方,x为矩阵对应元素
#把x中的元素都变为正
partition = exp.sum(axis=1,keepdims=True)
#nd.sum():计算给定轴上数组元素的总和。axis=0表示沿着每一列,axis=1表示沿着每一行,
#keepdim为True表示缩减后的轴在结果中保留尺寸为1的维度,我认为在本函数在“缩减后的轴”指列轴,因为是行求和,列就被缩减了,或者说在结果中保留行和列这两个维度
#partition为对exp矩阵每行求和,并输出一个N行1列的矩阵
return exp/partition #最后输出的矩阵中每个元素等于exp矩阵中对应元素除以所在行和(又有广播机制)
#x = nd.random_normal(shape=(2,5))
#exp = nd.exp(x)
#partition = exp.sum(axis=1,keepdims=True)
#x,exp,partition,exp/partition #可以print看看
试验看看:
#对于随机输入,我们将每个元素变成非负数,而且每一行加起来为1
x = nd.random_normal(shape=(2,5))
x_prob = softmax(x)
print(x)
print(x_prob)
print(x_prob.sum(axis=1)) #对每一行求和,易知其和必为1
[[ 0.14495121 -0.0246426 -0.9130886 -0.02453975 0.40365326]
[ 0.60531294 -0.8816499 -0.5147005 -2.3413255 0.27619967]]
<NDArray 2x5 @cpu(0)>
[[0.23092102 0.19489907 0.0801609 0.19491912 0.29909992]
[0.43021652 0.09725396 0.14036909 0.02259323 0.30956724]]
<NDArray 2x5 @cpu(0)>
[1. 1.]
<NDArray 2 @cpu(0)>
定义玩softmax函数后,我们来可以来建模计算概率了。
def net(x):
return softmax(nd.dot(x.reshape((-1,num_inputs)),w)+b)
#-1表示由系统自行判断,值为x.size()/num_inputs,其实就是batch_size
#将x从batch_size * 28 * 28 * 1的矩阵转换成batch_size行784列的矩阵,每一行的784个元素表示输入样本的特征
#x和w矩阵相乘后得到batch_size * 10的矩阵,再加上b,经过softmax函数处理后每一行便是10个类的概率
交叉熵损失函数
交叉熵损失函数是针对预测为概率值的损失函数,将两个概率分布的负交叉熵作为目标值,最小化这个值等价于最大化这两个概率的相似度。
交叉熵:每一类的真实概率 * log(预测概率)求和。由于在本例中,真实概率非0即1且只有一个1,故等价于log(预测概率) 。
给一个例子:变量y_hat是2个样本在3个类别的预测概率,变量y是这2个样本的标签类别。通俗一点讲,第一个样本经过预测,第一类的可能性为0.1,第二类的可能性为0.3,第三类的可能性为0.6,而事实上这个样本是属于第一类的,所以这个样本的交叉熵为 1 * log(0.1)+ 0 * log(0.3)+ 0 * log(0.6)(由于真实情况是属于第一类的,所以第一类的真实概率为1,其他类的真实概率就是0),即log(0.1)(第二个样本同理)。用代码表示如下:
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)
#nd.pick():根据给定轴上的输入索引从输入数组中选择元素。
[0.1 0.5]
<NDArray 2 @cpu(0)>
函数定义如下:
def cross_entropy(y_hat,y):
return -nd.pick(y_hat,y).log()
精度
给定一个类别的预测概率分布yhat,我们把预测概率最大的类别作为输出类别。如果它与真实类别y一致,说明这次预测是正确的。分类准确率即正确预测数量与总预测数量之比。
#计算精度
def accuracy(output,label): #output是X行10列的矩阵,label是包含10个分类标签的数组
#print(output.argmax(axis=1) == label)可以在下面测试时加上看看效果哦
return nd.mean(output.argmax(axis=1) == label).asscalar()
#nd.mean():计算给定轴上数组元素的均值。
#nd.argmax():沿轴返回最大值的索引。axis=1时即返回每一行的最大值的索引,即概率最高的类。
#output.argmax(axis=1) == label:返回的是一个包含0,1的数组,其中1表示预测成功,0表示失败
#评估一个模型在这个数据上的精度
def evaluate_accuracy(data_iterator,net):
acc = 0.
for data,label in data_iterator:
output = net(data)
acc += accuracy(output,label)
return acc/len(data_iterator)
evaluate_accuracy(test_data,net) #尚未训练的模型的精度,基本都是在0.1左右(瞎猜1/10)
0.15244140625
训练
import sys
sys.path.append('..')
import utils #在utils中定义了SGD随机梯度下降函数,可以直接使用
from mxnet import autograd
learning_rate = .1
for epoch in range(5):
train_loss = 0. #误差
train_acc = 0. #训练的精度
for data,label in train_data: #后面流程就和线性回归的差不多了,就加了精度的计算
with autograd.record():
output = net(data)
loss = cross_entropy(output,label)
loss.backward()
utils.SGD(params,learning_rate/batch_size)
#这里将梯度做平均,这样学习率会对batch_size不那么敏感
train_loss += nd.mean(loss).asscalar()
train_acc += accuracy(output,label)
test_acc = evaluate_accuracy(test_data,net)
print("Epoch %d Loss: %f Train_acc %f Test_acc %f"%(epoch,
train_loss/len(train_data),train_acc/len(train_data),test_acc))
Epoch 0 Loss: 3.387688 Train_acc 0.484264 Test_acc 0.595898
Epoch 1 Loss: 1.872289 Train_acc 0.632364 Test_acc 0.661621
Epoch 2 Loss: 1.553925 Train_acc 0.679477 Test_acc 0.688086
Epoch 3 Loss: 1.387122 Train_acc 0.703031 Test_acc 0.716211
Epoch 4 Loss: 1.282835 Train_acc 0.719044 Test_acc 0.733398
准确率总体不是很高啊,一方面是因为那个图像确实是很难分辨hhh(你们看到图像就知道了),同时也能发现测试集的准确率总体是会高于训练集的准确率,大概是和数据分布有关吧,因为上面定义训练集和测试集时涉及到样本随机不随机的问题…(我猜的)
预测看看
data,label = mnist_test[9:20]
show_images(data)
print('true labels')
print(get_text_labels(label))
predicted_labels = net(data).argmax(axis=1)
print('predicted labels')
print(get_text_labels(predicted_labels.asnumpy()))
[11张对应的图片]
true labels
['t-shirt', 'dress', 'coat', 'coat', 'shirt', 'bag', 'sandal', 'shirt', 'dress', 'shirt', 'coat']
predicted labels
['t-shirt', 'dress', 'shirt', 'coat', 'pullover', 'bag', 'sandal', 'coat', 'dress', 'shirt', 'coat']
结果把一件coat认成了shirt,把一件shirt认成了pullover,把还有一件shirt认成了coat,70%准确率,还行8。
小结
其实把上两节的线性回归弄懂个70%–80%,后面学起来会慢慢顺利起来,无非是读数据、初始化参数、定义模型、定义损失函数、训练,这样手动实现还要好好想想如何定义函数实现,利用Gluon接口连这些烦恼也没了,但还是要把手动实现给搞搞清楚,不然接口用起来也会莫名其妙的。感觉慢慢适应了,后面要多加油了~