一、基于AlexNet的猫狗分类
1.1、项目背景
猫和狗是我们生活中最常见的宠物,它们的图像数据大量存在于互联网上。对此进行分类不仅可以帮助开发自动化宠物识别应用,也可以应用于更广泛的计算机视觉领域。例如,训练良好的模型可以支持流浪动物的识别和归类、动物行为监测、虚拟宠物应用等方面。通过本项目,我们希望展现深度学习在实际应用中的有效性,以及其对社会的正面影响。
1.2、项目目的
在本项目中,我们的目标是构建一个深度学习模型,能够有效地识别和分类猫和狗的图像。通过使用卷积神经网络(CNN),尤其是著名的AlexNet架构,我们希望实现高精度的图像分类。完成该项目后,用户能够方便地将图像上传至应用程序,并获得该图像是猫还是狗的预测结果。
1.3、网络描述

AlexNet 是由Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton 在2012年提出的深度学习模型。它在ImageNet大规模视觉识别挑战赛中取得了显著的成功,显著提高了图像分类的准确率。该模型采用了多个卷积层、ReLU激活函数、全连接层和Dropout正则化方法,以有效防止过拟合。AlexNet的成功不仅证明了深度学习在计算机视觉领域的潜力,还推动了后续更多深度学习模型的研究和应用。
1.4、数据集
1.5、构建随机数种子
python
import os
from random import random
import numpy as np
import torch
def setup_seed(seed):
np.random.seed(seed) # 设置 Numpy 随机种子
random.seed(seed) # 设置 Python 内置随机种子
os.environ['PYTHONHASHSEED'] = str(seed) # 设置 Python 哈希种子
torch.manual_seed(seed) # 设置 PyTorch 随机种子
if torch.cuda.is_available():
torch.cuda.manual_seed(seed) # 设置 CUDA 随机种子
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False # 关闭 cudnn 加速
torch.backends.cudnn.deterministic = True # 设置 cudnn 为确定性算法
1.6、 CUDA检测
python
import torch
# 检查是否有可用的 GPU,如果有则使用 GPU,否则使用 CPU
if torch.cuda.is_available():
device = torch.device("cuda") # 使用 GPU
print("CUDA is available. Using GPU.")
else:
device = torch.device("cpu") # 使用 CPU
print("CUDA is not available. Using CPU.")
1.7、读取数据
python
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
transform = {
"train": transforms.Compose([transforms.RandomResizedCrop(224), transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
"test": transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
}
train_dataset = datasets.ImageFolder("./dataset/train", transform=transform["train"])
test_dataset = datasets.ImageFolder("./dataset/test", transform=transform["test"])
train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=False)
1.8、显示图像
python
import matplotlib.pyplot as plt
import numpy as np
# 打印图片
examples = enumerate(test_dataloader)
batch_idx, (imgs, labels) = next(examples)
for i in range(4):
mean = np.array([0.5, 0.5, 0.5])
std = np.array([0.5, 0.5, 0.5])
image = imgs[i].numpy() * std[:, None, None] + mean[:, None, None]
# 将图片转成numpy数组,主要是转换通道和宽高位置
image = np.transpose(image, (1, 2, 0))
plt.subplot(2, 2, i+1)
plt.imshow(image)
plt.title(f"Truth: {labels[i]}")
plt.show()
1.9、构建网络
python
import torch
from torch import nn
class AlexNet(nn.Module):
def __init__(self, num_classes=1000):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 96, 11, 4, 2),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, 2),
nn.Conv2d(96, 256, 5, 1, 2),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, 2),
nn.Conv2d(256, 384, 3, 1, 1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, 3, 1, 1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, 3, 1, 1),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, 2),
)
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
1.10、损失函数和优化器
python
cri = torch.nn.CrossEntropyLoss()
optomizer = torch.optim.Adam(model.parameters(), lr=0.0001)
1.11、训练模型
python
epoches = 100
for epoch in range(epoches):
model.train()
total_loss = 0
for i, (images, labels) in enumerate(train_dataloader):
# 数据放在设备上
images = images.to(device)
labels = labels.to(device)
# 前向传播
outputs = model(images)
loss = cri(outputs, labels)
# 反向传播
optomizer.zero_grad()
loss.backward()
optomizer.step()
total_loss += loss
print(f"Epoch [{epoch + 1}/{epoches}], Iter [{i}/{len(train_dataloader)}], Loss {loss:.4f}")
avg_loss = total_loss / len(train_dataloader)
print(f"Epoch [{epoch + 1}/{epoches}], Loss {avg_loss:.4f}")
if (epoch+1) % 10 == 0:
torch.save(model.state_dict(), f"./model/model_{epoch}.pth")
1.12、 验证模型
python
print("开始验证/评估模型:")
model.load_state_dict(torch.load("./model_best.pth",weights_only=True))
model.eval()
total = 0
correct = 0
predicted_labels = []
true_labels = []
with torch.no_grad():
for images, labels in test_dataloader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
predicted_labels.extend(predicted.cpu().numpy())
true_labels.extend(labels.cpu().numpy())
print(f"ACC {correct / total * 100}%")
# 生成混淆矩阵
conf = confusion_matrix(true_labels, predicted_labels)
# 可视化
sns.heatmap(conf, annot=True, fmt="d", cmap="Blues")
plt.xlabel("predict")
plt.ylabel("true")
plt.show()

1.13、完整代码
python
import os
import random
import numpy as np
import torch
from torch import nn # 导入神经网络模块
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# 设置随机种子以保证结果的可重复性
def setup_seed(seed):
np.random.seed(seed) # 设置 Numpy 随机种子,用于控制 Numpy 相关的随机操作
random.seed(seed) # 设置 Python 内置随机种子,用于控制 Python 内置的随机操作
os.environ['PYTHONHASHSEED'] = str(seed) # 设置 Python 哈希种子,用于控制哈希操作的随机性,保证实验的可复现性
torch.manual_seed(seed) # 设置 PyTorch 的 CPU 随机种子,用于控制 PyTorch 中 CPU 相关的随机操作
if torch.cuda.is_available(): # 检查是否有可用的 CUDA (GPU)
torch.cuda.manual_seed(seed) # 设置当前 GPU 的随机种子
torch.cuda.manual_seed_all(seed) # 设置所有 GPU 的随机种子(如果有多个 GPU)
torch.backends.cudnn.benchmark = False # 关闭 cuDNN 的 benchmark 模式。benchmark 模式会自动寻找适合当前配置的高效算法,但可能牺牲可重复性
torch.backends.cudnn.deterministic = True # 设置 cuDNN 使用确定性算法,保证在相同的输入和权重下,输出是相同的,进一步提高可重复性
# 设置随机种子
setup_seed(0)
# 检查是否有可用的 GPU,如果有则使用 GPU,否则使用 CPU
if torch.cuda.is_available():
device = torch.device("cuda") # 将设备设置为 CUDA (GPU)
print("CUDA is available. Using GPU.")
else:
device = torch.device("cpu") # 将设备设置为 CPU
print("CUDA is not available. Using CPU.")
# 定义数据预处理的转换
transform = {
"train": transforms.Compose([ # 训练集的数据预处理
transforms.RandomResizedCrop(224), # 随机裁剪图片到 224x224 的大小,并进行缩放
transforms.ToTensor(), # 将 PIL 图像或 NumPy 数组转换为 PyTorch 张量,并将像素值缩放到 [0, 1]
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 对图像的每个通道进行标准化,使其均值为 0.5,标准差为 0.5
]),
"test": transforms.Compose([ # 测试集的数据预处理
transforms.Resize((224, 224)), # 将图片缩放至 224x224 的大小
transforms.ToTensor(), # 将 PIL 图像或 NumPy 数组转换为 PyTorch 张量,并将像素值缩放到 [0, 1]
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 对图像的每个通道进行标准化,使其均值为 0.5,标准差为 0.5
]),
}
# 加载训练数据集,"./dataset/train" 是训练集图片所在的文件夹,transform 指定了数据预处理方式
train_dataset = datasets.ImageFolder("./dataset/train", transform=transform["train"])
# 加载测试数据集,"./dataset/test" 是测试集图片所在的文件夹,transform 指定了数据预处理方式
test_dataset = datasets.ImageFolder("./dataset/test", transform=transform["test"])
# 创建训练数据的数据加载器,batch_size 定义了每个批次的大小,shuffle=True 表示在每个 epoch 开始时打乱数据
train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
# 创建测试数据的数据加载器,batch_size 定义了每个批次的大小,shuffle=False 表示不打乱数据
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=False)
# 定义 AlexNet 模型结构
class AlexNet(nn.Module):
def __init__(self, num_classes=1000):
super(AlexNet, self).__init__()
# 特征提取层
self.features = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2), # 卷积层1:输入通道3,输出通道96,卷积核11x11,步长4,填充2
nn.ReLU(inplace=True), # ReLU 激活函数,inplace=True 表示直接修改输入
nn.MaxPool2d(kernel_size=3, stride=2), # 最大池化层1:池化窗口3x3,步长2
nn.Conv2d(96, 256, kernel_size=5, padding=2), # 卷积层2:输入通道96,输出通道256,卷积核5x5,填充2
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # 最大池化层2:池化窗口3x3,步长2
nn.Conv2d(256, 384, kernel_size=3, padding=1), # 卷积层3:输入通道256,输出通道384,卷积核3x3,填充1
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, kernel_size=3, padding=1), # 卷积层4:输入通道384,输出通道384,卷积核3x3,填充1
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1), # 卷积层5:输入通道384,输出通道256,卷积核3x3,填充1
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # 最大池化层3:池化窗口3x3,步长2
)
# 分类器
self.classifier = nn.Sequential(
nn.Dropout(p=0.5), # Dropout 层1:以 50% 的概率随机将一部分神经元的输出置为 0,防止过拟合
nn.Linear(256 * 6 * 6, 4096), # 全连接层1:输入特征数 256 * 6 * 6,输出特征数 4096
nn.ReLU(inplace=True),
nn.Dropout(p=0.5), # Dropout 层2
nn.Linear(4096, 4096), # 全连接层2:输入特征数 4096,输出特征数 4096
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes), # 全连接层3:输出层,输出特征数等于类别数
)
def forward(self, x):
x = self.features(x) # 通过特征提取层
x = torch.flatten(x, 1) # 将多维特征图展平成一维向量,方便输入到全连接层
x = self.classifier(x) # 通过分类器
return x
# 实例化 AlexNet 模型,num_classes=2 表示分类的类别数为 2,并将模型移动到指定的设备 (GPU 或 CPU) 上
model = AlexNet(num_classes=2).to(device)
# 定义损失函数为交叉熵损失,常用于多分类任务
cri = torch.nn.CrossEntropyLoss()
# 定义优化器为 Adam 优化器,model.parameters() 指定了需要优化的模型参数,lr=0.0001 是学习率
optomizer = torch.optim.Adam(model.parameters(), lr=0.0001)
# 定义训练的 epoch 数
epoches = 100
# 开始训练循环
for epoch in range(epoches):
most_acc = 0 # 初始化最佳的测试集准确率
model.train() # 将模型设置为训练模式,启用 dropout 和 batch normalization
total_loss = 0 # 初始化每个 epoch 的总损失
# 遍历训练数据加载器中的每个批次
for i, (images, labels) in enumerate(train_dataloader):
# 将图像和标签移动到指定的设备 (GPU 或 CPU) 上
images = images.to(device)
labels = labels.to(device)
# 前向传播:将图像输入模型,得到模型的输出
outputs = model(images)
# 计算损失:将模型的输出和真实的标签输入损失函数,得到损失值
loss = cri(outputs, labels)
# 反向传播之前将优化器的梯度清零
optomizer.zero_grad()
# 反向传播:计算损失相对于模型参数的梯度
loss.backward()
# 更新模型参数:根据计算得到的梯度和优化器的规则更新模型参数
optomizer.step()
# 累加每个批次的损失值
total_loss += loss
# 打印训练过程中的信息,包括当前的 epoch、迭代次数和当前的损失值
print(f"Epoch [{epoch + 1}/{epoches}], Iter [{i}/{len(train_dataloader)}], Loss {loss:.4f}")
# 计算一个 epoch 的平均训练损失
avg_loss = total_loss / len(train_dataloader)
# 打印每个 epoch 的平均训练损失
print(f"Train Data: Epoch [{epoch + 1}/{epoches}], Loss {avg_loss:.4f}")
# 进入模型评估模式,禁用 dropout 和 batch normalization
model.eval()
total, correct, test_loss, total_loss = 0, 0, 0, 0 # 初始化测试过程中的总样本数、正确样本数、批次损失和总损失
# 在评估模式下,不计算梯度
with torch.no_grad():
# 遍历测试数据加载器中的每个批次
for images, labels in test_dataloader:
# 将图像和标签移动到指定的设备 (GPU 或 CPU) 上
images = images.to(device)
labels = labels.to(device)
# 前向传播:将图像输入模型,得到模型的输出
outputs = model(images)
# 计算测试集的损失
test_loss = cri(outputs, labels)
# 累加每个批次的测试损失
total_loss += test_loss
# 获取模型输出中每个样本的最大值和对应的索引(即预测的类别)
_, predicted = torch.max(outputs.data, 1)
# 累加测试集的总样本数
total += labels.size(0)
# 累加预测正确的样本数
correct += (predicted == labels).sum().item()
# 计算平均测试损失
avg_test_loss = total_loss / len(test_dataloader)
# 计算测试集的准确率
acc = correct / total
# 打印每个 epoch 的测试损失和准确率
print(f"Test Data: Epoch [{epoch+1}/{epoches}], Loss {avg_test_loss:.4f}, Accuracy {acc * 100}%")
# 如果当前的测试准确率是最高的,则保存当前模型的权重
if acc > most_acc:
torch.save(model.state_dict(), f"./model/model_best.pth")
most_acc = acc
# 每隔 10 个 epoch 保存一次模型的权重
if (epoch+1) % 10 == 0:
torch.save(model.state_dict(), f"./model/model_{epoch+1}.pth")