计算机视觉基础
Reference: PyTorch Computer Vision
该页面由 Jupyter Notebook 生成,原文件于 Github
PyTorch 中关于计算机视觉的库有:
模块 | 作用 |
---|---|
torchvision | 包含通常用于计算机视觉问题的数据集、模型架构和图像转换 |
torchvision.datasets | 包含许多计算机视觉数据集,用于解决图像分类、对象检测、图像字幕、视频分类等一系列问题,还包含一系列用于创建自定义数据集的基类 |
torchvision.models | 包含在 PyTorch 中实现的性能良好且常用的计算机视觉模型架构,可以将其用于解决问题 |
torchvision.transforms | 图像需要在与模型一起使用之前进行预处理(转换为数字/处理/增强),包含常见的图像转换 |
torch.utils.data.Dataset | PyTorch 基础数据集类 |
torch.utils.data.DataLoader | 创建一个可在数据集上迭代的对象(torch.utils.data.Dataset ) |
# PyTorch
import torch
from torch import nn
# torchvision
import torchvision
from torchvision import datasets
from torchvision.transforms import ToTensor
# matplotlib
import matplotlib.pyplot as plt
print(f"{torch.__version__}\n{torchvision.__version__}")
2.5.1+cu124
0.20.1+cu124
device = "cuda" if torch.cuda.is_available() else "cpu"
device
'cuda'
torchvision.datasets
包含了大量的示例数据集,可以使用它们来练习编写计算机视觉代码。
- MNIST:手写数字数据集,包含数千个手写数字(从0到9)的示例。
- FashionMNIST:有 10 个不同的图像类(不同类型的衣服),这是一个多类分类问题。
FashionMNIST 可以通过 torchvision.datasets.FashionMNIST
获得。提供以下参数:
root: str
,数据下载到哪个文件夹;train: Bool
,训练还是测试分割;download: Bool
,是否下载数据;transform: torchvision.transforms
,对数据的转换;target_transform
,对目标(标签)的转换。
train_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor(), #图像以 PIL 格式出现,将其转换为 Torch 张量
target_transform=None
)
test_data = datasets.FashionMNIST(
root="data",
train=False, # 表示测试
download=True,
transform=ToTensor()
)
len(train_data), len(test_data)
(60000, 10000)
得到数据后,需要了解数据的 Shape:
image, label = train_data[0]
image.shape
torch.Size([1, 28, 28])
图像张量的形状是 [1,28,28]
或更具体地:[color_channels=1,height=28,width=28]
。
不同的问题会有不同的输入和输出形式。但前提仍然是将数据编码成数字,建立一个模型来找到这些数字中的模式,将这些模式表达成有意义的东西。
除了 CHW(通道,高度,宽度) 表示,后续还会见到 NCHW 和 NHWC 格式,其中 N 代表图像数量。例如,当 batch_size=32,则张量形状可能是 [32,1,28,28]。
PyTorch 通常接受 NCHW(通道优先)作为许多操作的默认设置。然而,PyTorch 还解释说,NHWC(通道最后)性能更好,被认为是最佳实践。
由于目前的数据集和模型相对较小,这不会有太大的区别。
了解数据集的数量后,还可以了解一下类别属性:
class_names = train_data.classes
class_names
['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
可以可视化一下数据:
image, label = train_data[0]
plt.figure(figsize=(4, 4))
plt.imshow(image.squeeze(), cmap="gray") # Shape为 [1, 28, 28],将通道挤压一下
plt.title(label)
Text(0.5, 1.0, '9')
查看更多:
torch.manual_seed(42)
fig = plt.figure(figsize=(7, 7))
rows, cols = 4, 4
for i in range(1, rows * cols + 1):
random_idx = torch.randint(0, len(train_data), size=[1]).item()
img, label = train_data[random_idx]
fig.add_subplot(rows, cols, i)
plt.imshow(img.squeeze(), cmap="gray")
plt.title(class_names[label])
plt.axis(False)
现在已经有了一个数据集,下一步是用 torch.utils.data.DataLoader
准备它。
DataLoader 有助于将数据加载到模型中。为了训练和推理,它将一个大的数据集转换成一个可迭代的小块。
- 这些较小的块称为批处理或小批处理,可以通过
batch_size
参数进行设置,这样做可以让计算效率更高。
对于小批量(数据的一小部分)而言,每个 epoch 执行梯度下降的频率更高。
- 每个 batch 一次,而不是每个 epoch 一次。
batch_size
是一个超参数,视情况而定。通常使用 2 的幂 (例如32、64、128、256、512)。
from torch.utils.data import DataLoader
BATCH_SIZE = 32
train_dataloader = DataLoader(train_data,
batch_size=BATCH_SIZE,
shuffle=True # 每一个epoch 都洗牌数据
)
test_dataloader = DataLoader(test_data,
batch_size=BATCH_SIZE,
shuffle=False
)
print(f"Train: {len(train_dataloader)}")
print(f"Test: {len(test_dataloader)}")
Train: 1875
Test: 313
看看每个 batch 的 shape:
train_features_batch, train_labels_batch = next(iter(train_dataloader))
train_features_batch.shape, train_labels_batch.shape
(torch.Size([32, 1, 28, 28]), torch.Size([32]))
数据已加载并准备就绪。
基线模型是最简单的模型之一。使用基线作为起点,并尝试使用后续的更复杂的模型对其进行改进。目前基线模型将由两个 nn.Linear()
层组成。
因为这是图像数据,所以将使用 nn.Flatten()
层开始。
nn.Flatten()
将张量的维度压缩为单个向量。
class FashionMNISTModelV0(nn.Module):
def __init__(self, input_shape: int, hidden_units: int, output_shape: int):
super().__init__()
self.model = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=input_shape, out_features=hidden_units),
nn.Linear(in_features=hidden_units, out_features=output_shape)
)
def forward(self, x):
return self.model(x)
设置以下参数:
input_shape=784
:此例中,目标图像中的每个像素都有一个特征(28像素高× 28像素宽= 784个特征)。hidden_units=10
:隐藏层中的神经元数量。output_shape=len(class_names)
:多分类问题,需要为数据集中的每个类提供一个输出神经元。
torch.manual_seed(42)
model_0 = FashionMNISTModelV0(input_shape=784,
hidden_units=10,
output_shape=len(class_names)
)
设置上损失函数和优化器:
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(params=model_0.parameters(), lr=0.1)
自定义计算准确率的函数和计时函数:
def accuracy_fn(y_true, y_pred):
correct = torch.eq(y_true, y_pred).sum().item()
acc = (correct / len(y_pred)) * 100
return acc
from timeit import default_timer as timer
def print_train_time(start: float, end: float, device: torch.device = None):
total_time = end - start
print(f"{device}: {total_time:.3f} seconds")
return total_time
构建训练和测试循环(CPU):
torch.manual_seed(42)
train_time_start = timer()
epochs = 3
for epoch in range(epochs):
print(f"Epoch: {epoch}\n-------")
train_loss = 0
for batch, (X, y) in enumerate(train_dataloader):
model_0.train()
y_pred = model_0(X)
loss = loss_fn(y_pred, y)
train_loss += loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss /= len(train_dataloader)
test_loss, test_acc = 0, 0
model_0.eval()
with torch.inference_mode():
for X, y in test_dataloader:
test_pred = model_0(X)
test_loss += loss_fn(test_pred, y)
test_acc += accuracy_fn(y_true=y, y_pred=test_pred.argmax(dim=1))
test_loss /= len(test_dataloader)
test_acc /= len(test_dataloader)
print(f"\n训练 loss: {train_loss:.5f} | 测试 loss: {test_loss:.5f}, 测试 acc: {test_acc:.2f}%\n")
train_time_end = timer()
total_train_time_model_0 = print_train_time(start=train_time_start,
end=train_time_end,
device=str(next(model_0.parameters()).device))
Epoch: 0
-------
训练 loss: 0.59039 | 测试 loss: 0.50954, 测试 acc: 82.04%
Epoch: 1
-------
训练 loss: 0.47633 | 测试 loss: 0.47989, 测试 acc: 83.20%
Epoch: 2
-------
训练 loss: 0.45503 | 测试 loss: 0.47664, 测试 acc: 83.43%
cpu: 37.968 seconds
创建一个函数,接受一个训练过的模型,一个 DataLoader,一个损失函数和一个精度函数,使用模型对 DataLoader 中的数据进行预测,然后使用损失函数和精度函数来评估这些预测。
torch.manual_seed(42)
def eval_model(model: torch.nn.Module,
data_loader: torch.utils.data.DataLoader,
loss_fn: torch.nn.Module,
accuracy_fn,
device: torch.device = device):
"""Evaluates a given model on a given dataset.
Args:
model (torch.nn.Module): A PyTorch model capable of making predictions on data_loader.
data_loader (torch.utils.data.DataLoader): The target dataset to predict on.
loss_fn (torch.nn.Module): The loss function of model.
accuracy_fn: An accuracy function to compare the models predictions to the truth labels.
device (str, optional): Target device to compute on. Defaults to device.
Returns:
(dict): Results of model making predictions on data_loader.
"""
loss, acc = 0, 0
model.eval()
with torch.inference_mode():
for X, y in data_loader:
# Send data to the target device
X, y = X.to(device), y.to(device)
y_pred = model(X)
loss += loss_fn(y_pred, y)
acc += accuracy_fn(y_true=y, y_pred=y_pred.argmax(dim=1))
# Scale loss and acc
loss /= len(data_loader)
acc /= len(data_loader)
return {"model_name": model.__class__.__name__, # only works when model was created with a class
"model_loss": loss.item(),
"model_acc": acc}
# Calculate model 0 results on test dataset
model_0_results = eval_model(model=model_0,
data_loader=test_dataloader,
loss_fn=loss_fn,
accuracy_fn=accuracy_fn,
device=str(next(model_0.parameters()).device)
)
model_0_results
{'model_name': 'FashionMNISTModelV0', 'model_loss': 0.47663894295692444, 'model_acc': 83.42651757188499}
class FashionMNISTModelV1(nn.Module):
def __init__(self, input_shape: int, hidden_units: int, output_shape: int):
super().__init__()
self.model = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=input_shape, out_features=hidden_units),
nn.ReLU(),
nn.Linear(in_features=hidden_units, out_features=output_shape),
nn.ReLU()
)
def forward(self, x: torch.Tensor):
return self.model(x)
接着实例化:
torch.manual_seed(42)
model_1 = FashionMNISTModelV1(input_shape=784,
hidden_units=10,
output_shape=len(class_names)
).to(device)
next(model_1.parameters()).device
device(type='cuda', index=0)
再次设置损失函数和优化器:
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(params=model_1.parameters(),
lr=0.1)
将训练过程封装成函数:
def train_step(model: torch.nn.Module,
data_loader: torch.utils.data.DataLoader,
loss_fn: torch.nn.Module,
optimizer: torch.optim.Optimizer,
accuracy_fn,
device: torch.device = device):
train_loss, train_acc = 0, 0
model.to(device)
for batch, (X, y) in enumerate(data_loader):
# Send data to GPU
X, y = X.to(device), y.to(device)
y_pred = model(X)
loss = loss_fn(y_pred, y)
train_loss += loss
train_acc += accuracy_fn(y_true=y,
y_pred=y_pred.argmax(dim=1))
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss /= len(data_loader)
train_acc /= len(data_loader)
print(f"训练 loss: {train_loss:.5f} | 训练 accuracy: {train_acc:.2f}%")
def test_step(data_loader: torch.utils.data.DataLoader,
model: torch.nn.Module,
loss_fn: torch.nn.Module,
accuracy_fn,
device: torch.device = device):
test_loss, test_acc = 0, 0
model.to(device)
model.eval()
with torch.inference_mode():
for X, y in data_loader:
X, y = X.to(device), y.to(device)
test_pred = model(X)
test_loss += loss_fn(test_pred, y)
test_acc += accuracy_fn(y_true=y,
y_pred=test_pred.argmax(dim=1))
test_loss /= len(data_loader)
test_acc /= len(data_loader)
print(f"Test loss: {test_loss:.5f} | Test accuracy: {test_acc:.2f}%\n")
然后调用函数:
torch.manual_seed(42)
train_time_start = timer()
epochs = 3
for epoch in range(epochs):
print(f"Epoch: {epoch}\n---------")
train_step(data_loader=train_dataloader,
model=model_1,
loss_fn=loss_fn,
optimizer=optimizer,
accuracy_fn=accuracy_fn)
test_step(data_loader=test_dataloader,
model=model_1,
loss_fn=loss_fn,
accuracy_fn=accuracy_fn)
train_time_end = timer()
total_train_time_model_1 = print_train_time(start=train_time_start,
end=train_time_end,
device=device)
Epoch: 0
---------
训练 loss: 1.09199 | 训练 accuracy: 61.34%
Test loss: 0.95636 | Test accuracy: 65.00%
Epoch: 1
---------
训练 loss: 0.78101 | 训练 accuracy: 71.93%
Test loss: 0.72227 | Test accuracy: 73.91%
Epoch: 2
---------
训练 loss: 0.67027 | 训练 accuracy: 75.94%
Test loss: 0.68500 | Test accuracy: 75.02%
cuda: 40.312 seconds
注意:CUDA 与 CPU 的训练时间在很大程度上取决于您使用的 CPU/GPU 的质量。
问题:“我使用了 GPU,但我的模型没有更快地训练,为什么会这样?"
答:一个原因可能是因为你的数据集和模型都很小(就像此例子正在使用的数据集和模型一样),使用GPU的好处被传输数据所花费的时间所抵消。在将数据从 CPU 内存(默认)复制到 GPU 内存之间存在一个小瓶颈。因此,对于较小的模型和数据集,CPU 实际上可能是计算的最佳位置。对于较大的数据集和模型,GPU 可以提供的计算速度通常远远超过获取数据的成本。但是,这在很大程度上取决于您使用的硬件。
来评估一下模型:
torch.manual_seed(42)
model_1_results = eval_model(model=model_1,
data_loader=test_dataloader,
loss_fn=loss_fn,
accuracy_fn=accuracy_fn,
device=device)
model_0_results, model_1_results
({'model_name': 'FashionMNISTModelV0', 'model_loss': 0.47663894295692444, 'model_acc': 83.42651757188499}, {'model_name': 'FashionMNISTModelV1', 'model_loss': 0.6850008964538574, 'model_acc': 75.01996805111821})
在这种情况下,看起来向模型添加非线性使它的性能比基线模型更差。
从事物的外观来看,模型似乎对训练数据过度拟合。过度拟合意味着我们的模型很好地学习了训练数据,但这些模式并没有推广到测试数据。
修复过拟合的两种主要方法包括:
- 使用较小或不同的模型(某些模型比其他模型更适合某些类型的数据)。
- 使用更大的数据集(数据越多,模型学习可推广模式的机会就越大)。
卷积神经网络的典型结构:输入层 -> [卷积层 -> 激活层 -> 池化层] -> 输出层
- 其中,
[卷积层 -> 激活层 -> 池化层]
的内容可以根据要求放大和重复多次。
下表是一个很好的通用指南,可以指导使用哪种模型(尽管也有例外)。
类型 | 一般使用模型 | 示例 |
---|---|---|
结构化数据(如表格、行、列数据) | 梯度提升模型,随机森林,XGBoost | sklearn.ensemble ,XGBoost library |
非结构化数据(如图像、音频、语言) | 卷积神经网络,Transformers | torchvision.models ,HuggingFace Transformers |
使用 torch.nn
中的 nn.Conv2d()
和 nn.MaxPool2d()
层:
class FashionMNISTModelV2(nn.Module):
"""
Model architecture copying TinyVGG from:
https://poloclub.github.io/cnn-explainer/
"""
def __init__(self, input_shape: int, hidden_units: int, output_shape: int):
super().__init__()
self.block_1 = nn.Sequential(
nn.Conv2d(in_channels=input_shape,
out_channels=hidden_units,
kernel_size=3, # 卷积核大小
stride=1, # default
padding=1), # "valid"(无填充),"same"(输出与输入具有相同的形状),int表示特定的数字
nn.ReLU(),
nn.Conv2d(in_channels=hidden_units,
out_channels=hidden_units,
kernel_size=3,
stride=1,
padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,
stride=2) # 默认 stride 与池化窗口大小一致
)
self.block_2 = nn.Sequential(
nn.Conv2d(hidden_units, hidden_units, 3, padding=1),
nn.ReLU(),
nn.Conv2d(hidden_units, hidden_units, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2)
)
self.classifier = nn.Sequential(
nn.Flatten(),
# 这个in_features形状是是因为网络的每一层都会压缩和改变我们输入数据的形状。
# 此例中 28 * 28 池化一次变成 14 * 14,再池化变成 7 * 7
nn.Linear(in_features=hidden_units * 7 * 7,
out_features=output_shape)
)
def forward(self, x: torch.Tensor):
x = self.block_1(x)
x = self.block_2(x)
x = self.classifier(x)
return x
torch.manual_seed(42)
model_2 = FashionMNISTModelV2(input_shape=1,
hidden_units=10,
output_shape=len(class_names)).to(device)
model_2
FashionMNISTModelV2( (block_1): Sequential( (0): Conv2d(1, 10, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): ReLU() (2): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (3): ReLU() (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (block_2): Sequential( (0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): ReLU() (2): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (3): ReLU() (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (classifier): Sequential( (0): Flatten(start_dim=1, end_dim=-1) (1): Linear(in_features=490, out_features=10, bias=True) ) )
该笔记只讲代码,不讲原理。
设置损失函数与优化器:
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(params=model_2.parameters(),
lr=0.1)
进行训练:
torch.manual_seed(42)
train_time_start_model_2 = timer()
epochs = 3
for epoch in range(epochs):
print(f"Epoch: {epoch}\n---------")
train_step(data_loader=train_dataloader,
model=model_2,
loss_fn=loss_fn,
optimizer=optimizer,
accuracy_fn=accuracy_fn,
device=device
)
test_step(data_loader=test_dataloader,
model=model_2,
loss_fn=loss_fn,
accuracy_fn=accuracy_fn,
device=device
)
train_time_end_model_2 = timer()
total_train_time_model_2 = print_train_time(start=train_time_start_model_2,
end=train_time_end_model_2,
device=device)
Epoch: 0
---------
训练 loss: 0.59430 | 训练 accuracy: 78.44%
Test loss: 0.40483 | Test accuracy: 85.33%
Epoch: 1
---------
训练 loss: 0.36487 | 训练 accuracy: 86.81%
Test loss: 0.35013 | Test accuracy: 87.23%
Epoch: 2
---------
训练 loss: 0.32794 | 训练 accuracy: 88.12%
Test loss: 0.31522 | Test accuracy: 88.63%
cuda: 52.375 seconds
看起来效果甚好。
model_2_results = eval_model(
model=model_2,
data_loader=test_dataloader,
loss_fn=loss_fn,
accuracy_fn=accuracy_fn,
device=device
)
model_2_results
{'model_name': 'FashionMNISTModelV2', 'model_loss': 0.3152208626270294, 'model_acc': 88.62819488817891}
这里训练了三个模型:
model_0
:具有两个nn.Linear()
层的基线模型。model_1
:与基线模型相同,除了在nn.Linear()
层之间有nn.ReLU()
层。model_2
:CNN模型,模仿 CNN Explainer 网站上的 TinyVGG 架构。
使用 pandas
的 DataFrame
将数据展示:
import pandas as pd
compare_results = pd.DataFrame([model_0_results, model_1_results, model_2_results])
compare_results["training_time"] = [total_train_time_model_0,
total_train_time_model_1,
total_train_time_model_2]
compare_results
model_name | model_loss | model_acc | training_time | |
---|---|---|---|---|
0 | FashionMNISTModelV0 | 0.476639 | 83.426518 | 37.967943 |
1 | FashionMNISTModelV1 | 0.685001 | 75.019968 | 40.312245 |
2 | FashionMNISTModelV2 | 0.315221 | 88.628195 | 52.375387 |
结论:
CNN(FashionMNISTModelV2)模型表现最好(损失最低,准确率最高),但训练时间最长。
基线模型(FashionMNISTModelV0)的性能优于 model_1(FashionMNISTModelV1)。
比较可视化:
plt.figure(figsize=(4, 4))
compare_results.set_index("model_name")["model_acc"].plot(kind="barh")
plt.xlabel("accuracy (%)")
plt.ylabel("model")
Text(0, 0.5, 'model')
可以使用许多不同的评估指标来解决分类问题,其中最直观的是混淆矩阵。
混淆矩阵显示了分类模型在预测和真实标签之间混淆的地方。
制作混淆矩阵:
- 使用训练模型进行预测(混淆矩阵将预测与真实标签进行比较)。
- 使用
torchmetrics.ConfusionMatrix
制作混淆矩阵。 - 使用
mlxtend.plotting.plot_confusion_matrix()
绘制混淆矩阵。
# 1. 使用训练模型进行预测
y_preds = []
model_2.eval()
with torch.inference_mode():
for X, y in test_dataloader:
X, y = X.to(device), y.to(device)
y_logit = model_2(X)
y_pred = torch.softmax(y_logit, dim=1).argmax(dim=1)
y_preds.append(y_pred.cpu())
y_pred_tensor = torch.cat(y_preds)
下载并导入 torchmetrics
和 mlxtend
:
from torchmetrics import ConfusionMatrix
from mlxtend.plotting import plot_confusion_matrix
# 2. 设置混淆矩阵实例并将预测与目标进行比较
confmat = ConfusionMatrix(num_classes=len(class_names), task='multiclass')
confmat_tensor = confmat(preds=y_pred_tensor,
target=test_data.targets)
# 3. 绘图
fig, ax = plot_confusion_matrix(
conf_mat=confmat_tensor.numpy(), # matplotlib 就像 NumPy 一样
class_names=class_names, # 将行和列标签转换为类名
figsize=(4, 4)
)
可以看到模型表现相当好,因为大多数黑色方块对角线(理想模型将只在这些方块中有值,其他地方都是0)。
混淆矩阵的信息能够进一步检查模型和数据,看看如何改进。