d2l自学笔记 – 3.线性神经网络

该章主要包括线性回归与softmax回归。

《动手学深度学习》 — 动手学深度学习 2.0.0-beta0 documentation (d2l.ai)

两类预测问题:

  • 回归
  • 分类

线性回归

一些概念

  • 仿射变换(affine transformation)

  • 解析解:线性回归存在解析解,但并不是所有问题都存在解析解

  • 批量大小(batch size)B|\mathcal{B}|:每个小批量中的样本数

  • 学习率(learning rate)η|\eta|

  • 预测(prediction):给定特征估计目标

  • 推断(inference):基于数据集估计参数

正态分布与平方损失

假设观测中包含噪声,噪声服从正态分布。

y=wx+b+ϵϵN(0,σ2)y=\mathbf{w}^\top\mathbf{x}+b+\epsilon\\ \epsilon\sim\mathcal{N}(0,\sigma^2)

P(yx)=12πσ2exp(12σ2(ywxb)2)P(yX)=i=1np(y(i)x(i))logP(yX)=i=1n12log(2πσ2)+12σ2(y(i)wx(i)b)2P (y \mid \mathbf{x})=\frac{1}{\sqrt{2 \pi \sigma^{2}}} \exp \left(-\frac{1}{2 \sigma^{2}}\left(y-\mathbf{w}^{\top} \mathbf{x}-b\right)^{2}\right) \\ P(\mathbf{y} \mid \mathbf{X})=\prod_{i=1}^{n} p\left(y^{(i)} \mid \mathbf{x}^{(i)}\right) \\ -\log P(\mathbf{y} \mid \mathbf{X})=\sum_{i=1}^{n} \frac{1}{2} \log \left(2 \pi \sigma^{2}\right)+\frac{1}{2 \sigma^{2}}\left(y^{(i)}-\mathbf{w}^{\top} \mathbf{x}^{(i)}-b\right)^{2}
在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计。

下面是具体实现:

生成数据集

假设ϵ\epsilon服从正态分布

读取数据集

定义一个data_iter函数,接受批量大小、特征矩阵、标签向量作为输入,生成大小为batch_size的小批量。

样本随机读取,用到的函数有:

indices = list(range(num_examples))
random.shuffle(indices)

为什么要分批?利用GPU并行运算的优势,处理合理大小的“小批量”。每个样本都可以并行地进行模型计算。

自定义迭代器要求将所有数据加载到内存中,并执行大量随机内存访问;内置迭代器效率要高很多。

若使用框架中现有API来读取数据,则可以通过data.DataLoader来实现。

from torch.utils import data
dataset = data.TensorDataset(X, y) # X, y的第一维度当相等
data_iter = data.DataLoader(dataset=dataset, batch_size=batch_size, shuffle=True)

定义模型

线性回归模型

from torch import nn
# 第一个参数指定输入特征形状,第二个参数指定输出特征形状
net = nn.Sequential(nn.Linear(2,1));

初始化模型参数

如果使用自定义的模型,需要requires_grad=True

如果使用预定义的框架,可以直接访问参数以设定初始值。

  1. 通过net[0]选择网络中的第一个图层
  2. 使用weight.databias.data方法访问参数
  3. 使用替换方法normal_fill_来重写参数值
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

定义损失函数

平方损失,MSELoss类(Mean Square Error),squared L2L_2 norm

loss = nn.MSELoss()

定义优化算法

小批量随机梯度下降

一些技巧:

with torch.no_grad():

Disabling gradient calculation is useful for inference, when you are sure that you will not call :meth:Tensor.backward(). It will reduce memory consumption for computations that would otherwise have requires_grad=True.

若使用框架预定义的SGD(Stochastic gradient descent)算法:

# net.parameters()返回一个迭代器,包含net模型的所有参数
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

训练

在每次迭代中,读取一小批量训练样本,并通过模型来获得一组预测。计算完损失后,开始反向传播,存储每个参数的梯度。最后,调用优化算法sgd来更新模型参数。

迭代周期个数num_epochs和学习率learning_rate都是超参数。

for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(net(X), y)
        trainer.zero_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

softmax回归

分类问题

即使我们只关心硬类别,我们仍然使用软类别的模型——用回归解决分类问题。

表示分类数据的方法:独热编码(向量表示)

softmax运算

输出规范化:

  1. 非负
  2. 总和为1
  3. 校准(calibration):在分类器输出0.5的所有样本中,有一半实际属于预测的类。

y^=softmax(o) 其中 y^j=exp(oj)kexp(ok)\hat{\mathbf{y}}=\operatorname{softmax}(\mathbf{o}) \quad \text { 其中 } \quad \hat{y}_{j}=\frac{\exp \left(o_{j}\right)}{\sum_{k} \exp \left(o_{k}\right)}
最可能的类别为
argmaxjy^j=argmaxjoj\underset{j}{\operatorname{argmax}} \hat{y}_{j}=\underset{j}{\operatorname{argmax}} o_{j}

交叉熵损失

设特征维度(输入数量)为dd,批量大小为nn,输出类别数为qq,则根据最小化负对数似然,有:
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\left(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}\right)=\sum_{i=1}^{n} l\left(\mathbf{y}^{(i)}, \hat{\mathbf{y}}^{(i)}\right),
对于特定的标签y\mathbf{y}和模型预测y^\hat{\mathbf{y}},损失函数为
l(y,y^)=j=1qyjlogy^jl(\mathbf{y}, \hat{\mathbf{y}})=-\sum_{j=1}^{q} y_{j} \log \hat{y}_{j}
由于y\mathbf{y}独热编码,上式右边只有一项非零,此项即为预测概率的负对数。

l(y,y^)=j=1qyjlogexp(oj)k=1qexp(ok)=j=1qyjlogk=1qexp(ok)j=1qyjoj=logk=1qexp(ok)j=1qyjoj.ojl(y,y^)=exp(oj)k=1qexp(ok)yj=softmax(o)jyj\begin{aligned} l(\mathbf{y}, \hat{\mathbf{y}}) &=-\sum_{j=1}^{q} y_{j} \log \frac{\exp \left(o_{j}\right)}{\sum_{k=1}^{q} \exp \left(o_{k}\right)} \\ &=\sum_{j=1}^{q} y_{j} \log \sum_{k=1}^{q} \exp \left(o_{k}\right)-\sum_{j=1}^{q} y_{j} o_{j} \\ &=\log \sum_{k=1}^{q} \exp \left(o_{k}\right)-\sum_{j=1}^{q} y_{j} o_{j} . \end{aligned} \\ \partial_{o_{j}} l(\mathbf{y}, \hat{\mathbf{y}})=\frac{\exp \left(o_{j}\right)}{\sum_{k=1}^{q} \exp \left(o_{k}\right)}-y_{j}=\operatorname{softmax}(\mathbf{o})_{j}-y_{j}

一行代码实现交叉熵函数:

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

实际应用中,在对y_hat的计算时,若一些ojo_j非常大,会导致上溢为inf,解决方案是在计算softmax前做一步减法:
y^j=exp(ojmax(ok))exp(max(ok))kexp(okmax(ok))exp(max(ok))=exp(ojmax(ok))kexp(okmax(ok))\begin{aligned} \hat{y}_{j} &=\frac{\exp \left(o_{j}-\max \left(o_{k}\right)\right) \exp \left(\max \left(o_{k}\right)\right)}{\sum_{k} \exp \left(o_{k}-\max \left(o_{k}\right)\right) \exp \left(\max \left(o_{k}\right)\right)} \\ &=\frac{\exp \left(o_{j}-\max \left(o_{k}\right)\right)}{\sum_{k} \exp \left(o_{k}-\max \left(o_{k}\right)\right)} \end{aligned}
若一些ojo_j非常小,会导致y^j\hat{y}_j为为零,logy^j\log{\hat{y}_j}值为-inf,反向传播后会出现nan,解决方案是按照下述化简式计算交叉熵损失函数:
log(y^j)=log(exp(ojmax(ok))kexp(okmax(ok)))=ojmax(ok)log(kexp(okmax(ok)))\begin{aligned} \log \left(\hat{y}_{j}\right) &=\log \left(\frac{\exp \left(o_{j}-\max \left(o_{k}\right)\right)}{\sum_{k} \exp \left(o_{k}-\max \left(o_{k}\right)\right)}\right) \\ &=o_{j}-\max \left(o_{k}\right)-\log \left(\sum_{k} \exp \left(o_{k}-\max \left(o_{k}\right)\right)\right) \end{aligned}

信息熵

H[P]=jP(j)logP(j)H[P]=\sum_{j}-P(j) \log P(j)

理解

  • 可以把信息熵H(P)H(P)理解为“知道真实概率的人所经历的惊异程度
  • 可以把交叉熵H(P,Q)H(P,Q)理解为“主观概率为Q的观察者在看到根据概率P生成的数据时的预期惊异

交叉熵分类目标

最大化观测数据的似然,亦即,最小化传达标签所需的惊异

如无必要,勿增实体。
知之为知之,不知为不知,是知也。

模型预测和评估

使用精度(accuracy)来评估模型性能。
精度等于正确预测数占预测总数的比率。

softmax回归的简洁实现

初始化模型参数

net = nn.Sequential(nn.Flatten(), nn.Linear(784,10));

def init_weights(m):
    if isinstance(m, nn.Linear):
        torch.nn.init.normal_(m.weight, mean=0, std=1)
        torch.nn.init.constant_(m.bias, 0)

net.apply(init_weights)

定义损失函数与优化算法

loss = nn.CrossEntropyLoss()
trainer = torch.optim.SGD(net.parameters(), lr=0.1)

发表评论

%d 博主赞过: