1.1 线性模型的缺陷
在线性模型中,操作主要包含加减乘除,表达能力往往有限,如果某一个特征对应的权重是正,那么该特征的增大会直接导致模型输出增大。线性模型有时候会受限于单调性,当我们想要根据体温预测死亡率时,若对于体温高于37摄氏度的人来说,温度越高风险越大。 但是,对于体温低于37摄氏度的人来说,温度越高风险就越低。
所以,单纯的线性网络很难训练出良好的参数,受限于单调性,线性模型的表达能力是有限的,但是我们可以基于线性网络再建立一个线性模型,这样神经网络中的信息更多,我们模型的表达能力更强。
1.2 在网络中加入隐藏层
我们可以在一个简单的网络中加入一个或多个隐藏层来克服线性模型的限制,其能处理更普遍的函数关系类型。 要做到这一点,可以将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。 我们可以把前 层看作表示,把最后一层看作线性预测器。 这种架构通常称为多层感知机。
这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。 输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。因此,这个多层感知机中的层数为2。并且,这两个层都是全连接的。每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。
1.3 获得非线性表达
考虑一般化的模型,在一个小批量中,个样本组成矩阵 ,每个样本具有个 输入特征。若隐藏层是具有 个隐藏单元的单隐藏层多层感知机,表示隐藏层的输出,隐藏层权重 ,隐藏层偏置 ,以及输出层权重 ,隐藏层偏置,我们可以安装如下方式计算出单隐藏层多层感知机的输出 :
为了发挥多层架构的潜力, 我们还需要一个额外的关键要素: 在仿射变换之后对每个隐藏单元应用非线性的激活函数 ,一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型:
为了构建更通用的多层感知机, 我们可以继续堆叠这样的隐藏层,例如:
1.4 激活函数
ReLU函数
ReLU函数被定义为该元素与 的最大值:
下面我们绘制ReLU函数
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))
当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1,下面我们绘制ReLU函数的导数。
y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
使用ReLU的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过。
sigmoid函数
该函数将输入变换到区间 上的输出,通常成为挤压函数:
并且,当输入接近 时,sigmoid函数接近线性变换:
y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
sigmoid函数的导数图像如下所示:
x.grad.data.zero_()
y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))
2.1 多层感知机的实现
在多层感知机的实现中,我们继续使用softmax回归中的Fashion-MNIST图像分类数据集,并比较一下新的网络相对于之前单层的线性网络的优势
导入所需的包,并加载数据
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)
搭建神经网络
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 128
W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens1, requires_grad=True)*0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens1, requires_grad=True))
W2 = nn.Parameter(torch.randn(num_hiddens1, num_hiddens2, requires_grad=True)*0.01)
b2 = nn.Parameter(torch.zeros(num_hiddens2, requires_grad=True))
W3 = nn.Parameter(torch.randn(num_hiddens2, num_outputs, requires_grad=True)*0.01)
b3 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))
params = [W1, b1, W2, b2, W3, b3]
在这里我构建了包含两个隐藏层的神经网络,第一个隐藏层有 256 个隐藏层单元,第二个隐藏层有 128 个隐藏层单元
定义激活函数
def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)
定义模型
def net(X):
X = X.reshape((-1, num_inputs))
H = relu(X @ W1 + b1)
O = relu(H @ W2 + b2)
return O @ W3 + b3
定义损失函数
loss = nn.CrossEntropyLoss(reduction='none')
定义优化函数
trainer = torch.optim.SGD(params, lr=0.1)
训练
epochs = 20
d2l.train_ch3(net, train_iter, test_iter, loss, epochs, trainer)
可以看到结果和准确度
d2l.evaluate_accuracy(net, test_iter)
0.862
但是考虑到隐藏层可能有较多层,直接手写较为麻烦,这里可以使用Sequential容器快速构建神经网络:
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 512),
nn.ReLU(),
nn.Linear(512, 128),
nn.ReLU(),
nn.Linear(128, 10))
Sequential(
(0): Flatten(start_dim=1, end_dim=-1)
(1): Linear(in_features=784, out_features=512, bias=True)
(2): ReLU()
(3): Linear(in_features=512, out_features=128, bias=True)
(4): ReLU()
(5): Linear(in_features=128, out_features=10, bias=True)
)
这里修改了一下隐藏层单元数量进行比较
0.8604
对比线性网络
直接构建线性的全连接层
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
Sequential(
(0): Flatten(start_dim=1, end_dim=-1)
(1): Linear(in_features=784, out_features=10, bias=True)
)
结果如下:
0.8394
因此,多隐藏层的预测结果更好,准确率也更高,可见多层的神经网络组合激活函数表达性更强
2.2 交叉验证
交叉验证,就是重复的使用数据,把把得到的样本数据进行切分,组合为不同的训练集和测试集,用训练集来训练模型,用测试集来评估模型预测的好坏。在此基础上可以得到多组不同的训练集和测试集,某次训练集中的某样本在下次可能成为测试集中的样本,即所谓交叉。
当数据不是很充足时候,经常使用k折交叉验证,方法如下:
① 首先随机地将数据集分为 k 个互不相交的大小相同的子集
② 然后将其中k-1 个子集当成训练集,剩下的一个子集当测试集
③ 在上一步中测试集有 k 种选择,重复进行 ②
④ 这样一共训练了 k 个模型, 每个模型都在相应的测试集上计算测试误差,得到了 k 个测试误差,对这 k 次的测试误差取平均便得到一个交叉验证误差
该方法最大的优点是充分利用了所有数据,有效避免了过拟合。