深度学习确实需要大量数据训练;
但可以站在巨人的肩膀——预训练模型;
迁移学习:借助预训练模型进行特征提取,泛化到我们自己的数据集上。
迁移学习的理论基础
卷积神经网络底层提取的是一些底层的特征如边缘、转角、颜色,中层提取比较高级的特征如形状、条纹、斑点,越到高层提取的高维特征越来越复杂,即我们提取到不同数据集的底层的特征都是普适、通用的。并且卷积层之间有脆弱的耦合关系,如果在某一层之前把网络结构冻住,紧跟着的那一层就学不太好。
比如分类问题,预训练模型中最后一层FC-1000有1000个神经元,我们可以改成自己分类个数比如C类个神经元,然后把前面所有层的结构和权重参数都冻结住(不参与训练,即不会反向传播),只在自己数据集上训练新加的FC-C层,这个层随机初始化。
当我们自己数据集比较大时,可以少冻结几层,多往前训练几层但是一般卷积层不动。
总结的来说,如果拿ImageNet做预训练模型,归纳如下所示
类似ImageNet数据集 | 非常不类似 | |
---|---|---|
自己的数据集小 | 改动线性分类输出层(新的全连接层随机初始化) | 不要全连接层甚至部分卷积层,重新提取自己数据集上的高维特征 |
自己的数据集大 | 少冻结几层卷积层,多往前训练几层 | 往前训练更多层(甚至不冻结,进行retrain重新训练,初始化还是用预训练模型参数) |
model = models.resnet18(pretrained=True) # 载入预训练模型
# 修改全连接层,使得全连接层的输出与当前数据集类别数对应
# 新建的层默认 requires_grad=True
model.fc = nn.Linear(model.fc.in_features, len(class_names))
# 只微调训练最后一层全连接层的参数,其它层冻结
optimizer = optim.SGD(model.fc.parameters(), lr=0.001, momentum=0.9)
# 微调训练所有层
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
# Decay LR by a factor of 0.1 every 7 epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)
更换backbone(以Faster RCNN为例)
#以下是替换骨干网的示例
import torchvision
from torchvision.models.detection import FasterRCNN
from torchvision.models.detection.rpn import AnchorGenerator
# 先加载一个预训练模型的特征
# only the features:其实就是不包含最后的classifier的两层
backbone = torchvision.models.mobilenet_v2(pretrained=True).features
# 或者自己选取backbone中feature模块结束的节点,即选取合适的特征输出维度,其中feature模块的索引通过打印backbone得到
backbone=create_feature_extractor(backbone,return_nodes={"features.42":"0"})
# 需要知道backbone的输出通道数,这样才能在替换时保证通道数的一致性
'''
可以通过传入tensor来获取输出通道
out=backbone(torch.rand(1,3,224,224))
print(out["0"].shape)
'''
backbone.out_channels = 1280
# 设置RPN的anchor生成器,下面的anchor_generator可以在每个空间位置生成5x3的anchors,即5个不同尺寸×3个不同的比例
anchor_generator = AnchorGenerator(sizes=((32, 64, 128, 256, 512),),
aspect_ratios=((0.5, 1.0, 2.0),))
# 定义roipool模块
roi_pooler = torchvision.ops.MultiScaleRoIAlign(featmap_names=[0],
output_size=7,
sampling_ratio=2)
# 将以上模块集成到FasterRCNN中
model = FasterRCNN(backbone,
num_classes=4,
rpn_anchor_generator=anchor_generator,
box_roi_pool=roi_pooler)
Pytorch 加载部分预训练模型并冻结某些层
多GPU并行训练时层名即key值多一个’module’
Pytorch修改预训练模型的方法汇总
torch.save(model, 'net.pth') # 保存整个神经网络的模型结构以及参数
pretrained_model = torch.load('net.pth') # 加载预训练网络的模型结构及参数
pretrained_dict = pretrained_model.state_dict()
torch.save(model.state_dict(), 'net_params.pth') # 只保存模型参数
pretrained_dict = torch.load('net_params.pth') # key是layer名称, value是对应的tensor值,cpu加载方式ckpt = torch.load(model_file, map_location='cpu')
layer_name = list(pretrained_dict.keys()) # 获取预训练模型各层的名称,并转为list
print(pretrained_dict[layer_name[2]) # 查看第2个层的参数值
# 删除预训练模型跟当前模型层名称相同,层结构却不同的元素;假如这里有两个'fc.weight'、'fc.bias'
pretrained_dict.pop('fc.weight')
pretrained_dict.pop('fc.bias')
model = MyNet() # 初始化一个自己的网络模型结构,这个结构一般是在预训练模型结构的基础上重写部分中间层
model_dict = model.state_dict() # 自己模型的所有参数,网络层名对应层的参数
print(model) # 打印模型结构
print(list(model.parameters())) # 打印模型加载参数前的可训练即可导参数
pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict} # 将pretrained_dict里不属于model_dict的键剔除掉
# 更新现有的model_dict
model_dict.update(pretrained_dict)
# 加载我们真正需要的state_dict
model.load_state_dict(model_dict)
print(list(model_conv.parameters())) # 打印加载预训练模型参数后的可训练参数值,没有对应的网络层名称
# 将要冻结层参数的requires_grad属性设置为False
for name, value in model.named_parameters():
if (name != 'fc.weight') and (name != 'fc.bias'):
value.requires_grad = False
# filter 函数将模型中属性 requires_grad = True 的参数选出来
params_grad = filter(lambda p: p.requires_grad, model.parameters())
model = model.to(device)
optimizer = optim.SGD(params_grad, lr=0.001, momentum=0.9)
#先将模型的网络层列表化,每一层对应列表一个元素,bottleneck对应一个序列层
net_structure = list(model.children())
print(net_structure)
#去除最后两层得到新的网络
resnet_modified = nn.Sequential(*net_structure[:-2])
#去除后两层后构建新网络
class Net(nn.Module):
def __init__(self , model):
super(Net, self).__init__()
#去掉model的后两层
self.resnet_layer = nn.Sequential(*list(model.children())[:-2])
self.transion_layer = nn.ConvTranspose2d(2048, 2048, kernel_size=14, stride=3)
self.pool_layer = nn.MaxPool2d(32)
self.Linear_layer = nn.Linear(2048, 8)
def forward(self, x):
x = self.resnet_layer(x)
x = self.transion_layer(x)
x = self.pool_layer(x)
x = x.view(x.size(0), -1)
x = self.Linear_layer(x)
return x
'''训练配置'''
model = model.to(device)
# 交叉熵损失函数
criterion = nn.CrossEntropyLoss()
# 学习率指数下降策略,每 4 个 epoch, 降为原来的 0.1
scheduler = lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)
# 训练轮次 Epoch
EPOCHS = 20
'''train'''
# 遍历每个 EPOCH
for epoch in tqdm(range(EPOCHS)):
model.train()
# 遍历每个 batch
for images, labels in train_loader: # 获得一个 batch 的数据和标注
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
'''test'''
model.eval()
with torch.no_grad():
correct = 0
total = 0
for images, labels in tqdm(test_loader):
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, preds = torch.max(outputs, 1)
total += labels.size(0)
correct += (preds == labels).sum()
print('测试集上的准确率为 {:.3f} %'.format(100 * correct / total))
经典的卷积神经网络,可以用这些最优秀的预训练模型当backbone来做上游任务特征提取,从而来fine-tuning做我们自己的各种下游任务,如目标检测、语义分割等。
ImageNet图像分类竞赛历年冠军 | … |
---|---|
AlexNet | 2012冠军 Hinton团队 多伦多大学 |
VGG | 2014亚军 VGG16和VGG19 牛津大学 |
GoogleNet | 2014冠军 Inception模块 谷歌 |
ResNet | 2015冠军 残差模块 何恺明团队 微软亚洲研究院 |
---- | ---- |
Also | … |
SENet | 2017冠军 SE模块 Momenta+牛津 胡杰团队 |
NiN(Network in Network) | 网中网 1×1卷积 global average pooling |
Wide ResNet | 增加残差块中军机和数量(宽度) |
ResNetXT | ResNet + Inception |
DenseNet | 2017CVPR最佳论文 Dense模块 我的附庸的附庸,也是我的附庸 |
FractalNet | 分形网络 |
MobileNets | Group卷积核Depthwise Seperable卷积 |
NASNet | 2018 神经架构搜索 强化学习 谷歌大脑 |