5.1 计算机视觉 (Computer Vision - CV) 第五章:PyTorch 实战应用领域 - 5.1 计算机视觉 (Computer Vision - CV) 计算机视觉 (Computer Vision, CV) 是一门致力于使计算机能够“看”和“理解”世界的学科。它借鉴了计算机科学、人工智能、图像处理、模式识别等多个领域的知识,旨在模拟人类视觉系统,让机器能够从图像或视频中提取、分析并理解信息,最终完成各种视觉相关的任务。在人工智能和深度学习的浪潮下,计算机视觉迎来了前所未有的发展机遇,并在各个行业领域展现出巨大的应用潜力。 5.1.1 计算机视觉的核心任务 计算机视觉的任务多种多样,可以根据不同的角度进行分类。
计算机视觉 (Computer Vision, CV) 是一门致力于使计算机能够“看”和“理解”世界的学科。它借鉴了计算机科学、人工智能、图像处理、模式识别等多个领域的知识,旨在模拟人类视觉系统,让机器能够从图像或视频中提取、分析并理解信息,最终完成各种视觉相关的任务。在人工智能和深度学习的浪潮下,计算机视觉迎来了前所未有的发展机遇,并在各个行业领域展现出巨大的应用潜力。
计算机视觉的任务多种多样,可以根据不同的角度进行分类。从宏观层面来看,计算机视觉的核心任务可以归纳为以下几个方面:
图像分类 (Image Classification):给定一张图像,模型需要判断图像中包含的物体类别。例如,识别图像是猫、狗还是鸟。这是计算机视觉最基础的任务之一,也是许多更复杂任务的基础。
目标检测 (Object Detection):不仅要识别图像中的物体类别,还要定位物体在图像中的位置,通常使用 bounding box(边界框)来表示。例如,在一张图像中检测出所有的汽车、行人,并用矩形框标出它们的位置。
语义分割 (Semantic Segmentation):将图像中的每个像素都划分到对应的类别,实现像素级别的分类。例如,将图像中的道路、天空、树木等区域分别标记出来,形成像素级别的语义标签图。
实例分割 (Instance Segmentation):在语义分割的基础上,区分同一类别中的不同实例。例如,在一张图像中,即使有多个人,实例分割也要将每个人都独立分割出来,区分彼此。
图像生成 (Image Generation):利用模型生成新的图像,例如根据文本描述生成图像,或者生成风格迁移后的图像。
图像风格迁移 (Image Style Transfer):将一张图像的内容与另一张图像的风格相结合,生成具有新风格的图像。
图像恢复 (Image Restoration):恢复受损或退化的图像,例如去噪、去模糊、图像超分辨率等。
视频分析 (Video Analysis):对视频序列进行理解和分析,例如视频分类、动作识别、视频目标跟踪等。
这些任务之间并非完全独立,很多时候它们会相互关联,甚至可以组合起来解决更复杂的视觉问题。例如,自动驾驶系统就需要同时进行目标检测、语义分割、路径规划等多个任务。
PyTorch 之所以成为计算机视觉领域的热门选择,得益于其以下几个显著的优势:
动态计算图 (Dynamic Computation Graph):PyTorch 使用动态计算图,这使得模型构建和调试更加灵活和直观。尤其在处理复杂或变长的模型结构时,动态图的优势更加明显。这对于研究人员快速迭代和实验新的 CV 模型架构非常有帮助。
Pythonic 风格和易用性:PyTorch 深度融合了 Python 语言的特性,API 设计简洁明了,易于学习和使用。对于熟悉 Python 的开发者来说,上手 PyTorch 非常快速。
强大的社区支持和丰富的工具库:PyTorch 拥有庞大而活跃的社区,提供了大量的教程、文档和预训练模型。torchvision 作为 PyTorch 官方提供的计算机视觉工具库,集成了常用的数据集、模型架构、图像变换等模块,极大地简化了 CV 任务的开发流程。
高性能和硬件加速:PyTorch 底层使用 C++ 实现,保证了运算效率。同时,PyTorch 能够很好地利用 GPU 进行加速计算,大幅提升模型训练和推理的速度,这对于计算密集型的计算机视觉任务至关重要。
易于部署和扩展:PyTorch 模型可以方便地导出为 ONNX 等通用格式,方便在各种平台和设备上部署。同时,PyTorch 的模块化设计也使得模型扩展和定制变得更加容易。
接下来,我们将通过具体的代码示例,详细讲解如何使用 PyTorch 实现几种常见的计算机视觉任务。
图像分类是 CV 的基础任务,我们的目标是训练一个模型,能够正确识别图像中的物体类别。我们将使用经典的 CIFAR-10 数据集,并构建一个简单的卷积神经网络 (CNN) 进行分类。
1. 数据准备 (Data Preparation)
首先,我们需要加载 CIFAR-10 数据集,并进行预处理。PyTorch 的 torchvision.datasets 模块提供了 CIFAR-10 数据集的接口,torchvision.transforms 模块提供了常用的图像变换方法。
import torch import torchvision import torchvision.transforms as transforms import torch.nn as nn import torch.nn.functional as F import torch.optim as optim # 数据预处理 transform = transforms.Compose( [transforms.ToTensor(), # 将 PIL Image 或 numpy.ndarray 转换为 tensor,并将像素值缩放到 [0, 1] transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) # 标准化,将像素值缩放到 [-1, 1] # 加载训练集 trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2) # 加载测试集 testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2) classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') # 可视化部分训练数据 (可选) import matplotlib.pyplot as plt import numpy as np def imshow(img): img = img / 2 + 0.5 # 反标准化 npimg = img.numpy() plt.imshow(np.transpose(npimg, (1, 2, 0))) # 调整维度顺序,从 (C, H, W) 变为 (H, W, C) plt.show() # 获取一些随机的训练图像 dataiter = iter(trainloader) images, labels = next(dataiter) # 显示图像 imshow(torchvision.utils.make_grid(images)) # 打印 labels print(' '.join(f'{classes[labels[j]]:5s}' for j in range(4)))
代码详解:
transforms.Compose:组合多个图像变换操作。
transforms.ToTensor():将图像数据转换为 PyTorch Tensor,并将像素值缩放到 [0, 1] 范围。
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)): 对图像进行标准化,使得每个通道的像素值均值为 0.5,标准差为 0.5,将像素值范围缩放到 [-1, 1]。标准化有助于模型更快地收敛。
torchvision.datasets.CIFAR10: 加载 CIFAR-10 数据集,root 指定数据存储路径,train=True/False 指定加载训练集或测试集,download=True 表示如果数据不存在则自动下载。
torch.utils.data.DataLoader: 数据加载器,用于批量加载数据,batch_size 指定批量大小,shuffle=True 表示在每个 epoch 开始时打乱数据顺序,num_workers 指定数据加载时使用的子进程数量。
imshow 函数用于可视化图像,首先进行反标准化,然后调整 Tensor 的维度顺序,最后使用 matplotlib.pyplot.imshow 显示图像。
2. 定义 CNN 模型 (Define a CNN)
接下来,我们定义一个简单的 CNN 模型,用于图像分类。
class Net(nn.Module): def __init__(self): super().__init__() # 卷积层,输入通道 3 (RGB),输出通道 6,卷积核 5x5 self.conv1 = nn.Conv2d(3, 6, 5) # 最大池化层,池化窗口 2x2 self.pool = nn.MaxPool2d(2, 2) # 卷积层,输入通道 6,输出通道 16,卷积核 5x5 self.conv2 = nn.Conv2d(6, 16, 5) # 全连接层,输入特征 16 * 5 * 5 (经过两次池化后图像尺寸变为 5x5),输出特征 120 self.fc1 = nn.Linear(16 * 5 * 5, 120) # 全连接层,输入特征 120,输出特征 84 self.fc2 = nn.Linear(120, 84) # 全连接层,输入特征 84,输出特征 10 (CIFAR-10 数据集类别数) self.fc3 = nn.Linear(84, 10) def forward(self, x): # 卷积 -> ReLU 激活 -> 池化 x = self.pool(F.relu(self.conv1(x))) # 卷积 -> ReLU 激活 -> 池化 x = self.pool(F.relu(self.conv2(x))) # 将多维特征图展平为一维向量 x = torch.flatten(x, 1) # flatten all dimensions except batch # 全连接 -> ReLU 激活 x = F.relu(self.fc1(x)) # 全连接 -> ReLU 激活 x = F.relu(self.fc2(x)) # 全连接 (输出层,无需激活函数,因为后面会使用 CrossEntropyLoss) x = self.fc3(x) return x net = Net()
代码详解:
nn.Module: 所有神经网络模块的基类。我们的 Net 类继承自 nn.Module。
nn.Conv2d(in_channels, out_channels, kernel_size): 二维卷积层,in_channels 输入通道数,out_channels 输出通道数 (卷积核数量),kernel_size 卷积核大小。
nn.MaxPool2d(kernel_size, stride): 最大池化层,kernel_size 池化窗口大小,stride 步长。
nn.Linear(in_features, out_features): 全连接层,in_features 输入特征数,out_features 输出特征数。
F.relu(x): ReLU 激活函数。
F.max_pool2d(x, kernel_size, stride): 最大池化操作 (函数式接口)。
torch.flatten(x, start_dim=1): 将 Tensor 从 start_dim 维度开始展平为一维向量。
forward(x) 方法定义了模型的前向传播过程,即数据在网络中的流动路径。
模型结构图 (Mermaid Graph TD):
3. 定义损失函数和优化器 (Define a Loss function and optimizer)
我们使用交叉熵损失函数 (nn.CrossEntropyLoss) 和随机梯度下降优化器 (optim.SGD)。
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数,适用于多分类问题 optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) # SGD 优化器,lr 学习率,momentum 动量
代码详解:
nn.CrossEntropyLoss(): 交叉熵损失函数,它将 nn.LogSoftmax 和 nn.NLLLoss 结合在一起。适用于多分类问题,输入是模型的原始输出 (logits),目标是类别索引。
optim.SGD(net.parameters(), lr=0.001, momentum=0.9): 随机梯度下降优化器,net.parameters() 指定要优化的模型参数,lr 学习率,momentum 动量参数,用于加速 SGD 的收敛。
4. 训练模型 (Train the network)
for epoch in range(2): # loop over the dataset multiple times running_loss = 0.0 for i, data in enumerate(trainloader, 0): # 获取输入数据;data 是 [inputs, labels] 列表 inputs, labels = data # 梯度清零 optimizer.zero_grad() # 前向传播 + 反向传播 + 优化 outputs = net(inputs) # 前向传播,得到模型输出 loss = criterion(outputs, labels) # 计算损失 loss.backward() # 反向传播,计算梯度 optimizer.step() # 更新模型参数 # 打印统计信息 running_loss += loss.item() if i % 2000 == 1999: # 每 2000 个 mini-batches 打印一次 print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}') running_loss = 0.0 print('Finished Training')
代码详解:
训练循环遍历数据集若干次 (epochs)。
在每个 epoch 内,遍历数据加载器 trainloader,获取一个 batch 的数据 (inputs, labels)。
optimizer.zero_grad(): 在每次迭代前,需要将优化器的梯度清零,因为 PyTorch 默认会累积梯度。
outputs = net(inputs): 进行前向传播,得到模型的预测输出。
loss = criterion(outputs, labels): 计算模型输出与真实标签之间的损失。
loss.backward(): 进行反向传播,计算损失函数关于模型参数的梯度。
optimizer.step(): 根据计算出的梯度,更新模型参数。
每 2000 个 mini-batches 打印一次平均损失值,用于监控训练过程。
5. 在测试集上测试模型 (Test the network on the test data)
训练完成后,我们需要在测试集上评估模型的性能。
correct = 0 total = 0 # 关闭梯度计算,测试阶段不需要梯度 with torch.no_grad(): for data in testloader: images, labels = data # 前向传播得到模型输出 outputs = net(images) # 预测类别是概率最高的那个类别 _, predicted = torch.max(outputs.data, 1) total += labels.size(0) # 累加样本总数 correct += (predicted == labels).sum().item() # 累加预测正确的样本数 print(f'Accuracy of the network on the 10000 test images: {100 * correct // total} %')
代码详解:
torch.no_grad(): 在测试阶段,我们不需要计算梯度,使用 torch.no_grad() 上下文管理器可以关闭梯度计算,减少内存消耗并加速推理。
torch.max(outputs.data, 1): torch.max 函数返回每行 (每个样本) 的最大值和最大值对应的索引。outputs.data 是模型的输出 logits,1 表示在维度 1 (类别维度) 上取最大值。我们只关心最大值对应的索引,即预测的类别 predicted。
(predicted == labels).sum().item(): 比较预测类别 predicted 和真实标签 labels,得到一个布尔 Tensor,然后使用 sum() 统计 True 的数量,即预测正确的样本数,最后使用 .item() 将 Tensor 转换为 Python 数值。
计算并打印在测试集上的准确率。
我们还可以查看模型在每个类别上的表现:
correct_pred = {classname: 0 for classname in classes} total_pred = {classname: 0 for classname in classes} with torch.no_grad(): for data in testloader: images, labels = data outputs = net(images) _, predictions = torch.max(outputs, 1) for label, prediction in zip(labels, predictions): if label == prediction: correct_pred[classes[label]] += 1 total_pred[classes[label]] += 1 for classname, correct_count in correct_pred.items(): accuracy = 100 * float(correct_count) / total_pred[classname] print(f'Accuracy for class: {classname:5s} is {accuracy:.2f} %')
代码详解:
使用字典 correct_pred 和 total_pred 分别记录每个类别的预测正确数量和总数量。
遍历测试集,对于每个 batch 的预测结果,遍历每个样本,如果预测正确,则更新对应类别的 correct_pred 和 total_pred 计数器。
最后,遍历所有类别,计算并打印每个类别的准确率。
至此,我们完成了一个简单的图像分类任务的 PyTorch 代码实践,涵盖了数据准备、模型定义、损失函数和优化器选择、模型训练以及测试评估等关键步骤。
目标检测任务旨在识别图像中多个物体的类别,并定位它们的位置。PyTorch 提供了 torchvision.models.detection 模块,其中包含了预训练的目标检测模型,例如 Faster R-CNN, Mask R-CNN 等。
这里我们以预训练的 Faster R-CNN 模型为例,演示如何进行目标检测。
1. 加载预训练模型 (Load Pre-trained Model)
import torchvision.models.detection as detection from PIL import Image # 加载预训练的 Faster R-CNN 模型 (在 COCO 数据集上预训练) model = detection.fasterrcnn_resnet50_fpn(pretrained=True) # 将模型设置为评估模式 model.eval()
代码详解:
torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=True): 加载预训练的 Faster R-CNN 模型,pretrained=True 表示加载在 COCO 数据集上预训练的权重。ResNet-50-FPN 是 Faster R-CNN 的骨干网络结构。
model.eval(): 将模型设置为评估模式。这会关闭 Dropout 和 Batch Normalization 等在训练阶段启用的层,确保模型在推理时使用预训练的参数。
模型结构图 (Faster R-CNN, 简化版 Mermaid Graph TD):
2. 图像预处理 (Image Preprocessing)
# 加载一张测试图像 image_path = './data/dog.jpg' # 请替换为实际的图像路径 input_image = Image.open(image_path) # 图像预处理,转换为 Tensor 并进行标准化 transform = transforms.Compose([ transforms.ToTensor() ]) input_tensor = transform(input_image) # 添加 batch 维度,模型期望输入是 (B, C, H, W) 格式 input_batch = input_tensor.unsqueeze(0) # 如果有 GPU,将模型和输入数据移动到 GPU if torch.cuda.is_available(): model.cuda() input_batch = input_batch.to('cuda')
代码详解:
加载测试图像,使用 PIL.Image.open() 打开图像。
使用 transforms.ToTensor() 将图像转换为 Tensor。
input_tensor.unsqueeze(0): 添加 batch 维度,将 Tensor 的形状从 (C, H, W) 变为 (1, C, H, W)。
如果 GPU 可用,使用 model.cuda() 将模型移动到 GPU,使用 input_batch.to('cuda') 将输入数据移动到 GPU。
3. 模型推理 (Model Inference)
with torch.no_grad(): # 前向传播,得到模型输出 output = model(input_batch) # output 是一个包含 boxes, labels, scores 的字典 print(output)
代码详解:
使用 torch.no_grad() 关闭梯度计算。
output = model(input_batch): 进行前向传播,得到模型的输出。
output 是一个字典,包含以下键:
'boxes': 检测到的 bounding boxes,形状为 (N, 4),N 是检测到的物体数量,4 表示 (x1, y1, x2, y2) 坐标。
'labels': 检测到的物体的类别标签,形状为 (N,),每个元素是类别索引。
'scores': 检测到的物体的置信度分数,形状为 (N,),范围在 [0, 1]。
4. 后处理和可视化 (Post-processing and Visualization)
# 将输出移动到 CPU (如果之前在 GPU 上) output = [{k: v.to('cpu') for k, v in t.items()} for t in output] # 获取 bounding boxes, labels, scores boxes = output[0]['boxes'] labels = output[0]['labels'] scores = output[0]['scores'] # 加载 COCO 类别名称 COCO_INSTANCE_CATEGORY_NAMES = [ '__background__', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'N/A', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'N/A', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'N/A', 'dining table', 'N/A', 'toilet', 'N/A', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'N/A', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush' ] # 绘制 bounding boxes 和 labels import matplotlib.pyplot as plt import matplotlib.patches as patches plt.figure(figsize=(10, 10)) plt.imshow(input_image) ax = plt.gca() # 获取当前 axes 对象 for box, label, score in zip(boxes, labels, scores): if score > 0.5: # 只显示置信度大于 0.5 的检测结果 box = box.tolist() # 将 Tensor 转换为 list label_name = COCO_INSTANCE_CATEGORY_NAMES[label] # 创建矩形框 rect = patches.Rectangle((box[0], box[1]), box[2] - box[0], box[3] - box[1], linewidth=1, edgecolor='r', facecolor='none') # 添加矩形框到 axes ax.add_patch(rect) # 添加类别标签和置信度分数 ax.text(box[0], box[1] - 10, f'{label_name} {score:.2f}', bbox=dict(facecolor='yellow', alpha=0.5), fontsize=8) plt.axis('off') # 关闭坐标轴 plt.show()
代码详解:
将模型输出移动到 CPU,方便后续处理。
从 output 中提取 bounding boxes, labels, scores。
加载 COCO 数据集的类别名称列表 COCO_INSTANCE_CATEGORY_NAMES。
使用 matplotlib.pyplot 和 matplotlib.patches 绘制 bounding boxes 和 labels。
只显示置信度分数大于 0.5 的检测结果。
在图像上绘制矩形框,并在框上方添加类别标签和置信度分数。
plt.axis('off') 关闭坐标轴,使图像显示更简洁。
通过以上代码,我们成功使用预训练的 Faster R-CNN 模型对图像进行了目标检测,并可视化了检测结果。
语义分割任务是将图像中的每个像素都划分到对应的类别。PyTorch 同样提供了 torchvision.models.segmentation 模块,其中包含了预训练的语义分割模型,例如 FCN, DeepLabV3, Mask R-CNN (带分割分支) 等。
这里我们以预训练的 FCN-ResNet50 模型为例,演示如何进行语义分割。
1. 加载预训练模型 (Load Pre-trained Model)
import torchvision.models.segmentation as segmentation # 加载预训练的 FCN-ResNet50 模型 (在 Pascal VOC 数据集上预训练) model = segmentation.fcn_resnet50(pretrained=True) # 将模型设置为评估模式 model.eval()
代码详解:
torchvision.models.segmentation.fcn_resnet50(pretrained=True): 加载预训练的 FCN-ResNet50 模型,pretrained=True 表示加载在 Pascal VOC 数据集上预训练的权重。ResNet-50 是 FCN 的骨干网络结构。
model.eval(): 将模型设置为评估模式。
模型结构图 (FCN, 简化版 Mermaid Graph TD):
2. 图像预处理 (Image Preprocessing)
# 加载一张测试图像 image_path = './data/city.jpg' # 请替换为实际的图像路径 input_image = Image.open(image_path) # 图像预处理,转换为 Tensor 并进行标准化 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # ImageNet 标准化参数 ]) input_tensor = transform(input_image) # 添加 batch 维度 input_batch = input_tensor.unsqueeze(0) # 如果有 GPU,将模型和输入数据移动到 GPU if torch.cuda.is_available(): model.cuda() input_batch = input_batch.to('cuda')
代码详解: