基于CNN的AnimalTriClassifier
关于项目实现的文档说明书,三个要素:数据、模型、训练
1、项目简介
关于项目的基本介绍。
本项目实现的是对猫科动物的划分,划分的物种有猫、狗、野生三种分类,属于小颗粒度分类
- 大颗粒度分类:以物种作为分类,比如飞机、青蛙、狗、猫、马、鹿等。
- 实体颗粒度分类:具体到具体的人,比如指纹识别、人脸识别等具体的个体,具体的实体
1.1 项目名称
基于CNN的AnimalTriClassifie
1.2 项目简介
本项目旨在使用卷积神经网络(CNN)进行图像分类任务。我们将使用 LeNet5(衍生) 模型来训练一个可以区分猫、狗和野生动物的分类器。项目中包括了数据预处理、模型训练、测试、验证以及单张图片推理等功能。
2、数据
公开的数据集
2.1 公开数据集
2.3 数据增强
提升模型的泛化能力和鲁棒性。
# 数据预处理和加载
transform = transforms.Compose([
# transforms.RandomVerticalFlip(),
# transforms.RandomRotation(degrees=(0, 180)),
# transforms.RandomHorizontalFlip(), # 随机水平翻转
# transforms.RandomRotation(10),
# transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
transforms.ToTensor(),
transforms.Resize((64, 64)),
transforms.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225]) ,
transforms.RandomRotation(degrees=(0, 180)),
transforms.RandomInvert(), # 随机反转变换,
transforms.RandomAffine(degrees=(30, 70), translate=(0.1, 0.3), scale=(0.5, 0.75)),
])
3. 神经网络
手写LeNets5
import torch
import torch.nn as nn
import torch.nn.functional as F
class LeNet5(nn.Module):
def __init__(self, num_classes=3):
super(LeNet5, self).__init__()
# 第一层卷积层,输入通道为3,输出通道为16,卷积核大小为5x5,步幅为1,填充为2
self.layer1 = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=5, stride=1, padding=2), # 输出大小: (64 + 2*2 - 5)/1 + 1 = 64
nn.ReLU(), # 使用 ReLU 激活函数
nn.AvgPool2d(kernel_size=2, stride=2) # 输出大小: 64 / 2 = 32
)
# 第二层卷积层,输入通道为16,输出通道为32,卷积核大小为5x5,步幅为1,填充为2
self.layer2 = nn.Sequential(
nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2), # 输出大小: (32 + 2*2 - 5)/1 + 1 = 32
nn.ReLU(), # 使用 ReLU 激活函数
nn.AvgPool2d(kernel_size=2, stride=2) # 输出大小: 32 / 2 = 16
)
# 第三层卷积层,输入通道为32,输出通道为64,卷积核大小为5x5,步幅为1,填充为2
self.layer3 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=5, stride=1, padding=2), # 输出大小: (16 + 2*2 - 5)/1 + 1 = 16
nn.ReLU(), # 使用 ReLU 激活函数
nn.AvgPool2d(kernel_size=2, stride=2) # 输出大小: 16 / 2 = 8
)
# 全连接层
self.fc1 = nn.Linear(64 * 8 * 8, 120) # 输入大小: 64 * 8 * 8
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, num_classes)
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = self.layer3(out)
out = out.reshape(out.size(0), -1) # 展平
out = F.relu(self.fc1(out)) # 使用 ReLU 激活函数
out = F.relu(self.fc2(out)) # 使用 ReLU 激活函数
out = self.fc3(out)
return out
# 创建模型实例
model = LeNet5(num_classes=3)
4. 模型训练
def train():
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 数据预处理和加载
transform = transforms.Compose([
transforms.RandomVerticalFlip(),
# transforms.RandomRotation(degrees=(0, 180)),
# transforms.RandomHorizontalFlip(), # 随机水平翻转
# transforms.RandomRotation(10),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
transforms.ToTensor(),
transforms.Resize((64, 64)),
transforms.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225]) ,
transforms.RandomRotation(degrees=(0, 180)),
transforms.RandomInvert(), # 随机反转变换,
transforms.RandomAffine(degrees=(30, 70), translate=(0.1, 0.3), scale=(0.5, 0.75)),
])
# 检查数据集路径是否存在
train_path = os.path.join(data_path, 'train')
if not os.path.exists(train_path):
raise FileNotFoundError(f"数据集路径不存在: {
train_path}")
# 加载整个数据集
full_dataset = ImageFolder(root=train_path, transform=transform)
print("分类列表:", full_dataset.classes)
print("分类和索引的对应关系:", full_dataset.class_to_idx)
# 分割数据集为训练集和测试集
train_ratio = 0.7
train_size = int(train_ratio * len(full_dataset))
test_size = len(full_dataset) - train_size
train_dataset, test_dataset = random_split(full_dataset, [train_size, test_size])
# 模型准备
net = LeNet5(num_classes=len(full_dataset.classes)).to(device) # 使用 LeNet5 模型
state_dict = torch.load(pth_path)
net.load_state_dict(state_dict)
net.train()
# 保存网络结构到tensorboard
writer.add_graph(net, torch.randn(1, 3, 64, 64).to(device)) # 添加模型的计算图
# 训练设置
epochs = 10
batch_size = 64
criterion = nn.CrossEntropyLoss(reduction="sum")
optimizer = optim.Adam(net.parameters(), lr=0.0001)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
for epoch in range(epochs):
start_time = time.time()
accuracy = 0
total_loss = 0
# 使用 tqdm 显示进度条
for i, (x, y) in enumerate(tqdm(train_loader, desc=f"Epoch {
epoch+1}/{
epochs}")):
x, y = x.to(device), y.to(device)
optimizer.zero_grad()
yhat = net(x)
loss = criterion(yhat, y)
loss.backward()
optimizer.step()
accuracy += torch.sum(torch.argmax(yhat, dim=1) == y).item()
total_loss += loss.item()
# 每 1 个批次保存一次图像
if i % 1 == 0:
img_grid = vutils.make_grid(x, normalize=True, nrow=8) # 生成图像网格
writer.add_image(f"r_m_{
epoch}_{
i * 1}", img_grid, epoch * len(train_dataset) + i)
print(
f"Epoch {
epoch+1}/{
epochs} - Time: {
time.time() - start_time:.2f}s, Accuracy: {
accuracy / len(train_dataset):.4f}, Loss: {
total_loss / len(train_dataset):.4f}")
writer.add_scalar("Loss/train", total_loss / len(train_dataset), epoch)
writer.add_scalar("Accuracy/train", accuracy / len(t