06-PyTorch迁移学习:在预训练模型上进行训练
本文为PyTorch Transfer Learning的学习笔记,对原文进行了翻译和编辑,本系列课程介绍和目录在《使用 PyTorch 进行深度学习系列》课程介绍。
文章将最先在我的博客发布,其他平台因为限制不能实时修改。
在微信公众号内无法嵌入超链接,可以点击底部阅读原文获得更好的阅读体验。
到目前为止,我们已经手工构建了一些模型。但他们的表现却很差。您可能会想,是否能借鉴一个已经被训练好的模型?我们将了解如何使用一种称为迁移学习的强大技术。
什么是迁移学习?
迁移学习允许我们采用另一个模型从另一个问题中学到的模式(也称为权重)并将它们用于我们自己的问题。
例如,我们可以采用计算机视觉模型从 ImageNet(数百万张不同对象的图像)等数据集学习到的模式,并使用它们来为我们的 FoodVision Mini 模型提供支持。
或者,我们可以从语言模型(通过大量文本来学习语言表示的模型)中获取模式,并将它们用作模型的基础来对不同的文本样本进行分类。
前提仍然是:找到一个性能良好的现有模型并将其应用于您自己的问题。
迁移学习应用于计算机视觉和自然语言处理 (NLP) 的示例。就计算机视觉而言,计算机视觉模型可能会学习 ImageNet 中数百万张图像的模式,然后使用这些模式来推断另一个问题。对于 NLP 来说,语言模型可以通过阅读所有维基百科(也许更多)来学习语言结构,然后将这些知识应用于不同的问题。
为什么要使用迁移学习?
使用迁移学习有两个主要好处:
-
可以利用已证明可以解决与我们类似的问题的现有模型(通常是神经网络架构)。
-
可以利用一个工作模型,该模型已经学习了与我们自己类似的数据的模式。这通常会用更少的自定义数据获得很好的结果。
我们将针对 FoodVision Mini 问题对这些进行测试,我们将采用在 ImageNet 上预训练的计算机视觉模型,并尝试利用其底层学习表示对披萨、牛排和寿司的图像进行分类。
最近的一篇机器学习研究论文的发现建议从业者尽可能使用迁移学习:
在哪里可以找到预训练模型
Location 地点 | What’s there? 什么东西在那里? | Link(s) 链接) |
---|---|---|
PyTorch domain libraries | 每个 PyTorch 域库( torchvision 、 torchtext )都附带某种形式的预训练模型。那里的模型可以在 PyTorch 中运行。 |
torchvision.models , torchtext.models , torchaudio.models , torchrec.models torchvision.models 、 torchtext.models 、 torchaudio.models 、 torchrec.models |
HuggingFace Hub(推荐) | 来自世界各地的组织针对许多不同领域(视觉、文本、音频等)的一系列预训练模型。还有很多不同的数据集。 | https://huggingface.co/models, https://huggingface.co/datasets |
timm |
(PyTorch 图像模型)库,PyTorch 代码中几乎所有最新、最好的计算机视觉模型以及许多其他有用的计算机视觉功能。 | https://github.com/rwightman/pytorch-image-models |
带代码的论文 | 最新最先进的机器学习论文的集合,并附有代码实现。您还可以在此处找到不同任务的模型性能基准。 | https://paperswithcode.com/ |
2.创建数据集和 DataLoader
2.1 手动创建 transforms
# 手动创建转换变化
manual_transforms = transforms.Compose([
transforms.Resize((224, 224)), # 1. 将所有图像重新调整为 224x224 的大小(尽管某些模型可能需要不同的大小)
transforms.ToTensor(), # 2. 将图像值转换为介于 0 和 1 之间
transforms.Normalize(mean=[0.485, 0.456, 0.406], # 3. 使用 [0.485, 0.456, 0.406] 的均值(在每个颜色通道上)
std=[0.229, 0.224, 0.225]) # 4. 使用 [0.229, 0.224, 0.225] 的标准差(在每个颜色通道上)
])
平均值和标准差值从何而来?为什么我们需要这样做?
这些是根据数据计算出来的。具体来说,ImageNet 数据集通过获取图像子集的平均值和标准差来实现。我们也不需要这样做。神经网络通常非常有能力计算出适当的数据分布(它们将自行计算平均值和标准差),但在开始时设置它们可以帮助我们的网络更快地实现更好的性能。
# 创建DataLoader
"""
包含用于创建图像分类数据的 PyTorch DataLoaders 的功能。
"""
import os
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
NUM_WORKERS = os.cpu_count()
def create_dataloaders(
train_dir: str,
test_dir: str,
transform: transforms.Compose,
batch_size: int,
num_workers: int=NUM_WORKERS
):
"""创建训练和测试的 DataLoaders。
接受训练目录和测试目录路径,并将它们转换为 PyTorch 数据集,然后转换为 PyTorch DataLoaders。
参数:
train_dir: 训练目录的路径。
test_dir: 测试目录的路径。
transform: 在训练和测试数据上执行的 torchvision 转换。
batch_size: 每个 DataLoader 中的批次样本数。
num_workers: 每个 DataLoader 的工作进程数。
返回:
一个由 (train_dataloader, test_dataloader, class_names) 组成的元组。
其中 class_names 是目标类别的列表。
示例用法:
train_dataloader, test_dataloader, class_names = \
= create_dataloaders(train_dir=path/to/train_dir,
test_dir=path/to/test_dir,
transform=some_transform,
batch_size=32,
num_workers=4)
"""
# 使用 ImageFolder 创建数据集
train_data = datasets.ImageFolder(train_dir, transform=transform)
test_data = datasets.ImageFolder(test_dir, transform=transform)
# 获取类别名称
class_names = train_data.classes
# 将图像转换为数据加载器
train_dataloader = DataLoader(
train_data,
batch_size=batch_size,
shuffle=True,
num_workers=num_workers,
pin_memory=True,
)
test_dataloader = DataLoader(
test_data,
batch_size=batch_size,
shuffle=False,
num_workers=num_workers,
pin_memory=True,
)
return train_dataloader, test_dataloader, class_names
# 设置路径
train_dir = image_path / "train"
test_dir = image_path / "test"
train_dataloader, test_dataloader, class_names = create_dataloaders(train_dir=train_dir,
test_dir=test_dir,
transform=manual_transforms, # 调整大小,转换图像为张量,正则化
batch_size=32) # 设置32 个样本的小批量
train_dataloader, test_dataloader, class_names
out:
(<torch.utils.data.dataloader.DataLoader at 0x7fa9429a3a60>,
<torch.utils.data.dataloader.DataLoader at 0x7fa9429a37c0>,
['pizza', 'steak', 'sushi'])
2.2 使用 torchvision.models 进行自动 transform
从 torchvision
v0.13+ 开始,添加了自动 transform 功能。
当您从 torchvision.models
设置模型并选择您想要使用的预训练模型权重时,例如,假设我们想要使用:weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT
:
EfficientNet_B0_Weights
是我们想要使用的模型架构权重(torchvision.models
中有许多不同的模型架构选项)。DEFAULT
表示最佳可用权重(ImageNet 中的最佳性能)。
# 获取预训练模型
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT
weights
>>>
EfficientNet_B0_Weights.IMAGENET1K_V1
现在要访问与 weights
相关的转换,我们可以使用 transforms()
方法。
这本质上是说“获取用于在 ImageNet 上训练 EfficientNet_B0_Weights
的数据转换”。
auto_transforms = weights.transforms()
auto_transforms
out:
ImageClassification(
crop_size=[224]
resize_size=[256]
mean=[0.485, 0.456, 0.406]
std=[0.229, 0.224, 0.225]
interpolation=InterpolationMode.BICUBIC
)
通过 weights.transforms()
自动创建转换的好处是,您可以确保使用与训练时使用的预训练模型相同的数据转换。
# 自动转换
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(train_dir=train_dir,
test_dir=test_dir,
transform=auto_transforms,
batch_size=32)
train_dataloader, test_dataloader, class_names
3. 获取预训练模型
于我们正在研究计算机视觉问题(使用 FoodVision Mini 进行图像分类),因此我们可以在 [torchvision.models
](https://pytorch.org/vision/stable/models.html#classification) 中找到预训练的分类模型。
浏览文档,您会发现大量常见的计算机视觉架构主干,例如:ResNet’s,VGG,EfficientNet’s ,VisionTransformer (ViT’s),ConvNeXt等。
3.2 建立预训练模型
我们将使用的预训练模型是 torchvision.models.efficientnet_b0()
。该架构来自论文 EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks。
我们要创建的示例是来自 torchvision.models
的预训练 EfficientNet_B0
模型,其输出层根据我们对披萨、牛排和寿司图像进行分类的用例进行了调整。
我们可以使用与创建转换相同的代码来设置 EfficientNet_B0
预训练的 ImageNet 权重。该模型已经过数百万张图像的训练,并且具有良好的图像数据基础表示。该预训练模型的 PyTorch 版本能够在 ImageNet 的 1000 个类别中实现约 77.7% 的准确率。
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT
model = torchvision.models.efficientnet_b0(weights=weights).to(device)
我们的 efficientnet_b0
分为三个主要部分:
features
- 卷积层和其他各种激活层的集合,用于学习视觉数据的基本表示(此基本表示/层集合通常称为特征或特征提取器,“视觉数据的基本层”,模型在此层学习图像的不同特征)。avgpool
- 取features
层输出的平均值并将其转换为特征向量。classifier
- 将特征向量转换为与所需输出类数量具有相同维度的向量(因为efficientnet_b0
是在 ImageNet 上预训练的,并且 ImageNet 有 1000 个类:out_features=1000
3.3 使用 torchinfo.summary() 查看模型信息
summary(model=model,
input_size=(32, 3, 224, 224), # make sure this is "input_size", not "input_shape"
# col_names=["input_size"], # uncomment for smaller output
col_names=["input_size", "output_size", "num_params", "trainable"],
col_width=20,
row_settings=["var_names"]
)
- 训练列 – 您将看到许多基础层(
features
部分中的层)的可训练值为False
。这是因为我们设置了requires_grad=False
,这些层在未来的训练期间不会更新。 classifier
的输出形状 - 模型的classifier
部分现在的输出形状值为[32, 3]
而不是[32, 1000]
。它的可训练值requires_grad
也是True
。这意味着它的参数将在训练期间更新。本质上,我们使用features
部分为classifier
部分提供图像的基本表示,然后我们的classifier
层将学习如何基本表示与我们的问题相符。- 可训练参数更少 - 之前有 5,288,548 个可训练参数。但由于我们冻结了模型的许多层,只留下
classifier
可训练,因此现在只有 3,843 个可训练参数(甚至比我们的 TinyVGG 模型还要少)。尽管还有 4,007,548 个不可训练参数,但这些参数将创建输入图像的基本表示,最终影像到我们的classifier
层。
注意:模型的可训练参数越多,计算能力就越强/训练时间就越长。冻结模型的基础层并保留较少的可训练参数意味着我们的模型应该训练得更快。这是迁移学习的一大好处,它采用对于问题训练的模型的已学习参数,并且仅稍微调整输出以适应您的问题。
4. 训练模型
现在我们已经有了一个半冻结的预训练模型,并且有一个定制的 classifier
,我们在此基础上训练模型,看看效果。
首先,我们创建一个损失函数和一个优化器。
我们仍在处理多类分类,所以我们将使用 nn.CrossEntropyLoss()
作为损失函数,并且使用 lr=0.001
的torch.optim.Adam()
的优化器。
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
为了训练我们的模型,我们可以先定义一个train()
函数,以下是一个完整函数,将其保存到 going_modular 文件夹下的 engine.py 文件中。
"""
包含用于训练和测试PyTorch模型的函数。
"""
import torch
from tqdm.auto import tqdm
from typing import Dict, List, Tuple
def train_step(model: torch.nn.Module,
dataloader: torch.utils.data.DataLoader,
loss_fn: torch.nn.Module,
optimizer: torch.optim.Optimizer,
device: torch.device) -> Tuple[float, float]:
"""对PyTorch模型进行单个epoch的训练。
将目标PyTorch模型设置为训练模式,然后执行所有必要的训练步骤(前向传播、损失计算、优化器步骤)。
参数:
model:要训练的PyTorch模型。
dataloader:用于训练模型的DataLoader实例。
loss_fn:要最小化的PyTorch损失函数。
optimizer:帮助最小化损失函数的PyTorch优化器。
device:计算设备(例如:"cuda"或"cpu")。
返回:
训练损失和训练准确率的元组。
格式为(train_loss,train_accuracy)。例如:
(0.1112,0.8743)
"""
# 将模型设置为训练模式
model.train()
# 设置训练损失和训练准确率的初始值
train_loss, train_acc = 0, 0
# 遍历数据加载器中的数据批次
for batch, (X, y) in enumerate(dataloader):
# 将数据发送到目标设备
X, y = X.to(device), y.to(device)
# 1. 前向传播
y_pred = model(X)
# 2. 计算并累加损失
loss = loss_fn(y_pred, y)
train_loss += loss.item()
# 3. 优化器梯度清零
optimizer.zero_grad()
# 4. 反向传播计算梯度
loss.backward()
# 5. 优化器更新参数
optimizer.step()
# 计算并累加准确率指标
y_pred_class = torch.argmax(torch.softmax(y_pred, dim=1), dim=1)
train_acc += (y_pred_class == y).sum().item()/len(y_pred)
# 调整指标以获得每个批次的平均损失和准确率
train_loss = train_loss / len(dataloader)
train_acc = train_acc / len(dataloader)
return train_loss, train_acc
def test_step(model: torch.nn.Module,
dataloader: torch.utils.data.DataLoader,
loss_fn: torch.nn.Module,
device: torch.device) -> Tuple[float, float]:
"""对PyTorch模型进行单个epoch的测试。
将目标PyTorch模型设置为“eval”模式,然后在测试数据集上执行前向传播。
参数:
model:要测试的PyTorch模型。
dataloader:用于测试模型的DataLoader实例。
loss_fn:用于计算测试数据上的损失的PyTorch损失函数。
device:计算设备(例如:"cuda"或"cpu")。
返回:
测试损失和测试准确率的元组。
格式为(test_loss,test_accuracy)。例如:
(0.0223,0.8985)
"""
# 将模型设置为评估模式
model.eval()
# 设置测试损失和测试准确率的初始值
test_loss, test_acc = 0, 0
# 打开推理上下文管理器
with torch.inference_mode():
# 遍历DataLoader中的数据批次
for batch, (X, y) in enumerate(dataloader):
# 将数据发送到目标设备
X, y = X.to(device), y.to(device)
# 1. 前向传播
test_pred_logits = model(X)
# 2. 计算并累加损失
loss = loss_fn(test_pred_logits, y)
test_loss += loss.item()
# 计算并累加准确率
test_pred_labels = test_pred_logits.argmax(dim=1)
test_acc += ((test_pred_labels == y).sum().item()/len(test_pred_labels))
# 调整指标以获得每个批次的平均损失和准确率
test_loss = test_loss / len(dataloader)
test_acc = test_acc / len(dataloader)
return test_loss, test_acc
def train(model: torch.nn.Module,
train_dataloader: torch.utils.data.DataLoader,
test_dataloader: torch.utils.data.DataLoader,
optimizer: torch.optim.Optimizer,
loss_fn: torch.nn.Module,
epochs: int,
device: torch.device) -> Dict[str, List]:
"""训练和测试PyTorch模型。
将目标PyTorch模型通过train_step()和test_step()函数进行多个epoch的训练和测试,
在同一个epoch循环中训练和测试模型。
计算、打印和存储评估指标。
参数:
model:要训练和测试的PyTorch模型。
train_dataloader:用于训练模型的DataLoader实例。
test_dataloader:用于测试模型的DataLoader实例。
optimizer:帮助最小化损失函数的PyTorch优化器。
loss_fn:用于计算两个数据集上的损失的PyTorch损失函数。
epochs:要训练的epoch数。
device:计算设备(例如:"cuda"或"cpu")。
返回:
包含训练和测试损失以及训练和测试准确率指标的字典。
每个指标都有一个列表值,表示每个epoch的指标。
格式为:{train_loss: [...],
train_acc: [...],
test_loss: [...],
test_acc: [...]}
例如,如果训练2个epochs:
{train_loss: [2.0616, 1.0537],
train_acc: [0.3945, 0.3945],
test_loss: [1.2641, 1.5706],
test_acc: [0.3400, 0.2973]}
"""
# 创建空的结果字典
results = {"train_loss": [],
"train_acc": [],
"test_loss": [],
"test_acc": []
}
# 确保模型在目标设备上
model.to(device)
# 循环进行训练和测试步骤,直到达到指定的epoch数
for epoch in tqdm(range(epochs)):
train_loss, train_acc = train_step(model=model,
dataloader=train_dataloader,
loss_fn=loss_fn,
optimizer=optimizer,
device=device)
test_loss, test_acc = test_step(model=model,
dataloader=test_dataloader,
loss_fn=loss_fn,
device=device)
# 打印当前进度
print(
f"Epoch: {epoch+1} | "
f"train_loss: {train_loss:.4f} | "
f"train_acc: {train_acc:.4f} | "
f"test_loss: {test_loss:.4f} | "
f"test_acc: {test_acc:.4f}"
)
# 更新结果字典
results["train_loss"].append(train_loss)
results["train_acc"].append(train_acc)
results["test_loss"].append(test_loss)
results["test_acc"].append(test_acc)
# 在训练结束时返回填充的结果字典
return results
接下来进行训练:
# 设置随机种子
torch.manual_seed(42)
torch.cuda.manual_seed(42)
# 开始计时器
from timeit import default_timer as timer
start_time = timer()
# 开始训练并且保存结果
results = engine.train(model=model,
train_dataloader=train_dataloader,
test_dataloader=test_dataloader,
optimizer=optimizer,
loss_fn=loss_fn,
epochs=5,
device=device)
# 返回时间
end_time = timer()
print(f"[INFO] Total training time: {end_time-start_time:.3f} seconds")
OUT:
0%| | 0/5 [00:00<?, ?it/s]
Epoch: 1 | train_loss: 1.0924 | train_acc: 0.3984 | test_loss: 0.9133 | test_acc: 0.5398
Epoch: 2 | train_loss: 0.8717 | train_acc: 0.7773 | test_loss: 0.7912 | test_acc: 0.8153
Epoch: 3 | train_loss: 0.7648 | train_acc: 0.7930 | test_loss: 0.7463 | test_acc: 0.8561
Epoch: 4 | train_loss: 0.7108 | train_acc: 0.7539 | test_loss: 0.6372 | test_acc: 0.8655
Epoch: 5 | train_loss: 0.6254 | train_acc: 0.7852 | test_loss: 0.6260 | test_acc: 0.8561
[INFO] Total training time: 8.977 seconds
借助 efficientnet_b0
主干,我们的模型在测试数据集上实现了近 85% 以上的准确率,几乎是我们使用 TinyVGG 实现的准确率的两倍。
5. 通过绘制损失曲线来评估模型
使用我们在 《04. PyTorch 自定义数据集》创建的函数 plot_loss_curves()
绘制损失曲线,看看随着时间的推移,模型效果怎么样。该函数存储在 helper_functions.py
脚本中,因此我们将尝试导入它并下载脚本(如果没有下载)。
# Get the plot_loss_curves() function from helper_functions.py, download the file if we don't have it
try:
from helper_functions import plot_loss_curves
except:
print("[INFO] Couldn't find helper_functions.py, downloading...")
with open("helper_functions.py", "wb") as f:
import requests
request = requests.get("https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/helper_functions.py")
f.write(request.content)
from helper_functions import plot_loss_curves
# Plot the loss curves of our model
plot_loss_curves(results)
看起来效果不错,损失逐步降低,准确度逐步上升。
这证明了迁移学习的力量。使用预训练模型通常可以在更短的时间内用少量数据产生非常好的结果。
6. 对测试集中的图像进行预测
为了让我们的模型对图像进行预测,该图像必须与我们的模型所训练的图像具有相同的格式:
- Same shape - 如果我们的图像与模型训练时的形状不同,我们会得到形状错误。
- Same datatype - 如果我们的图像是不同的数据类型(例如
torch.int8
与torch.float32
),我们将收到数据类型错误。 - Same device - 如果我们的图像与我们的模型位于不同的设备上,我们将收到设备错误。
- Same transformations - 如果我们的模型是在以某种方式转换的图像上进行训练的(例如,用特定的平均值和标准差进行标准化),并且我们尝试对以不同方式转换的图像进行预测,那么这些预测可能会失败。
为了完成所有这些,我们将创建一个函数 pred_and_plot_image()
:
from typing import List, Tuple
from PIL import Image
# 1. 接受经过训练的模型、类名列表、目标图像的文件路径、图像大小、转换和目标设备。
def pred_and_plot_image(model: torch.nn.Module,
image_path: str,
class_names: List[str],
image_size: Tuple[int, int] = (224, 224),
transform: torchvision.transforms = None,
device: torch.device=device):
# 2. 使用 PIL.Image.open() 打开图像
img = Image.open(image_path)
# 3. 为图像创建一个转换(这将默认为我们上面创建的 manual_transforms 或者它可以使用从 weights.transforms() 生成的转换)。
if transform is not None:
image_transform = transform
else:
image_transform = transforms.Compose([
transforms.Resize(image_size),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])
### 预测图形 ###
# 4. 确保该模型位于目标设备上。
model.to(device)
# 5. 使用 model.eval() 打开模型评估模式(这会关闭 nn.Dropout() 等层,因此它们不用于推理)和推理模式上下文管理器。
model.eval()
with torch.inference_mode():
# 6. 使用步骤 3 中进行的变换来变换目标图像,并使用 torch.unsqueeze(dim=0) 添加额外的批量维度,以便我们的输入图像具有形状 [batch_size, color_channels, height, width] 。
transformed_image = image_transform(img).unsqueeze(dim=0)
# 7. 通过将图像传递给模型来对图像进行预测,确保它位于目标设备上。
target_image_pred = model(transformed_image.to(device))
# 8. 使用 torch.softmax() 将模型的输出 logits 转换为预测概率
target_image_pred_probs = torch.softmax(target_image_pred, dim=1)
# 9. 使用 torch.argmax() 将模型的预测概率转换为预测标签。
target_image_pred_label = torch.argmax(target_image_pred_probs, dim=1)
# 10. 使用 matplotlib 绘制图像,并将标题设置为步骤 9 中的预测标签和步骤 8 中的预测概率
plt.figure()
plt.imshow(img)
plt.title(f"Pred: {class_names[target_image_pred_label]} | Prob: {target_image_pred_probs.max():.3f}")
plt.axis(False);
让我们通过对测试集中的一些随机图像进行预测来测试它。
我们可以使用 list(Path(test_dir).glob("*/*.jpg"))
获取所有测试图像路径的列表。
然后我们可以使用 Python 的 random.sample(populuation, k)
随机采样其中的多个,其中 population
是要采样的序列, k
是要检索的样本数量。
import random
num_images_to_plot = 3
test_image_path_list = list(Path(test_dir).glob("*/*.jpg")) # 获取所有图片
test_image_path_sample = random.sample(population=test_image_path_list, # 遍历所有图片文件路径
k=num_images_to_plot) # 随机选择 'k' 个文件
# 预测并绘制结果
for image_path in test_image_path_sample:
pred_and_plot_image(model=model,
image_path=image_path,
class_names=class_names,
# transform=weights.transforms(), # 可选
image_size=(224, 224))
这些预测看起来比我们的 TinyVGG 模型之前做出的预测要好得多。
感谢
感谢原作者 Daniel Bourke,访问https://www.learnpytorch.io/可以阅读英文原文,点击原作者的 Github 仓库:https://github.com/mrdbourke/pytorch-deep-learning/可以获得帮助和其他信息。
本文同样遵守遵守 MIT license,不受任何限制,包括但不限于权利
使用、复制、修改、合并、发布、分发、再许可和/或出售。但需标明原始作者的许可信息:renhai-lab:https://blog.renhai.online/
。