softmax回归
1. softmax回归
虽然softmax回归里面带有回归二字,但是其实这是一个分类模型。类似于线性回归,softmax回归是一个单层神经网络,不同的是,线性回归的输出只有一个,而softmax回归的输出有多个。
(图片引用自zh-v2.d2l.ai)
之所以softmax回归用来做分类,是该模型经过softmax操作将输出转换成概率,根据较大的概率选择归为哪一类。以上图为例,可以使用
o
=
W
x
+
b
o=Wx+b
o=Wx+b的形式进行表达。其中
W
W
W是3X4的矩阵,每一行就是输入层的每一个参数对某一个分类的权重,
x
x
x就是输入层,是一个4X1的列向量,
b
b
b和
x
x
x一样,也是一个4X1的列向量,最终的
o
o
o是一个3X1的列向量。
在得到向量
o
o
o之后,再经过softmax操作转换成概率形式,例如
[
0.2
,
0.3
,
0.5
]
T
[0.2, 0.3,0.5]^T
[0.2,0.3,0.5]T,那么就可以把该样本归为第3类。
softmax的向量表达形式和线性回归非常类似,softmax回归同样也是线性模型,尽管softmax是一个非线性函数(softmax回归的输出由输入特征的仿射变换决定)。在线性模型中常常提到的logistic回归就是softmax回归k分类中k=2的一般形式。
2. softmax操作
首先,对于给定的样本标签
x
x
x经过仿射变换后得到输出
o
o
o,然后再经过softmax函数得到预测标签。可以看成是一个概率分布。
y
^
=
s
o
f
t
m
a
x
(
o
)
\hat{y}=softmax(o)
y^=softmax(o)
y
j
^
=
e
x
p
(
o
j
)
∑
k
e
x
p
(
o
k
)
\hat{y_j}=\frac{exp(o_j)}{\sum_kexp(o_k)}
yj^=∑kexp(ok)exp(oj)
即softmax操作就是把列向量的数值加起来,然后让每个元素除以这个总数,这样就能得到总和为1,不小于0的概率形式。
最后,
a
r
g
m
a
x
j
y
j
^
=
a
r
g
m
a
x
j
o
j
{argmax}_j\hat{y_j}=argmax_jo_j
argmaxjyj^=argmaxjoj
3. 最大似然估计
假设噪声服从正态分布。在线性回归中,
y
=
w
T
x
+
b
+
ε
y=w^Tx+b+\varepsilon
y=wTx+b+ε
其中
ε
∼
N
(
0
,
δ
2
)
\varepsilon\sim N(0,\delta^2)
ε∼N(0,δ2)
给定x到y的似然函数:
P
(
y
∣
x
)
=
1
2
π
δ
2
e
x
p
(
−
1
2
δ
2
(
y
−
w
T
x
−
b
)
2
)
P(y|x)=\frac{1}{\sqrt{2\pi\delta^2}}exp(-\frac{1}{2\delta^2}(y-w^Tx-b)^2)
P(y∣x)=2πδ21exp(−2δ21(y−wTx−b)2)
P
(
y
∣
X
)
=
∏
i
=
1
n
p
(
y
(
i
)
∣
x
(
i
)
)
P(y|X)=\prod^n_{i=1}p(y^{(i)}|x^{(i)})
P(y∣X)=i=1∏np(y(i)∣x(i))
要使上式最大,可以等价位最小化负对数似然,可以得到:
−
l
o
g
P
(
y
∣
X
)
=
∑
i
=
1
n
1
2
l
o
g
(
2
π
δ
2
)
+
1
2
δ
2
(
y
(
i
)
−
w
T
x
(
i
)
−
b
)
-logP(y|X)=\sum^n_{i=1}\frac{1}{2}log(2\pi\delta^2)+\frac{1}{2\delta^2}(y^{(i)}-w^Tx^{(i)}-b)
−logP(y∣X)=i=1∑n21log(2πδ2)+2δ21(y(i)−wTx(i)−b)
第一项是一个常数项,剩余部分就是均方误差。这就是为什么,在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计。
4. 损失函数
同理,利用极大似然估计:
P
(
Y
∣
X
)
=
∏
i
=
1
n
P
(
y
(
i
)
∣
x
(
i
)
)
P(Y|X)=\prod^n_{i=1}P(y^{(i)}|x^{(i)})
P(Y∣X)=i=1∏nP(y(i)∣x(i))
同样,求最小化负对数似然:
−
l
o
g
P
(
Y
∣
X
)
=
∑
1
n
−
l
o
g
P
(
y
(
i
)
∣
x
(
i
)
)
-logP(Y|X)=\sum^n_1-logP(y^{(i)}|x^{(i)})
−logP(Y∣X)=1∑n−logP(y(i)∣x(i))
这里,softmax函数给出预测向量
y
^
\hat{y}
y^,为每一类的概率。所以可以写成,
P
(
y
(
i
)
∣
x
(
i
)
)
=
y
^
=
∑
j
−
y
j
l
o
g
(
y
j
^
)
P(y^{(i)}|x^{(i)})=\hat{y}=\sum_j-y_jlog(\hat{y_j})
P(y(i)∣x(i))=y^=∑j−yjlog(yj^)
因为最终标签
y
y
y是one-hot向量,所以乘起来其实最终只会有一项。
故:
−
l
o
g
P
(
Y
∣
X
)
=
∑
i
=
1
n
∑
j
=
1
q
−
y
i
j
l
o
g
y
i
j
^
-logP(Y|X)=\sum_{i=1}^n\sum_{j=1}^q-y_{ij}log\hat{y_{ij}}
−logP(Y∣X)=i=1∑nj=1∑q−yijlogyij^
softmax回归中的损失函数为交叉熵损失函数。这是由于交叉熵损失
l
(
y
,
y
^
)
=
−
∑
j
=
1
q
y
j
l
o
g
y
j
^
l(y,\hat{y})=-\sum_{j=1}^qy_jlog\hat{y_j}
l(y,y^)=−j=1∑qyjlogyj^
所以,最终最小化负对数似然可以写成:
−
l
o
g
P
(
Y
∣
X
)
=
∑
1
n
l
l
(
y
(
i
)
,
y
^
(
i
)
)
-logP(Y|X)=\sum^n_1ll(y^{(i)},\hat{y}^{(i)})
−logP(Y∣X)=1∑nll(y(i),y^(i))
最小化损失函数即可。
但是在后文的实际编码中,损失函数通常写为:
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
这是因为在Fashion_Mnist数据集中,获取的
y
y
y标签并不是one-hot向量,而是一个类别。
举个例子,对于批量大小为n,即n个样例:
O
=
X
W
+
b
O=XW+b
O=XW+b
Y
^
=
s
o
f
t
m
a
x
(
O
)
\hat{Y}=softmax(O)
Y^=softmax(O)
其中X为nxd的矩阵,W为dxq的矩阵,b为1xq的矩阵,则O和
y
^
\hat{y}
y^都是nxq的矩阵。而得到的y向量为nx1的向量,表示每一个样例对应的分类,例如
[
2
,
3
,
1
,
0
,
5
]
T
[2,3,1,0,5]^T
[2,3,1,0,5]T. 在损失函数中,
y
j
y_j
yj非0即1,则只会保留预测概率的log值的对应的实际值为1的那一项。这和损失函数的概念是吻合的。我们需要最小化损失函数,也就是最大化预测概率log和实际值y的乘积。
而以上代码就是直接取实际值为1的那一项的预测概率,再取log,达到的效果是一样的。
5. 梯度
求损失函数的梯度,因为梯度下降更新的是线性网络中的参数,所以是对预测值
y
^
\hat{y}
y^求导,对softmax函数中的
o
j
o_j
oj求导:
l
(
y
,
y
^
)
=
−
∑
j
=
1
q
y
j
l
o
g
e
x
p
(
o
j
)
∑
k
=
1
q
e
x
p
(
o
k
)
l(y,\hat{y})=-\sum^q_{j=1}y_jlog\frac{exp(o_j)}{\sum_{k=1}^qexp(o_k)}
l(y,y^)=−j=1∑qyjlog∑k=1qexp(ok)exp(oj)
=
∑
j
=
1
q
y
j
l
o
g
∑
k
=
1
q
e
x
p
(
o
k
)
−
∑
j
=
1
q
y
j
o
j
=\sum_{j=1}^qy_jlog\sum_{k=1}^qexp(o_k)-\sum_{j=1}^qy_jo_j
=j=1∑qyjlogk=1∑qexp(ok)−j=1∑qyjoj
=
l
o
g
∑
k
=
1
q
e
x
p
(
o
k
)
−
∑
j
=
1
q
y
j
o
j
=log\sum_{k=1}^qexp(o_k)-\sum_{j=1}^qy_jo_j
=logk=1∑qexp(ok)−j=1∑qyjoj
∂
o
j
l
(
y
,
y
^
)
=
e
x
p
(
o
j
)
∑
k
=
1
q
e
x
p
(
o
k
)
−
y
j
=
s
o
f
t
m
a
x
(
o
)
j
−
y
j
\partial_{o_j}l(y,\hat{y})=\frac{exp(o_j)}{\sum_{k=1}^qexp(o_k)}-y_j=softmax(o)_j-y_j
∂ojl(y,y^)=∑k=1qexp(ok)exp(oj)−yj=softmax(o)j−yj
即梯度是观测值
y
y
y和估计值
y
^
\hat{y}
y^之间的差异。
6. 实现
6.1 从零实现softmax回归
- 导入相关库并下载数据集,这里使用的是FashionMnist数据集。
%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l
from IPython import display
d2l.use_svg_display()
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(root="../data",train=True,transform=
trans,download=True)
mnist_test = torchvision.datasets.FashionMNIST(root="../data",train=False,transform=trans,
download=True)
- 导入数据集,分批进行训练
batch_size =256
def get_dataloader_workers():
return 0
def load_data_fashion_mnist(batch_size,resize=None):
trans = [transforms.ToTensor()]
if resize:
trans.insert(0,transforms.Resize(resize))
trans = transforms.Compose(trans)
return (data.DataLoader(mnist_train,batch_size,shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test,batch_size,shuffle=False,
num_workers=get_dataloader_workers))
batch_size= 256
train_iter,test_iter =d2l.load_data_fashion_mnist(batch_size)
这里的get_dataloader_workers是线程的数量,表示可以使用多个线程并行load数据,大于0就会加快加载速度。
3. 定义网络模型以及softmax函数
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1,keepdim=True)
return X_exp/partition
num_inputs = 784
num_outputs = 10
W = torch.normal(0,0.01,size=(num_inputs,num_outputs),requires_grad=True)
b = torch.zeros(num_outputs,requires_grad=True)
def net(X):
return softmax(torch.matmul(X.reshape((-1,W.shape[0])),W)+b)
- 定义损失函数
def cross_entropy(y_hat,y):
return -torch.log(y_hat[range(len(y_hat)),y])
- 定义累加类和动图类
class Accumulator:
def __init__(self,n):
self.data=[0.0]*n
def add(self,*args):
self.data = [a+float(b) for a,b in zip(self.data,args)]
def reset(self):
self.data = [0.0]*len(self.data)
def __getitem__(self,idx):
return self.data[idx]
class Animator:
def __init__(self,xlabel=None,ylabel=None,legend=None,xlim=None,ylim=None,
xscale='linear',yscale='linear',fmts=('-','m--','g-','r:'),nrows=1,
ncols=1,figsize=(3.5,2.5)):
if legend is None:
legend=[]
d2l.use_svg_display()
self.fig,self.axes = d2l.plt.subplots(nrows,ncols,figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes,]
self.config_axes = lambda:d2l.set_axes(
self.axes[0],xlabel,ylabel,xlim,ylim,xscale,yscale,legend)
self.X,self.Y,self.fmts = None,None,fmts
def add(self,x,y):
if not hasattr(y,"__len__"):
y=[y]
n = len(y)
if not hasattr(x,"__len__"):
x=[x]*n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i,(a,b) in enumerate(zip(x,y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x,y,fmt in zip(self.X,self.Y,self.fmts):
self.axes[0].plot(x,y,fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)
- 评估精度
def accuracy(y_hat,y):
if len(y_hat.shape)>1 and y_hat.shape[1]>1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype)==y
return float(cmp.type(y.dtype).sum())
def evaluate_accuracy(net,data_iter):
if isinstance(net,torch.nn.Module):
net.eval()
metric = Accumulator(2)
for X,y in data_iter:
metric.add(accuracy(net(X),y),y.numel())
return metric[0]/metric[1]
- 梯度下降进行训练
def train_epoch_ch3(net,train_iter,loss,updater):
if isinstance(net,torch.nn.Module):
net.train()
metric = Accumulator(3)
for X,y in train_iter:
y_hat = net(X)
l = loss(y_hat,y)
if isinstance(updater,torch.optim.Optimizer):
updater.zero_grad()
l.mean().backward()
updater.step()
else:
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()),accuracy(y_hat,y),y.numel())
return metric[0]/metric[2],metric[1]/metric[2]
def train_ch3(net,train_iter,test_iter,loss,num_epochs,updater):
animator = Animator(xlabel='epoch',xlim=[1,num_epochs],ylim=[0.3,0.9],
legend=['train_loss','train acc','test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net,train_iter,loss,updater)
test_acc = evaluate_accuracy(net,test_iter)
animator.add(epoch+1,train_metrics+(test_acc,))
train_loss , train_acc = train_metrics
assert train_loss <0.5 ,train_loss
assert train_acc <=1 and train_acc >0.7 , train_acc
assert test_acc <=1 and test_acc > 0.7 , test_acc
- 最后使用之前用过的随机梯度下降进行参数训练
lr = 0.1
def updater(batch_size):
return d2l.sgd([W,b],lr,batch_size)
num_epochs =10
train_ch3(net,train_iter,test_iter,cross_entropy,num_epochs,updater)
9.可以使用训练好的模型进行预测
def predict_ch3(net,test_iterm,n=6):
for X,y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles=[true + '\n' + pred for true,pred in zip(trues,preds)]
d2l.show_images(X[0:n].reshape((n,28,28)),1,n,titles=titles[0:n])
predict_ch3(net,test_iter)
6.2 简洁实现
softmax的简洁实现基于已有的框架实现,代码非常简洁
首先读取数据集
import torch
from torch import nn
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
然后定义网络。这里需要在前面加入一个nn.Flatten()层,这是展平层,就是把数据集的维度变成1x(-1)维,-1表示自动推导。
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
然后定义损失函数
loss = nn.CrossEntropyLoss(reduction='none')
然后定义随机梯度下降优化器
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
最后训练就行
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
需要注意的是: 在代码中我们看不到softmax,这很奇怪。
这是因为这里将softmax操作合并进入了损失函数。
在softmax中有指数操作,如果指数很大,就可能出现溢出。
这里有一个处理方式是:在指数之中提前减去最大值,这样就不会溢出了,这是因为:
y
j
^
=
e
x
p
(
o
j
−
m
a
x
(
o
k
)
)
(
o
j
−
m
a
x
(
o
k
)
)
∑
k
e
x
p
(
o
k
−
m
a
x
(
o
k
)
)
(
o
j
−
m
a
x
(
o
k
)
)
\hat{y_j}=\frac{exp(o_j-max(o_k))(o_j-max(o_k))}{\sum_kexp(o_k-max(o_k))(o_j-max(o_k))}
yj^=∑kexp(ok−max(ok))(oj−max(ok))exp(oj−max(ok))(oj−max(ok))
但是同样带来的问题是:分子可能很小,出现下溢,导致整个概率输出为0。
所以可以把损失函数中的log和softmax中的指数操作结合起来:
l
o
g
(
y
j
^
)
=
o
j
−
m
a
x
(
o
k
)
−
∑
k
e
x
p
(
o
k
−
m
a
x
(
o
k
)
)
log(\hat{y_j})=o_j-max(o_k)-\sum_kexp(o_k-max(o_k))
log(yj^)=oj−max(ok)−k∑exp(ok−max(ok))
7. 课后习题
Reference
https://zh-v2.d2l.ai/