Softmax多类逻辑回归训练 从零开始

前言

  在之前的线性回归中,我们知道了如何读取和操作数据,构造目标函数、损失函数,定义模型,对损失函数求导后利用随机梯度下降来修改参数,最后使得预测的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接口连这些烦恼也没了,但还是要把手动实现给搞搞清楚,不然接口用起来也会莫名其妙的。感觉慢慢适应了,后面要多加油了~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值