基于resnet的鸟类图片分类实验(pytorch版本)
- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
Ⅰ Ⅰ Ⅰ Introduction:
- 本文为机器学习使用resnet实现鸟类图片分类的实验,素材来自网络。
- 学习目标:
- 学习和理解resnet基本原理
- 基于tensorflow代码写出pytroch版本并跑通
Ⅱ Ⅱ Ⅱ Experiment:
-
数据准备与任务分析:
数据通过网络下载完成
resnet介绍:
一、ResNet的介绍 -
ResNet的背景与发展
ResNet(Residual Network)是由微软研究院提出的深度卷积神经网络,在2015年的ImageNet竞赛中以显著的优势赢得了图像分类、检测和定位任务的第一名。ResNet的提出解决了深层神经网络中存在的梯度消失和梯度爆炸问题,使得构建非常深的神经网络成为可能,网络的深度从传统的几十层增加到了上百层甚至更深。 -
ResNet的核心思想:残差学习
传统的深度神经网络随着层数的加深,容易出现模型的退化问题,即随着网络层数的增加,模型的性能不仅没有提升,反而可能下降。ResNet的核心创新在于引入了残差学习的概念。具体来说,它通过引入“残差块”(Residual Block),使得网络的某一层不仅学习当前层的输出,还通过跳跃连接(shortcut connection)直接将输入传递给后面的层,这种直接连接被称为“跳跃连接”或“捷径”。
即使某一层的输出值很小(表示这层没有学到什么新的特征),整个残差块的输出也可以依靠直接的跳跃连接保持较好的表现。这种机制大大缓解了深度网络中的梯度消失问题,使得网络能够更深。 -
ResNet50的架构
ResNet家族有不同的变种,如ResNet18、ResNet34、ResNet50、ResNet101等,其中数字表示网络层数。以ResNet50为例,整个网络由一个卷积层和4个残差块(每个块由多个残差单元构成)组成。每个残差单元中,主要的操作包括1×1卷积、3×3卷积和另一个1×1卷积,这种设计可以同时减少计算量和保持特征的丰富性。
在ResNet50中,有以下几个主要部分:
初始卷积层:7×7卷积核的卷积层,紧接着是批量归一化(Batch Normalization)和最大池化层(Max Pooling)。
四个残差块:每个残差块内包含若干个残差单元,其中包含卷积、批量归一化和ReLU激活函数等。
全局平均池化层:将特征图通过平均池化层缩小到1×1的尺寸。
全连接层:最后通过一个全连接层进行分类。
这种结构能够有效地提取图像特征,且由于残差块的引入,使得网络可以堆叠更多层而不会出现显著的退化问题。
二、
-
实验目的
本实验的主要目的是通过构建和训练ResNet50模型来对特定的数据集(如鸟类图片数据集)进行分类。通过使用PyTorch框架,实验展示了如何在实际应用中加载数据、预处理数据、构建ResNet模型、训练和测试模型,并最终评估模型的性能。 -
实验步骤
数据加载与预处理:首先从指定的数据文件夹中加载图像数据,并进行必要的预处理,如调整图像尺寸、归一化等。数据集被划分为训练集和测试集,以80%的数据用于训练,20%用于测试。
ResNet50模型构建:实验中自定义了一个ResNet50模型,其中包括了基础的卷积层、残差块、池化层和全连接层。自定义的ResNet50模型结构和原始ResNet50一致。
训练过程:使用交叉熵损失函数和Adam优化器对模型进行训练。模型训练的过程中,会跟踪并记录训练集和测试集的准确率和损失值。每个训练周期(epoch)结束后,模型会在测试集上进行评估,并保存最佳的模型。
结果分析与可视化:训练完成后,通过绘制训练和测试集的损失曲线和准确率曲线来分析模型的表现。可以从曲线中观察到模型的收敛情况,以及是否存在过拟合或欠拟合现象。
- 配置环境:
语言环境:python 3.8
编译器: pycharm
深度学习环境:
torch2.11
cuda12.1
torchvision0.15.2a0
导入一切需要的包:
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision
from torchvision import transforms, datasets
import os, PIL, pathlib, warnings
import torch.nn.functional as F
import matplotlib.pyplot as plt
import pandas as pd
from torchvision.io import read_image
from torch.utils.data import Dataset
import torch.utils.data as data
from PIL import Image
import copy
import numpy as np
查看数据:
def count_images(folder):
"""递归计算文件夹中的图像数量。"""
count = 0
for item in folder.iterdir():
if item.is_file():
count += 1
if item.is_dir():
count += count_images(item)
return count
def load_data(data_dir, train_transforms):
"""加载图像数据并进行训练集和测试集的划分。"""
total_data = datasets.ImageFolder(data_dir, transform=train_transforms)
print(total_data)
print(total_data.class_to_idx)
train_size = int(0.8 * len(total_data))
test_size = len(total_data) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(total_data, [train_size, test_size])
print(train_dataset, test_dataset)
return train_dataset, test_dataset, total_data
def create_data_loaders(train_dataset, test_dataset, batch_size):
"""创建训练和测试数据加载器。"""
train_dl = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=0)
test_dl = torch.utils.data.DataLoader(test_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=0)
return train_dl, test_dl
def visualize_sample_images(image_folder):
"""从指定的文件夹中显示一些示例图像。"""
image_files = [f for f in os.listdir(image_folder) if f.endswith((".jpg", ".png", ".jpeg"))]
fig, axes = plt.subplots(2, 4, figsize=(16, 6))
for ax, img_file in zip(axes.flat, image_files):
img_path = os.path.join(image_folder, img_file)
img = Image.open(img_path)
ax.imshow(img)
ax.axis('off')
plt.tight_layout()
plt.show()
- 构建网络:
为了提高模型性能,选择输入为3通道,经过4层卷积2层池化以及两层全连接输出最终结果,同时训练中加入BN与dropout方法。
class ResNetblock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(ResNetblock, self).__init__()
self.blockconv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride),
nn.BatchNorm2d(out_channels),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels * 4, kernel_size=1, stride=1),
nn.BatchNorm2d(out_channels * 4)
)
if stride != 1 or in_channels != out_channels * 4:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels * 4, kernel_size=1, stride=stride),
nn.BatchNorm2d(out_channels * 4)
)
def forward(self, x):
residual = x
out = self.blockconv(x)
if hasattr(self, 'shortcut'):
residual = self.shortcut(x)
out += residual
out = F.relu(out)
return out
class ResNet50(nn.Module):
def __init__(self, block, num_classes=1000):
super(ResNet50, self).__init__()
self.conv1 = nn.Sequential(
nn.ZeroPad2d(3),
nn.Conv2d(3, 64, kernel_size=7, stride=2),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d((3, 3), stride=2)
)
self.in_channels = 64
self.layer1 = self.make_layer(ResNetblock, 64, 3, stride=1)
self.layer2 = self.make_layer(ResNetblock, 128, 4, stride=2)
self.layer3 = self.make_layer(ResNetblock, 256, 6, stride=2)
self.layer4 = self.make_layer(ResNetblock, 512, 3, stride=2)
self.avgpool = nn.AvgPool2d((7, 7))
self.fc = nn.Linear(512 * 4, num_classes)
def make_layer(self, block, channels, num_blocks, stride=1):
"""创建ResNet层,每一层包含多个残差块。"""
strides = [stride] + [1] * (num_blocks - 1)
layers = []
for stride in strides:
layers.append(block(self.in_channels, channels, stride))
self.in_channels = channels * 4
return nn.Sequential(*layers)
def forward(self, x):
"""定义前向传播。"""
out = self.conv1(x)
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.avgpool(out)
out = out.view(out.size(0), -1)
out = self.fc(out)
return out
- 训练模型:
模型的损失函数选用交叉熵,通过以下代码对模型进行更新:
def train(dataloader, model, optimizer, loss_fn, device):
"""训练模型的一个epoch。"""
size = len(dataloader.dataset)
num_batches = len(dataloader)
train_acc, train_loss = 0, 0
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model(X)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
train_loss /= num_batches
train_acc /= size
return train_acc, train_loss
- 测试模型:
通过以下代码完成评估:
def test(dataloader, model, loss_fn, device):
"""测试模型的性能。"""
size = len(dataloader.dataset)
num_batches = len(dataloader)
test_loss, test_acc = 0, 0
with torch.no_grad():
for imgs, target in dataloader:
imgs, target = imgs.to(device), target.to(device)
target_pred = model(imgs)
loss = loss_fn(target_pred, target)
test_loss += loss.item()
test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()
test_acc /= size
test_loss /= num_batches
return test_acc, test_loss
- 实验结果及可视化:
主函数训练代码即绘制图像执行如下:
if __name__ == "__main__":
# 设置设备
device = set_device()
# 配置matplotlib
configure_plot()
# 数据路径
data_dir = 'bird_photos'
data_dir = pathlib.Path(data_dir)
# 统计图片数量
image_count = count_images(data_dir)
print("图片总数为:", image_count)
# 获取类别名称
data_paths = list(data_dir.glob('*'))
classNames = [str(path).split('/')[-1] for path in data_paths]
print("类别名称:", classNames)
# 数据预处理
train_transforms = transforms.Compose([
transforms.Resize([224, 224]),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
test_transform = transforms.Compose([
transforms.Resize([224, 224]),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# 加载数据
train_dataset, test_dataset, total_data = load_data(data_dir, train_transforms)
# 创建数据加载器
batch_size = 8
train_dl, test_dl = create_data_loaders(train_dataset, test_dataset, batch_size)
# 可视化部分图片
visualize_sample_images(‘bird_photos/Black Skimmer/')
# 定义ResNet模型
model = ResNet50(block=ResNetblock, num_classes=len(classNames)).to(device)
print(model)
# 设置损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
learn_rate = 1e-3
opt = torch.optim.Adam(model.parameters(), lr=learn_rate)
# 训练模型
epochs = 20
train_loss, train_acc, test_loss, test_acc = [], [], [], []
best_acc = 0
for epoch in range(epochs):
model.train()
epoch_train_acc, epoch_train_loss = train(train_dl, model, opt, loss_fn, device)
model.eval()
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn, device)
if epoch_test_acc > best_acc:
best_acc = epoch_test_acc
best_model = copy.deepcopy(model)
train_acc.append(epoch_train_acc)
train_loss.append(epoch_train_loss)
test_acc.append(epoch_test_acc)
test_loss.append(epoch_test_loss)
lr = opt.state_dict()['param_groups'][0]['lr']
template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}, Lr:{:.2E}')
print(template.format(epoch + 1, epoch_train_acc * 100, epoch_train_loss,
epoch_test_acc * 100, epoch_test_loss, lr))
# 绘制结果
plot_results(epochs, train_acc, test_acc, train_loss, test_loss)
print('训练完成')
Ⅲ Ⅲ Ⅲ Conclusion:
本实验成功实现了一个基于ResNet50的图像分类任务。实验结果表明,残差网络在分类任务中具有良好的表现,能够较好地学习和分类复杂的图像数据。在实验中,模型在训练过程中逐渐收敛,并且在测试集上也表现出较高的准确率,这验证了ResNet的有效性。
然而,实验中也可能遇到一些挑战,如数据不均衡、训练时间较长等问题。对于这些问题,可以考虑在后续实验中使用更多的数据增强技术、更复杂的模型结构或者调节超参数(如学习率、批大小等)来进一步优化模型的性能。此外,还可以通过迁移学习或微调预训练模型等方法,进一步提升模型的分类效果。