chap3 线性神经网络(2)

softmax 回归

本章视频地址本章讲义地址

遇到分类问题时,我们可以使用 softmax 方法将模型的输出作为概率,我们将优化参数以最大化观测数据的概率。为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。

回归 vs 分类

  • 回归估计一个连续值
  • 分类预测一个离散类别

回归:

  • 单连续值输出
  • 自然区间 R
  • 跟真实值的区别作为损失

分类:

  • 通常多个输出
  • 输出 i 时预测为第 i 类的置信度

分类 -> 回归

统计学家使用 独热编码 的形式来表示类别数据。

  • 独热编码(one-hot encoding)。独热编码是一个向量,它的分量和类别一样多。类别对应的分量设置为1,其他所有分量设置为0。

    例如,将用一个三维向量标签 yy 表示【猫,鸡,狗】中的一种动物,那么可以有如下对应关系:(1,0,0)(1, 0, 0)对应于“猫”、(0,1,0)(0, 1, 0)对应于“鸡”、(0,0,1)(0, 0, 1)对应于“狗”:

    y{(1,0,0),(0,1,0),(0,0,1)}.y \in \{(1, 0, 0), (0, 1, 0), (0, 0, 1)\}.

softmax 算子

softmax由三个步骤组成:
(1)对每个项求幂(使用exp);
(2)对每一行求和(小批量中每个样本是一行),得到每个样本的归一化常数;
(3)将每一行除以其归一化常数,确保结果的和为1。

y^=softmax(o)其中y^j=exp(oj)kexp(ok)\hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}

1
2
3
4
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里应用了广播机制

模型

假设我们读取了一个批量的样本X\mathbf{X},其中特征维度(输入数量)为 dd,批量大小为 nn。此外,假设我们在输出中有 qq 个类别。那么小批量特征为 XRn×d\mathbf{X} \in \mathbb{R}^{n \times d},权重为WRd×q\mathbf{W} \in \mathbb{R}^{d \times q},偏置为bR1×q\mathbf{b} \in \mathbb{R}^{1\times q}。softmax 回归的矢量计算表达式为:

O=XW+b,Y^=softmax(O).\begin{aligned} \mathbf{O} &= \mathbf{X} \mathbf{W} + \mathbf{b}, \\ \hat{\mathbf{Y}} & = \mathrm{softmax}(\mathbf{O}). \end{aligned}

相对于一次处理一个样本,小批量样本的矢量化加快了XW\mathbf{X}和\mathbf{W}的矩阵-向量乘法。

注:

由于X\mathbf{X}中的每一行代表一个数据样本,所以 softmax 运算可以按行(rowwise)执行:对于O\mathbf{O}的每一行,我们先对所有项进行幂运算,然后通过求和对它们进行标准化。
小批量的未归一化预测O\mathbf{O}和输出概率Y^\hat{\mathbf{Y}}都是形状为 n×qn \times q 的矩阵。

1
2
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)

损失函数

用来衡量真实值和预测值之间的区别,他的导数指明参数的更新方向。

L2 loss 均方误差

l(y,y)=12(yy)2l(y,y')=\frac{1}{2}(y-y')^2

  • 离原点较远的数据更新梯度较大,随着靠近原点,梯度慢慢变小。

L1 loss 绝对值

l(y,y)=yyl(y,y')=|y-y'|

  • 所有数据更新的梯度值都是常数,大部分情况下比 L2 更稳定。
  • 缺点:绝对值函数在 0 点不可导,预测值和真实值之间相距过近的时候,导数值会在 -1 和 1 之间震荡(不稳定)

Huber‘s Robust Loss

L1 和 L2 取长补短:

if yy>1:l(y,y)=yy12if~|y-y'|>1:l(y,y')=|y-y'|-\frac{1}{2};

otherwise:12(yy)2otherwise:\frac{1}{2}(y-y')^2

交叉熵损失

对数似然

softmax函数给出了一个向量y^\hat{\mathbf{y}},我们可以将其视为给定任意输入x\mathbf{x}的每个类的估计条件概率。例如,y^1\hat{y}_1=P(y=x)P(y=\text{猫} \mid \mathbf{x})。假设整个数据集{X,Y}\{\mathbf{X}, \mathbf{Y}\}具有nn个样本,其中索引ii的样本由特征向量x(i)\mathbf{x}^{(i)}和独热标签向量y(i)\mathbf{y}^{(i)}组成。我们可以将估计值与实际值进行比较:

P(YX)=i=1nP(y(i)x(i)).P(\mathbf{Y} \mid \mathbf{X}) = \prod_{i=1}^n P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}).

根据最大似然估计,我们最大化P(YX)P(\mathbf{Y} \mid \mathbf{X}),相当于最小化负对数似然(最大化似然估计等同于最小化损失函数):

logP(YX)=i=1nlogP(y(i)x(i))=i=1nl(y(i),y^(i)),-\log P(\mathbf{Y} \mid \mathbf{X}) = \sum_{i=1}^n -\log P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}) = \sum_{i=1}^n l(\mathbf{y}^{(i)}, \hat{\mathbf{y}}^{(i)}),

疑问 :第二个等号为什么成立?????

其中,对于任何标签y\mathbf{y}和模型预测y^\hat{\mathbf{y}},损失函数为:

l(y,y^)=j=1qyjlogy^j.l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j.

:eqlabel:eq_l_cross_entropy

在本节稍后的内容会讲到, :eqref:eq_l_cross_entropy中的损失函数通常被称为交叉熵损失(cross-entropy loss)。由于y\mathbf{y}是一个长度为qq的独热编码向量,所以除了一个项以外的所有项jj都消失了。由于所有y^j\hat{y}_j都是预测的概率,所以它们的对数永远不会大于00
因此,如果正确地预测实际标签,即,如果实际标签P(yx)=1P(\mathbf{y} \mid \mathbf{x})=1,则损失函数不能进一步最小化。
注意,这往往是不可能的。例如,数据集中可能存在标签噪声(某些样本可能被误标),或输入特征没有足够的信息来完美地对每一个样本分类。

交叉熵

  • 交叉熵用来衡量两个概率的区别: H[p,q]=jp(j)logq(j).H[p,q] = \sum_j - p(j) \log q(j).

  • 将他作为损失: l[y,y^]=jy(j)logy^(j)=log y^yl[y,\hat y] = \sum_j - y(j) \log \hat y(j)=-log~\hat y_y

    只关心正确类的置信值是多大

  • 其梯度是真实概率和预测概率的区别:

    ojl(y,y^)=exp(oj)k=1qexp(ok)yj=softmax(o)jyj.\partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}}) = \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} - y_j = \mathrm{softmax}(\mathbf{o})_j - y_j.

1
2
3
4
5
6
7
y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])

def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])

cross_entropy(y_hat, y)

softmax 回归的简洁实现

  1. 导入包和数据集
1
2
3
4
5
6
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)
  1. 初始化模型参数

    softmax 回归的输出层是一个全连接层*。

    全连接层的每一个节点都与上一层的所有结点相连,用来把前面提取到的特征综合起来。基于这个特性,全连接层的参数也是最多的。
    具体来说,对于任何具有dd个输入和qq个输出的全连接层,参数开销为O(dq)\mathcal{O}(dq),在实践中可能高得令人望而却步。
    幸运的是,将dd个输入转换为qq个输出的成本可以减少到O(dqn)\mathcal{O}(\frac{dq}{n}),其中超参数nn可以由我们灵活指定,以在实际应用中平衡参数节约和模型有效性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # PyTorch不会隐式地调整输入的形状。因此,
    # 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
    # flatten: 将任意维度的 tensor 都转为 2-维 tensor,第0维度保留,其他维度展开成向量
    # 输入:784,输出:10
    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);
  2. 在交叉熵损失函数中传递未归一化的预测,并同时计算 softmax 及其对数

    1
    loss = nn.CrossEntropyLoss()
  3. 使用学习率为0.1的小批量随机梯度下降作为优化算法

    1
    trainer = torch.optim.SGD(net.parameters(), lr=0.1)
  4. 训练

    1
    2
    num_epochs = 10
    d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

问题记录

  1. softmax 和 logistic 回归分析是一样的吗,如果不一样,区别在哪里?

    logistic 是二分类的预测,是 softmax 的一个特例。

  2. 为什么使用交叉熵作为损失函数,而不用相对熵/互信息等其他基于信息量的度量?

    我们的目标:需要量化两个概率之间的区别,这些函数理论上都能做到。

    不用其他的函数是因为交叉熵是最好计算的,在此之外没有什么区别。

    互信息 I(p,q)=I(q,p)I(p,q)=I(q,p),交叉熵 H(p,q)!=H(q,p)H(p,q)!=H(q,p)

  3. 极大似然估计和交叉熵损失函数之间的关系???

python 语法点

记录一些不太熟悉的 python 语法知识点,方便回顾。

zip() 函数

描述

zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。

如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同,利用 * 号操作符,可以将元组解压为列表。

zip 方法在 Python 2 和 Python 3 中的不同:在 Python 3.x 中为了减少内存,zip() 返回的是一个对象。如需展示列表,需手动 list() 转换。

如果需要了解 Python3 的应用,可以参考 Python3 zip()

语法

zip 语法:

1
zip([iterable, ...])

参数说明:

  • iterable – 一个或多个迭代器;

返回值

返回元组列表。

实例

以下实例展示了 zip 的使用方法:

1
2
3
4
5
6
7
8
9
>>>a = [1,2,3] 
>>> b = [4,5,6]
>>> c = [4,5,6,7,8]
>>> zipped = zip(a,b) # 打包为元组的列表
[(1, 4), (2, 5), (3, 6)]
>>> zip(a,c) # 元素个数与最短的列表一致
[(1, 4), (2, 5), (3, 6)]
>>> zip(*zipped) # 与 zip 相反,*zipped 可理解为解压,返回二维矩阵式
[(1, 2, 3), (4, 5, 6)]

isinstance() 函数

isinstance() 函数来判断一个对象是否是一个已知的类型,类似 type()。

isinstance() 与 type() 区别:

  • type() 不会认为子类是一种父类类型,不考虑继承关系。
  • isinstance() 会认为子类是一种父类类型,考虑继承关系。

如果要判断两个类型是否相同推荐使用 isinstance()。

语法

以下是 isinstance() 方法的语法:

1
isinstance(object, classinfo)

参数

  • object – 实例对象。
  • classinfo – 可以是直接或间接类名、基本类型或者由它们组成的元组。

返回值

如果对象的类型与参数二的类型(classinfo)相同则返回 True,否则返回 False。