[动手学深度学习]02 softmax回归

本文介绍了softmax回归的基本概念,包括其作为分类模型的原理,通过softmax操作将线性输出转化为概率分布。讲解了最大似然估计在模型中的应用,以及如何使用交叉熵损失函数衡量预测与真实标签的差异。还涵盖了梯度计算和实现过程,包括自定义实现和利用PyTorch库的简洁实现。最后,课后习题供读者进一步巩固所学知识。
摘要由CSDN通过智能技术生成

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(yx)=2πδ2 1exp(2δ21(ywTxb)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(yX)=i=1np(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(yX)=i=1n21log(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(YX)=i=1nP(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(YX)=1nlogP(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^=jyjlog(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(YX)=i=1nj=1qyijlogyij^
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=1qyjlogyj^
所以,最终最小化负对数似然可以写成:
− 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(YX)=1nll(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=1qyjlogk=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=1qyjlogk=1qexp(ok)j=1qyjoj
= 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=1qexp(ok)j=1qyjoj
∂ 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)jyj
即梯度是观测值 y y y和估计值 y ^ \hat{y} y^之间的差异。

6. 实现

6.1 从零实现softmax回归

  1. 导入相关库并下载数据集,这里使用的是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)
  1. 导入数据集,分批进行训练
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)
  1. 定义损失函数
def cross_entropy(y_hat,y):
    return -torch.log(y_hat[range(len(y_hat)),y])
  1. 定义累加类和动图类
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)
  1. 评估精度
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]
  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
  1. 最后使用之前用过的随机梯度下降进行参数训练
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(okmax(ok))(ojmax(ok))exp(ojmax(ok))(ojmax(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^)=ojmax(ok)kexp(okmax(ok))

7. 课后习题

Reference

https://zh-v2.d2l.ai/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值