softmax回归的简洁实现
1. 数据集
我们仍旧使用Fashion-MNIST
数据集,并且使用上一节中定义好的函数进行数据集的加载。
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
2. 初始化模型参数
由于softmax回归的输出层是一个全连接层,因此,我们只需要在sequential
中添加一个带有10个输出的全连接层。
def init_weight(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight,std=0.01)
net = nn.Sequential(nn.Flatten(),nn.Linear(784,10))
net.apply(init_weight)
注:1)由于pytorch
不可以隐式的调整输入的形状,所以在线性层前定义**展平层(flatten())**调整网络输入形状。
2)nn.init.
修改参数,net.apply
对网络层进行自定义修改,这一部分在后面章节会着重讲解。
3. 重新审视softmax实现
上一节说到softmax函数为 y ^ j = e x p ( o j ) ∑ k e x p ( o k ) \hat{y}_j = \frac{exp(o_j)}{\sum_kexp(o_k)} y^j=∑kexp(ok)exp(oj),由于指数函数本身特点,如果 o k o_k ok中有一些数值非常大,那么 e x p ( o k ) exp(o_k) exp(ok)可能会大于数据类型容许的最大数字,即上溢(overflow)。这将使分母或分子变为inf(无穷大),使得我们最后遇到的是0,inf 或 nan(不是数字)的 y ^ j \hat{y}_j y^j.这种情况下,我们将不能得到一个明确定义的交叉熵函数。
softmax处理技巧之一,以下引用原文内容:
解决这个问题的一个技巧是,在继续softmax计算之前,先从所有 o k o_k ok中减去 m a x ( 𝑜 𝑘 ) max(𝑜_𝑘) max(ok)。你可以证明每个 o k o_k ok 按常数进行的移动不会改变softmax的返回值。在减法和归一化步骤之后,可能有些 o j o_j oj 具有较大的负值。由于精度受限, e x p ( 𝑜 𝑗 ) exp(𝑜_𝑗) exp(oj) 将有接近零的值,即 下溢(underflow)。这些值可能会四舍五入为零,使 y ^ j \hat{y}_j y^j 为零,并且使得 $log(𝑦̂ _𝑗) $的值为
-inf
。反向传播几步后,我们可能会发现自己面对一屏幕可怕的nan
结果。
为了使得交叉熵可以正常计算,我们可以在计算交叉熵时直接传入原始
o
j
o_j
oj(未经过softmax处理过的输出),因为在计算交叉熵时取的是log值,会将exp抵消掉。具体过程将如下公式:
l
o
g
(
y
^
j
)
=
l
o
g
(
e
x
p
(
o
j
)
∑
k
e
x
p
(
o
k
)
)
=
l
o
g
(
e
x
p
(
o
j
)
)
−
l
o
g
(
∑
k
e
x
p
(
o
k
)
)
=
o
j
−
l
o
g
(
∑
k
e
x
p
(
o
k
)
)
log(\hat{y}_j) = log(\frac{exp(o_j)}{\sum_kexp(o_k)})\\ \ \ \ \ \quad \quad \qquad \qquad \qquad =log(exp(o_j)) - log(\sum_kexp(o_k))\\ \quad \qquad \qquad=o_j - log(\sum_kexp(o_k))
log(y^j)=log(∑kexp(ok)exp(oj)) =log(exp(oj))−log(k∑exp(ok))=oj−log(k∑exp(ok))
所以我们保留传统的softmax函数,用来评估通过模型输出的概率,但我们并没有将softmax函数传递到损失函数中,而是在交叉熵函数中传递未归一化的预测,并同时计算softmax及其对数。
loss = nn.CrossEntropyLoss()
4. 定义优化算法
我们仍然使用小批量随机梯度下降作为优化算法。
trainer = torch.optim.SGD(net.parameters(),lr=0.1)
5. 训练
我们调用之前定义好的函数进行训练
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)