主要贡献:
- 网络变深以后的梯度消失,梯度爆炸问题,这个问题被BN解决。
- 网络退化问题,并不是过拟合,而是在增加更多的层后导致的训练误差。
如relu函数,低维度的特征通过relu后,会有一部分被毁掉,因为维度越低分布到relu激活带的可能性就越小。那么在反向传播的时候就会出现梯度消失,那么神经元的权重就无法更新,导致特征退化。那么理想解决办法就是对冗余数据使用relu,对不含冗余信息的使用线性激活。
对现有网络A增加恒等映射为更深的B,A和B的效果应该是一样的,所以可以证明增加深度不会导致网络的效果更差。
残差块
- 若没有右侧箭头则为最原始的:F′(x,w′)→H(x),找出w'使得F'函数逼近真实函数。
- 加入右侧箭头(Shortcut Connection)变成F(x,w)+ x → H(x)。如果w为0,那么就是恒等映射。同时此时如果H(x)产生小的变化,F比F'的变化更明显对权重的调整影响就更大,反向传播的梯度更大,调整作用更大。
- 第一个因子,为损失函数到L的梯度,而最后一项残差梯度需要经过每个带权重的层,有1的存在不会导致梯度消失。
block结构
block存在两种形式,左边适用于较浅的网络,用在ResNet34中,右图用在网络较深的时候,在ResNet50/101/152中,目的是通过1*1卷积该改变维度进而降低参数量。
网络结构图中存在部分虚线,虚线是因为feature map数量发生了变化。在ShortCut connection中加1*1的卷积使维度统一。
- 与VGGNet不同的是,ResNet除了一开始的3*3的最大池化层,其他下采样都采用的stride=2的卷积层实现。
- 当feature map的大小降低一半的时候,数量增加一倍。
- Global Avg Pool
- 局部池化主要是为了增大卷积核的感受野,最大池化保留纹理信息,平均池化保留总体信息。
- 全局池化,每个特征图被压缩到一个点,更注重通道间的关系。GAP更重视整体信息,GMP容易受极值影响。(一个物体占feature map的尺寸很大的时候,GAP更能描述特征图的相应权重。
但是放在目标跟踪方面,比如你要跟踪的是一个比较小的物体,特征图相应很大,但范围很小。而在画面上有一个比较大的不相关的物体,特征图相应虽然小,但是占的范围大。这个时候可能GAP算出来反而是大不相关的物体权重(attention)更高,这样响应极值的GMP可能更好。)
import torch
from torch import nn
from torch.nn import functional as F
# 残差模块
class ResidualBlock(nn.Module):
def __init__(self, in_ch, out_ch, stride=1, shortcut=None):
super(ResidualBlock, self).__init__()
self.left = nn.Sequential(
nn.Conv2d(in_ch, out_ch, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
nn.Conv2d(out_ch, out_ch, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(out_ch),
)
self.right = shortcut
def forward(self, x):
output = self.left(x)
# 对应网络中的实线和虚线,是否需要调整特征图维度
residual = x if self.right is None else self.right(x)
output += residual
return F.relu(output)
def _make_layer(in_ch, out_ch, block_num, stride=1):
# 维度增加时执行网络中的虚线,对shortcut 使用1*1矩阵增大维度
shortcut = nn.Sequential(
nn.Conv2d(in_ch, out_ch, 1, stride, bias=False),
nn.BatchNorm2d(out_ch),
)
layers = []
layers.append(ResidualBlock(in_ch, out_ch, stride, shortcut))
# 只有第一个shortcut需要统一特征图维度。
for i in range(1, block_num):
layers.append(ResidualBlock(out_ch, out_ch))
return nn.Sequential(*layers)
class ResNet34(nn.Module):
def __init__(self, num_classes=10):
super(ResNet34, self).__init__()
# 第一层,输入为224*224*3。
self.pre = nn.Sequential(
# (224+p*2-7)/s+1=(224+6-7)/2+1=112
nn.Conv2d(3, 64, kernel_size=7, padding=3, stride=2, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
# 输入为112*112*64,输出为56*56*64
nn.MaxPool2d(kernel_size=3, padding=1, stride=2),
)
# 四个layer分别有3,4,6,3个residual block
self.layer1 = _make_layer(64, 64, 3) # 56*56*64
self.layer2 = _make_layer(64, 128, 4, stride=2) # 28*28*128 stride=2在第一层实现下采样
self.layer3 = _make_layer(128, 256, 6, stride=2) # 14*14*256
self.layer4 = _make_layer(256, 512, 3, stride=2) # 7*7*512
# 最终的全连接层
self.Conv = nn.Sequential(
self.layer1,
self.layer2,
self.layer3,
self.layer4,
)
self.fc = nn.Linear(512, num_classes) # 7*7*512使用全局平均池化
def forward(self, x):
x = self.pre(x)
x = self.Conv(x)
x = F.avg_pool2d(x, 7)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
if __name__ == "__main__":
x = torch.rand([2, 3, 224, 224])
model = ResNet34()
print(model)
y = model(x)
print(y)
因为服务器中存在Cifar10数据,所以这部分download设为False,如果需要下载则设置为True。
# 下载CIFAR-10数据集
train_dataset = datasets.CIFAR10(root='./data', train=True, transform=data_transform, download=False)
train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=64,
shuffle=True)
test_dataset = datasets.CIFAR10('./data', train=False, transform=data_transform, download=False)
test_dataloader = torch.utils.data.DataLoader(test_dataset,
batch_size=64,
shuffle=False)
import os
import torch
from torch import nn
from model import ResNet34
from torch import optim
from torchvision import datasets, transforms
import torch.utils.data
learning_rate = 1e-3
num_epoches = 100
data_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean = [0.485, 0.456, 0.406],
std = [0.229, 0.224, 0.225])
])
# 下载CIFAR-10数据集
train_dataset = datasets.CIFAR10(root='./data', train=True, transform=data_transform, download=False)
train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=64,
shuffle=True)
test_dataset = datasets.CIFAR10('./data', train=False, transform=data_transform, download=False)
test_dataloader = torch.utils.data.DataLoader(test_dataset,
batch_size=64,
shuffle=False)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = ResNet34().to(device)
# loss和optimizer
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)
lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
def train(dataloader, model, loss_fn, optimizer):
loss, current, n = 0.0, 0.0, 0
for batch, (x, y) in enumerate(dataloader):
# 前向传播
x, y = x.to(device), y.to(device)
output = model(x)
cur_loss = loss_fn(output, y)
_, pred = torch.max(output, axis=1)
cur_acc = torch.sum(y==pred)/output.shape[0]
# 反向传播
# 清楚过往梯度
optimizer.zero_grad()
cur_loss.backward()
# 根据梯度更新网络参数
optimizer.step()
loss += cur_loss.item()
current += cur_acc.item()
n += 1
train_loss = loss / n
train_acc = current / n
# 计算训练的错误率
print('train_loss:' + str(train_loss))
# 计算训练的准确率
print('train_acc:' + str(train_acc))
def val(dataloader, model, loss_fn):
model.eval()
loss, current, n = 0.0, 0.0, 0
# with torch.no_grad():将with语句包裹起来的部分停止梯度的更新,从而节省了GPU算力和显存,但是并不会影响dropout和BN层的行为
with torch.no_grad():
for batch, (x, y) in enumerate(dataloader):
# 前向传播
x, y = x.to(device), y.to(device)
output = model(x)
cur_loss = loss_fn(output, y)
_, pred = torch.max(output, axis=1)
cur_acc = torch.sum(y == pred) / output.shape[0]
loss += cur_loss.item()
current += cur_acc.item()
n += 1
# 验证的错误率
print("val_loss: " + str(loss / n))
print("val_acc: " + str(current / n))
# 返回模型准确率
return current / n
min_acc = 0
for t in range(num_epoches):
print(f'epoch {t + 1}\n-----------------')
train(train_dataloader, model, loss_fn, optimizer)
a = val(test_dataloader, model, loss_fn)
if a > min_acc:
folder = 'save_model'
if not os.path.exists(folder):
os.mkdir('save_model')
min_acc = a
print('save best model')
torch.save(model.state_dict(), 'save_model/best_model.pth')
print('Done!')