在VGG中,卷积网络达到了19层,在GoogLeNet中,网络史无前例的达到了22层。那么,网络的精度会随着网络的层数增多而增多吗?在深度学习中,网络层数增多一般会伴着下面几个问题
随着网络层数的增加,网络发生了退化(degradation)的现象:随着网络层数的增多,训练集loss逐渐下降,然后趋于饱和,当你再增加网络深度的话,训练集loss反而会增大。注意这并不是过拟合,因为在过拟合中训练loss是一直减小的。
当网络退化时,浅层网络能够达到比深层网络更好的训练效果,这时如果我们把低层的特征传到高层,那么效果应该至少不比浅层的网络效果差,或者说如果一个VGG-100网络在第98层使用的是和VGG-16第14层一模一样的特征,那么VGG-100的效果应该会和VGG-16的效果相同。所以,我们可以在VGG-100的98层和14层之间添加一条直接映射(Identity Mapping)来达到此效果。
从信息论的角度讲,由于DPI(数据处理不等式)的存在,在前向传输的过程中,随着层数的加深,Feature Map包含的图像信息会逐层减少,而ResNet的直接映射的加入,保证了 l + 1 l+1 l+1层的网络一定比 l l l层包含更多的图像信息。
基于这种使用直接映射来连接网络不同层直接的思想,残差网络应运而生。
随着我们设计越来越深的网络,深刻理解“新添加的层如何提升神经网络的性能”变得至关重要。更重要的是设计网络的能力,在这种网络中,添加层会使网络更具表现力,为了取得质的突破,我们需要一些数学基础知识。
首先,假设有一类特定的神经网络结构 F \mathcal{F} F,它包括学习速率和其他超参数设置。对于所有 f ∈ F f \in \mathcal{F} f∈F,存在一些参数集(例如权重和偏置),这些参数可以通过在合适的数据集上进行训练而获得。
现在假设 f ∗ f^* f∗ 是我们真正想要找到的函数,如果是 f ∗ ∈ F f^* \in \mathcal{F} f∗∈F,那我们可以轻而易举的训练得到它,但通常我们不会那么幸运。相反,我们将尝试找到一个函数 f F ∗ f^*_\mathcal{F} fF∗,这是我们在 F \mathcal{F} F 中的最佳选择。例如,给定一个具有 X \mathbf{X} X 特性和 y \mathbf{y} y 标签的数据集,我们可以尝试通过解决以下优化问题来找到它:
f F ∗ : = a r g m i n f L ( X , y , f ) subject to f ∈ F . (2.1) f^*_\mathcal{F} := \mathop{\mathrm{argmin}}_f L(\mathbf{X}, \mathbf{y}, f) \text{ subject to } f \in \mathcal{F}. \tag{2.1} fF∗:=argminfL(X,y,f) subject to f∈F.(2.1)
那么,怎样得到更近似真正 f ∗ f^* f∗ 的函数呢?
唯一合理的可能性是,我们需要设计一个更强大的结构 F ′ \mathcal{F}' F′。换句话说,我们预计 f F ′ ∗ f^*_{\mathcal{F}'} fF′∗ 比 f F ∗ f^*_{\mathcal{F}} fF∗ “更近似”。然而,如果 F ⊈ F ′ \mathcal{F} \not\subseteq \mathcal{F}' F⊆F′,则无法保证新的体系“更近似”。
事实上, f F ′ ∗ f^*_{\mathcal{F}'} fF′∗ 可能更糟:
如下图所示,对于非嵌套函数(non-nested function)类,较复杂的函数类并不总是向“真”函数 f ∗ f^* f∗ 靠拢(复杂度由 F 1 \mathcal{F}_1 F1 向 F 6 \mathcal{F}_6 F6 递增)。在下图的左边,虽然 F 3 \mathcal{F}_3 F3 比 F 1 \mathcal{F}_1 F1 更接近 f ∗ f^* f∗,但 F 6 \mathcal{F}_6 F6 却离的更远了。相反对于图右侧的嵌套函数(nested function)类 F 1 ⊆ … ⊆ F 6 \mathcal{F}_1 \subseteq \ldots \subseteq \mathcal{F}_6 F1⊆…⊆F6,我们可以避免上述问题。
因此,只有当较复杂的函数类包含较小的函数类时,我们才能确保提高它们的性能。对于深度神经网络,如果我们能将新添加的层训练成 恒等映射(identity function) f ( x ) = x f(\mathbf{x}) = \mathbf{x} f(x)=x ,新模型和原模型将同样有效。同时,由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。
针对这一问题,何恺明等人提出了残差网络(ResNet)He.Zhang.Ren.ea.2016
。它在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。于是,残差块 (residual blocks) 便诞生了,这个设计对如何建立深层神经网络产生了深远的影响。凭借它,ResNet 赢得了 2015 年 ImageNet 大规模视觉识别挑战赛。
让我们聚焦于神经网络局部:如下图所示,假设我们的原始输入为 x x x ,而希望学出的理想映射为 f ( x ) f(\mathbf{x}) f(x) (作为 residual_block
上方激活函数的输入)。下左图虚线框中的部分需要直接拟合出该映射 f ( x ) f(\mathbf{x}) f(x) ,而右图虚线框中的部分则需要拟合出残差映射 f ( x ) − x f(\mathbf{x}) - \mathbf{x} f(x)−x 。
残差映射在现实中往往更容易优化。以上一节中提到的恒等映射作为我们希望学出的理想映射 f ( x ) f(\mathbf{x}) f(x) ,我们只需将 residual_block
中右图虚线框内上方的加权运算(如仿射)的权重和偏置参数设成 0,那么 f ( x ) f(\mathbf{x}) f(x) 即为恒等映射。
实际中,当理想映射 f ( x ) f(\mathbf{x}) f(x) 极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。residual_block
右图是 ResNet 的基础结构-- 残差块(residual block)。在残差块中,输入可通过跨层数据线路更快地向前传播。
这里可能会让人有一些混淆,梳理一下:
residual_block
右图虚线框中的部分残差块的结构:
参差网络的通用表示方式是:
y l = h ( x l ) + F ( x l , W l ) (2.2) y_l = h(x_l)+ F(x_l,W_l) \tag{2.2} yl=h(xl)+F(xl,Wl)(2.2)
x l + 1 = f ( y l ) (2.3) x_{l+1} = f(y_l) \tag{2.3} xl+1=f(yl)(2.3)
现在我们先不考虑升维或者降维的情况,那么假设公式2.2和2.3中
那么残差块儿可表示为: x l + 1 = x l + F ( x l , W l ) (2.4) x_{l+1} = x_l + F(x_l,W_l) \tag{2.4} xl+1=xl+F(xl,Wl)(2.4)
对于一个更深的层L,其与l层的关系可以表示为: x L = x l + ∑ i = l L − 1 F ( x i , W i ) (2.5) x_L = x_l + \sum_{i=l}^{L-1}F(x_i,W_i) \tag{2.5} xL=xl+i=l∑L−1F(xi,Wi)(2.5)
公式2.5 反映残差网络的两个特性:
根据向后传播中使用的导数的链式法则,损失函数 ϵ \epsilon ϵ关于 x l x_l xl的梯度可以表示为:
∂ ϵ ∂ x l = ∂ ϵ ∂ x L ∂ x L ∂ x l = ∂ ϵ ∂ x L ( 1 + ∂ ∂ x l ∑ i = l L − 1 F ( x i , W i ) ) = ∂ ϵ ∂ x L + ∂ ϵ ∂ x L ∂ ∂ x l ∑ i = l L − 1 F ( x i , W i ) (2.6) \begin{aligned} \frac{\partial{\epsilon}}{\partial{x_l}} &= \frac{\partial{\epsilon}}{\partial{x_L}}\frac{\partial{x_L}}{\partial{x_l}} \\ &= \frac{\partial{\epsilon}}{\partial{x_L}}(1+\frac{\partial{}}{\partial{x_l}}\sum_{i=l}^{L-1}F(x_i,W_i))\\ &= \frac{\partial{\epsilon}}{\partial{x_L}}+ \frac{\partial{\epsilon}}{\partial{x_L}}\frac{\partial{}}{\partial{x_l}}\sum_{i=l}^{L-1}F(x_i,W_i)\\ \end{aligned} \tag{2.6} ∂xl∂ϵ=∂xL∂ϵ∂xl∂xL=∂xL∂ϵ(1+∂xl∂i=l∑L−1F(xi,Wi))=∂xL∂ϵ+∂xL∂ϵ∂xl∂i=l∑L−1F(xi,Wi)(2.6)
公式2.6 中反映了:
1.3 中我们假设了
对于假设1,采用反证法:假设 h ( x l ) = λ l x l h(x_l) = \lambda_{l}x_l h(xl)=λlxl,那么这个时候残差块可以表示为: x l + 1 = λ l x l + F ( x l , W l ) (2.7) x_{l+1} = \lambda_lx_l+F(x_l,W_l) \tag{2.7} xl+1=λlxl+F(xl,Wl)(2.7)
对于更深的层:
x L = ( ∏ i = l L − 1 λ i ) x l + ∑ i = l L − 1 F ( x i , W i ) (2.8) x_L = (\prod_{i=l}^{L-1}\lambda_i)x_l + \sum_{i=l}^{L-1}F(x_i,W_i) \tag{2.8} xL=(i=l∏L−1λi)xl+i=l∑L−1F(xi,Wi)(2.8)
为了简化问题,只考虑公式左半部分 x L = ( ∏ i = l L − 1 λ i ) x l x_L = (\prod_{i=l}^{L-1}\lambda_i)x_l xL=(∏i=lL−1λi)xl,损失函数 ϵ \epsilon ϵ关于 x l x_l xl的梯度可以表示为:
∂ ϵ ∂ x l = ∂ ϵ ∂ x L ( ( ∏ i = l L − 1 λ i ) + ∂ ∂ x l F ( x i , W i ) ) (2.9) \frac{\partial{\epsilon}}{\partial{x_l}} = \frac{\partial{\epsilon}}{\partial{x_L}}((\prod_{i=l}^{L-1}\lambda_i)+\frac{\partial{}}{\partial{x_l}}F(xi,Wi)) \tag{2.9} ∂xl∂ϵ=∂xL∂ϵ((i=l∏L−1λi)+∂xl∂F(xi,Wi))(2.9)
这个公式反映了:
这里参考详解残差网络
假设一个10层网络的输出为 y = f ( x ) y = f(x) y=f(x),关于底层的一个参数w的梯度为(不考虑loss,直接用输出表示了) y ′ = ∂ y ∂ w (2.10) y' = \frac{\partial{y}}{\partial{w}} \tag{2.10} y′=∂w∂y(2.10)
w的更新为: w = w − D ∂ y ∂ w (2.11) w = w - D\frac{\partial{y}}{\partial{w}} \tag{2.11} w=w−D∂w∂y(2.11)
在其上面再添加10层得到的输出为 y ^ = g ( y ) = g ( f ( x ) ) (2.12) \hat{y} = g(y) = g(f(x)) \tag{2.12} y^=g(y)=g(f(x))(2.12)
关于同样一个参数w的梯度为: y ^ ′ = g ( f ( x ) ) ′ = ∂ y ^ ∂ y ∂ y ∂ w (2.13) \hat{y} ' = g(f(x))' = \frac{\partial{\hat{y}}}{\partial{y}} \frac{\partial{y}}{\partial{w}} \tag{2.13} y^′=g(f(x))′=∂y∂y^∂w∂y(2.13)
问题就来了: ∂ y ^ ∂ y \frac{\partial{\hat{y}}}{\partial{y}} ∂y∂y^ 相当于是顶部层的一个结果和真实结果的一个偏差,如果这个部分特别小的话,梯度就会特别小,导致没有办法更新(特别是顶部如果是全连接层这种可以极大地提取特征的层影响非常大)
看看ResNet如何解决的:
ResNet其实是将上一层网络的输出和该层网络的输出组合起来作为下一层网络的输入:
y r e s i d u a l = f ( x ) + g ( f ( x ) ) (2.14) y_{residual} = f(x) + g(f(x)) \tag{2.14} yresidual=f(x)+g(f(x))(2.14)
那么它的梯度就是:
y r e s i d u a l ′ = ∂ y ∂ w + ∂ y ^ ∂ y ∂ y ∂ w (2.15) y_{residual}' = \frac{\partial{y}}{\partial{w}} + \frac{\partial{\hat{y}}}{\partial{y}} \frac{\partial{y}}{\partial{w}} \tag{2.15} yresidual′=∂w∂y+∂y∂y^∂w∂y(2.15)
华点出来:即使后面部分是一个非常小的数,$f(x)'$仍然能保证这梯度不是特别小
两种类型的网络:
use_1x1conv=False
、应用 ReLU 非线性函数之前,将输入添加到输出。use_1x1conv=True
,添加通过 1 × 1 1 \times 1 1×1 卷积调整通道和分辨率。import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
class Residual(nn.Module):
def __init__(self,input_channels,num_channels,use_1x1conv=False,strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels,num_channels,kernel_size=3,padding=1,stride=strides)
self.conv2 = nn.Conv2d(num_channels,num_channels,kernel_size=3,padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels,num_channels,kernel_size=1,stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
self.relu = nn.ReLU(inplace=True)
def forward(self,X):
Y = self.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return self.relu(Y)
# 验证块
blk = Residual(3, 3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
Y.shape
torch.Size([4, 3, 6, 6])
# 也可以在增加通道数的同时,高宽减半
blk = Residual(3, 6, use_1x1conv=True, strides=2)
blk(X).shape
torch.Size([4, 6, 3, 3])
(1)ResNet 的前两层跟 GoogLeNet 中的一样:
在输出通道数为 64、步幅为 2 的 7 × 7 7 \times 7 7×7 卷积层后,接步幅为 2 的 3 × 3 3 \times 3 3×3 的最大池化层。
不同之处在于 ResNet 每个卷积层后增加了批量归一化层。
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
(2)使用ResNet block构建完整的ResNet
GoogLeNet 在后面接了 4 个由Inception块组成的模块。ResNet 则使用 4 个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。
def resnet_block(input_channels,num_channels,num_residuals,first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(
Residual(input_channels,num_channels,use_1x1conv=True,strides=2)
)
else:
blk.append(
Residual(num_channels,num_channels)
)
return blk
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True)) # 这个高宽不减半,通道数加倍
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
(3)在ResNet加入平均池化层和全连接层输出
net = nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(), nn.Linear(512, 10))
(4)看一看网络输出的大小
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape:\t', X.shape)
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 128, 28, 28])
Sequential output shape: torch.Size([1, 256, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape: torch.Size([1, 512, 1, 1])
Flatten output shape: torch.Size([1, 512])
Linear output shape: torch.Size([1, 10])
# 读取数据
from torchvision import transforms
import torchvision
from torch.utils import data
batch_size = 256
def get_dataloader_workers():
"""使用四个进程读取数据"""
return 4
def load_data_fashion_mnist(batch_size,resize=None):
"""下载Fashion-MNIST数据集,并将其保存至内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0,transforms.Resize(resize)) # transforms.Resize将图片最小的一条边缩放到指定大小,另一边缩放对应比例
trans = transforms.Compose(trans) # compose用于串联多个操作
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)
return (data.DataLoader(mnist_train,batch_size,shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test,batch_size,shuffle=True,
num_workers = get_dataloader_workers()))
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度。"""
if isinstance(net, torch.nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
for X, y in data_iter:
if isinstance(X, list):
# BERT微调所需的(之后将介绍)
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
#@save
def train(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型"""
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device) # 将网络挪到gpu上
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,范例数
metric = d2l.Accumulator(3)
net.train()
for i, (X, y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=96)
train(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.019, train acc 0.994, test acc 0.882
854.6 examples/sec on cuda:0
He.Zhang.Ren.ea.2016
中的表 1,以实现不同的变体。参考文章详解残差网络
之后再实现
He.Zhang.Ren.ea.2016*1
中的图 1。调整一下结构即可,效果还不错
我认为第一是因为复杂的函数会增加拟合的计算要求,第二是增加了过拟合的风险,第三是复杂的函数对网络结构底层训练的要求太大,实现起来效果会很差