1.背景介绍
在深度学习领域,图像分类、对象检测等任务,随着数据集的增大,数据量和计算能力越来越大,训练出的模型也越来越准确。但是如何将这些预训练模型迁移到新的场景上,却成为当前研究热点。迁移学习的目标就是利用已经训练好的预训练模型的参数值,来适应新的场景,提升模型性能。本文介绍迁移学习的相关概念和方法,并基于Pytorch库进行实战演示。
2.核心概念与联系
迁移学习(Transfer Learning)是一种机器学习的方法,它通过使用已经训练好的模型参数值,来解决新任务。它的核心思想是通过共同学习多个任务的知识,使得模型在新任务上更好地泛化能力。迁移学习可以分成如下三种类型:
- 特征提取器(Feature Extractor):用于提取深层神经网络的特征作为新模型的输入;
- 微调(Finetuning):用于微调已有模型的参数,适应新任务;
- 特征迁移(Feature Transfer):将源域中已有模型提取出来的特征,直接迁移到目标域中去; 下图展示了迁移学习的过程。 其中,左侧为源域中的训练样本,右侧为目标域中的测试样本。迁移学习过程中需要注意以下几点:
- 数据不平衡:迁移学习需要保证源域和目标域的数据分布是一致的,才能做到较好的泛化能力;
- 不可用标签信息:源域和目标域之间可能没有可用的标签信息,需要借助其他方式进行迁移学习;
- 模型容量限制:由于使用了外部数据,导致在目标域上模型的容量受限,因此迁移学习的效果可能不如从零开始训练一个模型;
3.核心算法原理和具体操作步骤以及数学模型公式详细讲解
迁移学习可以分为两步:第一步是选择合适的预训练模型;第二步是在目标域上微调预训练模型的参数,然后在目标域上进行finetune,提升模型性能。下面对这一步骤进行详细讲解。
3.1 使用预训练模型
首先,我们需要选择合适的预训练模型。目前,常用的预训练模型有VGG、ResNet、Inception V3、MobileNet等。这里以ResNet为例,介绍其主要结构、特点及优缺点。
ResNet
ResNet由多个残差单元组成,每个残差单元都有一个主路径和一个辅助路径。主路径经过卷积和ReLU运算后接归一化处理,再送入下一个残差单元;而辅助路径则直接连接输入层。这样设计能够让网络快速收敛,并防止梯度消失或爆炸。ResNet最初设计是为了解决图像分类任务上的深度残差网络问题。在实际应用中,ResNet经常跟其它层一起组合使用,比如全连接层,通过残差块堆叠,最后再连接输出层。
VGG
VGG是一个经典的深度学习模型,其特点是采用了小卷积核,能够有效降低计算复杂度,并且不增加过多参数。它由五个卷积层和三个全连接层构成,前四个卷积层中间带有最大池化层,后两个全连接层之间无池化层。
Inception V3
Inception V3是一个非常有代表性的深度学习模型,其相比于之前版本提出了许多改进措施,主要包括降低模型大小、加强特征提取力度、使用更激活函数等。其主要架构如下图所示。
其中,Inception模块由多个卷积层和较大的池化层组成。不同尺寸的卷积核会在不同的位置提取特征,并通过串联的方式来获得高阶特征表示。
除了Inception模块之外,Inception V3还使用了多项改进,比如移除全连接层,用全局平均池化代替全连接层,并引入多种优化方法来缓解梯度消失或爆炸的问题,比如批量归一化、权重衰减等。
MobileNet
MobileNet是一种轻量级的深度学习模型,在模型大小和计算量方面都有优势。该模型的主要思路是逐渐减少深度,即每一次减少32倍。使用小卷积核,通过不同尺度的卷积层来抽取特征。同时,通过引入残差连接来保留特征图大小并避免梯度消失或爆炸。
小结
以上介绍了常用的预训练模型,它们各自的优点和缺点,以及如何选取合适的模型来实现迁移学习。
3.2 在目标域上微调预训练模型的参数
这一步可以简单理解为在目标域上重新训练模型。首先,我们需要准备目标域的训练集、验证集和测试集;然后,把预训练模型加载到内存中,设置学习率等超参数;然后,使用SGD或者Adam优化器进行迭代更新参数,评估模型效果并进行保存;最后,在测试集上进行最终的测试和评价,得到最终的模型性能指标。下面给出具体操作步骤。
3.3 操作步骤细节详解
-
设置迁移学习环境
- 安装PyTorch库
pip install torch torchvision -
下载预训练模型 PyTorch提供了一些预训练模型,可以使用
torchvision.models模块直接调用。例如,可以加载VGG16预训练模型如下:import torchvision.models as models # load pre-trained model vgg16 = models.vgg16(pretrained=True) print(vgg16)此时,打印出的模型结构如下图所示。
-
修改网络结构 一般来说,我们只修改最后一层的输出类别个数。对于迁移学习,由于目标域与源域的类别数量不一致,因此,需要添加一个全连接层。例如,把VGG16的最后一层fc7替换为两层fc8如下所示:
import torch.nn as nn class CustomCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features = models.vgg16().features self.classifier = nn.Sequential( nn.Linear(512*7*7, 4096), nn.ReLU(inplace=True), nn.Dropout(), nn.Linear(4096, 4096), nn.ReLU(inplace=True), nn.Dropout(), nn.Linear(4096, num_classes) ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) x = self.classifier(x) return x -
数据预处理 通常情况下,源域和目标域的数据分布往往存在较大差异。因此,需要对源域的数据进行适当的预处理,如数据增强、归一化等。例如,定义一个
CustomDataset类,继承Dataset,实现数据读取和预处理功能:from PIL import Image from torch.utils.data import Dataset class CustomDataset(Dataset): def __init__(self, imgs, labels): self.imgs = imgs self.labels = labels def __getitem__(self, index): img_path, label = self.imgs[index], self.labels[index] img = Image.open(img_path).convert('RGB') # data augmentation if np.random.rand() > 0.5: img = TF.hflip(img) if np.random.rand() > 0.5: angle = int(np.random.rand()*15-7.5)*10 img = TF.rotate(img, angle) transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) img = transform(img) return (img, label) def __len__(self): return len(self.imgs) -
定义训练过程 定义训练过程的关键是设置超参数,如batch size、学习率、损失函数等。例如,定义一个
train()函数,完成模型训练过程:import torch from torch.optim import SGD from torch.utils.data import DataLoader from torchvision import datasets import torchvision.transforms as transforms from utils import CustomDataset def train(): device = 'cuda' if torch.cuda.is_available() else 'cpu' model = CustomCNN(num_classes=2).to(device) criterion = nn.CrossEntropyLoss() optimizer = SGD(model.parameters(), lr=0.01) # load source domain dataset and target domain dataset src_dataset = datasets.ImageFolder('/path/to/source/domain', transform=transforms.Compose([transforms.RandomResizedCrop(224),transforms.RandomHorizontalFlip(),transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])) batch_size = 32 src_loader = DataLoader(src_dataset, batch_size=batch_size, shuffle=True) tgt_loader = DataLoader(tgt_dataset, batch_size=batch_size, shuffle=True) best_acc = 0.0 for epoch in range(100): running_loss = 0.0 model.train() for i, ((src_inputs, _), (_, tgt_labels)) in enumerate(zip(enumerate(src_loader), enumerate(tgt_loader))): src_inputs, tgt_labels = src_inputs[1].to(device), tgt_labels.long().to(device) outputs = model(src_inputs) loss = criterion(outputs, tgt_labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() if i % 10 == 9: print('[%d, %5d] loss: %.3f' %(epoch+1, i+1, running_loss / 10)) running_loss = 0.0 acc = test(model, tgt_loader) if acc > best_acc: best_acc = acc torch.save(model.state_dict(), './best_model.pth') def test(model, loader): correct = 0 total = 0 with torch.no_grad(): for inputs, labels in loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() return correct / total -
执行训练过程 最后一步是执行训练过程。训练过程包括准备源域和目标域的训练集,定义训练集和测试集的DataLoader,调用
train()函数训练模型,保存最佳模型参数。