Softmax回归与分类模型
概念
Softmax回归虽带有“回归”二字,但却是用来解决分类问题的。它可以用一个单层神经网络来形象化表示,如下图:
在这里,输入层有四个特征
x
1
、
x
2
、
x
3
、
x
4
x_1、x_2、x_3、x_4
x1、x2、x3、x4,输出层有三个输出神经元
o
1
、
o
2
、
o
3
o_1、o_2、o_3
o1、o2、o3。由于输入层与输出层之间只有一层权重与偏置,我们一般称输入层为0层,则该图中输出层为第一层,故称之为单层神经网络,输入层与输出层间进行的运算如下:
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}
o1=x1w11+x2w21+x3w31+x4w41+b1
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} o2=x1w12+x2w22+x3w32+x4w42+b2
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}
o3=x1w13+x2w23+x3w33+x4w43+b3
然后我们将输出值
o
i
o_i
oi当作预测类别是
i
i
i的置信度,并将值最大的输出所对应的类作为预测输出,即输出
arg
max
i
o
i
\underset{i}{\arg\max} o_i
iargmaxoi。例如,如果
o
1
,
o
2
,
o
3
o_1,o_2,o_3
o1,o2,o3分别为
0.1
,
10
,
0.1
0.1,10,0.1
0.1,10,0.1,由于
o
2
o_2
o2最大,则预测类别是o2所映射的类别。
但如果就这样直接利用输出,那么它仅仅是多个线性回归模型的叠加,则它有它的局限性:
1、 一方面,由于输出层的输出值的范围不确定,我们难以直观上判断这些值的意义。例如,刚才举的例子中的输出值10表示“很置信”图像类别为其所映射的类别,因为该输出值是其他两类的输出值的100倍。但如果
o
1
=
o
3
=
1
0
3
o_1=o_3=10^3
o1=o3=103,那么输出值10则又表示图像类别为其所映射的类别的概率很低。
2.、另一方面,由于真实标签是离散值,这些离散值与不确定范围的输出值之间的误差难以衡量。
所以为了突破这些局限,真正的Softmax回归出现了。我们将输出值经过softmax运算符转换成值为正且和为1的概率分布,如此每一个转换后的输出
y
1
、
y
2
、
y
3
y_1、y_2、y_3
y1、y2、y3表示预测类别为其所映射类别的概率,范围限制在了0~1之间,并且由
arg
max
i
o
i
=
arg
max
i
y
^
i
\underset{i}{\arg\max} o_i = \underset{i}{\arg\max} \hat{y}_i
iargmaxoi=iargmaxy^i知输出间的相对大小不发生改变,即softmax运算不改变预测类别输出。
转换运算如下:
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,y^2,y^3=softmax(o1,o2,o3)
其中
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=∑i=13exp(oi)exp(o1),y^2=∑i=13exp(oi)exp(o2),y^3=∑i=13exp(oi)exp(o3).
在深度学习中,我们推崇矢量计算,从线性回归的学习中我们已经看到矢量运算远比for循环更快,在编写代码时我们要尽可能少用for循环,能用矢量运算的不要犹豫一定要使用。我们一般是以一个批量为单位学习、训练模型,则在Softmax回归中小批量矢量计算表达式如下:
O
=
X
W
+
b
,
Y
^
=
softmax
(
O
)
,
\begin{aligned} \boldsymbol{O} &= \boldsymbol{X} \boldsymbol{W} + \boldsymbol{b},\\ \boldsymbol{\hat{Y}} &= \text{softmax}(\boldsymbol{O}), \end{aligned}
OY^=XW+b,=softmax(O),
(其中
X
∈
R
n
×
d
\boldsymbol{X} \in \mathbb{R}^{n \times d}
X∈Rn×d,
W
∈
R
d
×
q
\boldsymbol{W} \in \mathbb{R}^{d \times q}
W∈Rd×q,
b
∈
R
1
×
q
\boldsymbol{b} \in \mathbb{R}^{1 \times q}
b∈R1×q,n是指批量大小,d是指输入层特征数,q是指输出层输出单元个数)
我们看到表达式中的加法运算使用了广播机制,
O
,
Y
^
∈
R
n
×
q
\boldsymbol{O}, \boldsymbol{\hat{Y}} \in \mathbb{R}^{n \times q}
O,Y^∈Rn×q且这两个矩阵的第
i
i
i行分别为样本
i
i
i的输出
o
(
i
)
\boldsymbol{o}^{(i)}
o(i)和概率分布
y
^
(
i
)
\boldsymbol{\hat{y}}^{(i)}
y^(i)。
损失函数的选取
我们不推荐使用线性回归中的均方误差损失函数,因为想要预测分类结果正确,我们其实并不需要预测概率完全等于标签概率。例如,在图像分类的例子里,如果
y
(
i
)
=
3
y^{(i)}=3
y(i)=3,那么我们只需要
y
^
3
(
i
)
\hat{y}^{(i)}_3
y^3(i)比其他两个预测值
y
^
1
(
i
)
\hat{y}^{(i)}_1
y^1(i)和
y
^
2
(
i
)
\hat{y}^{(i)}_2
y^2(i)大就行了。即使
y
^
3
(
i
)
\hat{y}^{(i)}_3
y^3(i)值为0.6,不管其他两个预测值为多少,类别预测均正确。而平方损失则过于严格,例如
y
^
1
(
i
)
=
y
^
2
(
i
)
=
0.2
\hat y^{(i)}_1=\hat y^{(i)}_2=0.2
y^1(i)=y^2(i)=0.2比
y
^
1
(
i
)
=
0
,
y
^
2
(
i
)
=
0.4
\hat y^{(i)}_1=0, \hat y^{(i)}_2=0.4
y^1(i)=0,y^2(i)=0.4的损失要小很多,虽然两者都有同样正确的分类预测结果。
所以我们选取了更适合衡量两个概率分布差异的测量函数,比如交叉熵:
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)},
H(y(i),y^(i))=−j=1∑qyj(i)logy^j(i),交叉熵只关心对正确类别的预测概率,因为只要其值足够大,就可以确保分类结果正确。当然,遇到一个样本有多个标签时,例如图像里含有不止一个物体时,我们并不能做这一步简化。但即便对于这种情况,交叉熵同样只关心对图像中出现的物体类别的预测概率。
上面的损失函数是对单个样本来说的,那么一个批量上的代价函数怎样表示呢?
我们假设一个批量的样本数为
n
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 ),
ℓ(Θ)=n1i=1∑nH(y(i),y^(i)),
其中
Θ
\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)}
ℓ(Θ)=−(1/n)∑i=1nlogy^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)}
exp(−nℓ(Θ))=∏i=1ny^y(i)(i),即最小化交叉熵损失函数等价于最大化训练数据集所有标签类别的联合预测概率
。
小结:
1、softmax回归适用于分类问题。它使用softmax运算输出类别的概率分布。
2、softmax回归是一个单层神经网络,输出个数等于分类问题中的类别个数。
3、交叉熵适合衡量两个概率分布的差异。
Softmax回归的从零开始实现:
我将根据代码讲解一些要点:
首先导入所需的包或模块
'''
我们会使用torchvision包,它是服务于PyTorch深度学习框架的,主要用来构建计算机视觉模型。torchvision主要由以下几部分构成:
1. torchvision.datasets: 一些加载数据的函数及常用的数据集接口;
2. torchvision.models: 包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;
3. torchvision.transforms: 常用的图片变换,例如裁剪、旋转等;
4. torchvision.utils: 其他的一些有用的方法。
'''
import torch as t
import torchvision
import numpy as np
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l#本地自定义的包,添加了许多通用的函数,详情见《动手学深度学习(Pytorch版)》
获取和读取数据
我们将使用Fashion-MNIST数据集,并设置批量大小为256。
batch_size=256
train_iter,test_iter=d2l.load_data_fashion_mnist(batch_size)#加载数据集
'''
def load_data_fashion_mnist(batch_size):
mnist_train=torchvision.datasets.FashionMNIST(root='~/Datasets/FashionMNIST',
train=True,download=True,transform=tfs.ToTensor())
mnist_test=torchvision.datasets.FashionMNIST(root='~/Datasets/FashionMNIST',
train=False,download=True,transform=tfs.ToTensor())
if sys.platform.startswith('win'):
num_workers=0#0表示不用额外的进程来加速读取数据,如果有些电脑会编辑器不支持报错,去掉也无伤大雅
else:
num_workers=4
train_iter=t.utils.data.DataLoader(mnist_train,batch_size=batch_size,shuffle=True,num_workers=num_workers)
test_iter=t.utils.data.DataLoader(mnist_train,batch_size=batch_size,shuffle=False,num_workers=num_workers)
return train_iter,test_iter
'''
模型参数初始化
num_inputs=784
num_outputs=10
W=t.tensor(np.random.normal(0,0.01,(num_inputs,num_outputs)),dtype=t.float)#初始化时默requires_grad=False
b=t.zeros(1,num_outputs,dtype=t.float)
W.requires_grad_(requires_grad=True)#inplace操作,该函数使变量自身可求导
b.requires_grad_(requires_grad=True)
Out:
tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]], requires_grad=True)
实现SOFTMAX运算
X = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(X.sum(dim=0, keepdim=True)) # dim为0,按照相同的列求和,并在结果中保留列特征
print(X.sum(dim=1, keepdim=True)) # dim为1,按照相同的行求和,并在结果中保留行特征
print(X.sum(dim=0, keepdim=False)) # dim为0,按照相同的列求和,不在结果中保留列特征
print(X.sum(dim=1, keepdim=False)) # dim为1,按照相同的行求和,不在结果中保留行特征
Out:
tensor([[5, 7, 9]])
tensor([[ 6],
[15]])
tensor([5, 7, 9])
tensor([ 6, 15])
def softmax(X):
X_exp=X.exp()
partition=X_exp.sum(dim=1,keepdim=True)
return X_exp/partition
可以看到,对于随机输入,我们将每个元素变成了非负数,且每一行和为1。
X=t.rand((2,5))
X_prob=softmax(X)
print(X_prob,X_prob.sum(dim=1))
Out:
tensor([[0.1776, 0.2425, 0.2442, 0.2206, 0.1152],
[0.1445, 0.1783, 0.1595, 0.2899, 0.2279]]) tensor([1.0000, 1.0000])
定义模型
def net(X):
return softmax(t.mm(X.view((-1,num_inputs)),W)+b)
定义损失函数
def cross_entropy(y_hat,y):
return -t.log(y_hat.gather(1,y.view(-1,1)))#这里与pytorch中的交叉熵损失函数CrossEntropLoss有区别,它没有内置Softmax运算,返回的不是批量损失的平均值,即没有运用.mean()操作,相比之下少除了batch_size。
模型预测——定义准确率
def accuracy(y_hat, y):
return (y_hat.argmax(dim=1) == y).float().mean().item()
#dim=1按行取每行的最大值的索引,.float()操作把bool型元素转化为float型,以便进行.mean()操作,否则会报错:bool型无.mean()方法。最终返回一个tensor标量。
更进一步的实现如下:
# 本函数已保存在d2lzh_pytorch包中方便以后使用。该函数将被逐步改进:它的完整实现将在“图像增广”一节中描述
def evaluate_accuracy(data_iter, net):
acc_sum, n = 0.0, 0
for X, y in data_iter:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n#返回测试集上平均准确率
print(evaluate_accuracy(test_iter, net))#结果:0.0883,接近0.1,因为还为训练模型,参数是随机初始化的,模型预测准确率该接近随机情况0.1.
训练模型
num_epochs, lr = 5, 0.1#注意这里学习率为0.1,因为我们下面训练模型是损失函数和优化器是我们自定义的;如果采用pytorch内置的SGD,则在损失函数用上述自定义的话,学习率需缩小batch_size倍,才与原书0.5匹配;如果采用pytorch内置的交叉熵损失函数,则在优化器用上述自定义的话,学习率需扩大batch_size倍,才与原书0.5匹配。
# 本函数已保存在d2lzh_pytorch包中方便以后使用,考虑了使用自定义优化器和内置优化器模块两种训练情况。
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()#.sum(),体会标量.backward()的方便
# 梯度清零
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:
d2l.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))#整个训练集上的平均损失值、准确值
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)#开始训练!
Out:
epoch 1, loss 0.7847, train acc 0.749, test acc 0.792
epoch 2, loss 0.5699, train acc 0.814, test acc 0.807
epoch 3, loss 0.5248, train acc 0.827, test acc 0.821
epoch 4, loss 0.5007, train acc 0.833, test acc 0.824
epoch 5, loss 0.4860, train acc 0.838, test acc 0.826
训练结果Just so so!
模型预测
来看看训练的模型预测的准确性如何,我们用图像真实展示:
X, y = iter(test_iter).next()
true_labels = d2l.get_fashion_mnist_labels(y.numpy())#该函数已放在d2l中
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:9], titles[0:9])#该函数已放在d2l中
结果:
Softmax回归的简洁实现实现:
要运用Pytorch的封装好的模块!
看代码:
#导入模块
import torch as t
from torch import nn
from torch.nn import init
import numpy as np
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
batch_size=256
train_iter,test_iter=d2l.load_data_fashion_mnist(batch_size)
num_inputs=784
num_outputs=10
class LinearNet(nn.Module):
def __init__(self,num_inputs,num_outputs):
super(LinearNet,self).__init__()
self.linear=nn.Linear(num_inputs,num_outputs)
def forward(self,x):
y=self.linear(x.view(-1,num_inputs))# x 的形状: (batch, 1, 28, 28)
return y
net=LinearNet(num_inputs,num_outputs)
#变换输入样本的形状
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer,self).__init__()
def forward(self,x):
return x.view(x.shape[0],-1)
#这种方法构造模型很方便,但自由性不如构造子类直接继承nn.Module建立模型
from collections import OrderedDict
net=nn.Sequential(
#FlattenLayer(),
#nn.Linear(num_inputs,num_outputs)
OrderedDict([
('flatten',FlattenLayer()),
('linear',nn.Linear(num_inputs,num_outputs))# 或者写成我们自己定义的 LinearNet(num_inputs, num_outputs) 也可以
)
])
)
#初始化参数
init.normal_(net.linear.weight,mean=0,std=0.01)
init.constant_(net.linear.bias,val=0)
#定义损失函数
loss=nn.CrossEntropyLoss()#包括softmax运算和交叉熵损失计算的函数
#定义优化器
optimizer=t.optim.SGD(net.parameters(),lr=0.1)#与自定义的相比,它在更新参数时梯度未除以batch_size,因为已在内置损失函数时已除
#模型训练
num_epochs=5
d2l.train_ch3(net,train_iter,test_iter,loss,num_epochs,batch_size,None,None,optimizer)
#模型预测
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:9],titles[0:9])
另两个算法(线性回归、多层感知机)学习中提到的其他要点总结:
1.两种实现方式的比较:
a、从零开始的实现(推荐用来学习)能够更好的理解模型和神经网络底层的原理
b、使用pytorch的简洁实现能够更加快速地完成模型的设计与实现
2.为什么在要使用激活函数:
对于多层隐藏层的神经网络,若无加激活函数则仍等若一个单层网络,隐藏层数变得毫无意义。因为全连接层(隐藏层)只是对数据做仿射变换(affine transformation),而多个仿射变换的叠加仍然是一个仿射变换。解决这个问题的一个方法就是引入非线性变换——添加激活函数。
3.几种常用的激活函数:
a、ReLU()函数
ReLU函数只保留正数元素,并将负数元素清零。
y与x关系:
y
=
R
e
L
U
(
x
)
=
m
a
x
(
x
,
0
)
y=ReLU(x)=max(x,0)
y=ReLU(x)=max(x,0)
b、Sigmoid()函数
sigmoid函数可以将元素的值变换到0和1之间。
y与x关系:
y
=
sigmoid
(
x
)
=
1
1
+
exp
(
−
x
)
.
y=\text{sigmoid}(x) = \frac{1}{1 + \exp(-x)}.
y=sigmoid(x)=1+exp(−x)1.
c、tanh()函数
tanh(双曲正切)函数可以将元素的值变换到-1和1之间。
y与x的关系:
y
=
t
e
x
t
t
a
n
h
(
x
)
=
1
−
exp
(
−
2
x
)
1
+
exp
(
−
2
x
)
.
y=text{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}.
y=texttanh(x)=1+exp(−2x)1−exp(−2x).
d、Softmax()函数
softmax()函数可以把元素映射到(0~1)之间的概率分布。更多描述见上文。
4.激活函数的选择:
a、ReLu函数是一个通用的激活函数,目前在大多数情况下使用。但是,ReLU函数只能在隐藏层中使用。
b、用于分类器时,sigmoid函数及其组合通常效果更好。多分类问题Softmax函数在输出层用起来更好。由于梯度消失问题,有时要避免使用sigmoid和tanh函数。
c、在神经网络层数较多的时候,最好使用ReLu函数,ReLu函数比较简单计算量少,而sigmoid和tanh函数计算量大很多。
d、在选择激活函数的时候可以先选用ReLu函数如果效果不理想可以尝试其他激活函数。
如有错误,请不吝指正,谢谢!