介绍
当今社会,图像识别技术已经成为了人工智能领域中备受关注的研究方向之一。在这个领域中,动物识别作为一个重要的应用场景,引起了广泛的关注和研究。动物识别技术不仅能够帮助我们更好地了解动物的生态习性、行为特征,还能够在野生动物保护、农业生产、动物监测等领域发挥重要作用
而在动物识别技术中,图像分类是一个关键的环节。通过对动物图像进行分类,我们可以将动物按照种类进行区分,从而实现对不同动物的识别和分类。为了实现高效准确的动物图像分类,人们提出了各种各样的图像分类算法和模型。
在这篇博客中,我们将重点介绍基于深度学习模型的动物图像分类方法,并着重讨论使用 AlexNet 模型进行动物图像分类的背景和意义。AlexNet 是由 Alex Krizhevsky 等人在 ImageNet 大规模视觉识别挑战赛(ILSVRC)中提出的一种深度卷积神经网络模型,它以较大的卷积核和更深的网络结构为特点,在图像分类任务上取得了非常出色的表现。因此,使用 AlexNet 模型进行动物图像分类不仅能够实现高效准确的分类效果,还能够借鉴和应用深度学习在图像识别领域取得的先进成果,推动动物识别技术的发展和应用。
AlexNet 简介
论文原文AlexNet
论文简要介绍
这篇论文的全名是《ImageNet Classification with Deep Convolutional Neural Networks》,由Alex Krizhevsky, Ilya Sutskever, 和 Geoffrey E. Hinton 三位作者撰写,他们均来自多伦多大学。论文首次发表于2012年的NIPS会议。
这篇论文主要介绍了一种大型深度卷积神经网络,用于对ImageNet LSVRC-2010比赛中的1.2百万高分辨率图像进行分类,这些图像涵盖了1000个不同的类别。通过使用非饱和神经元和高效的GPU卷积运算实现,他们的模型在测试数据上达到了前所未有的低错误率,明显优于当时的最佳状态。具体来说,模型在top-1和top-5错误率上分别达到了37.5%和17.0%。
这项工作的主要贡献
- 训练了当时最大的卷积神经网络之一,并在ILSVRC-2010和ILSVRC-2012竞赛数据集上取得了突破性的结果。
- 实现了高度优化的GPU卷积运算。
- 网络采用了多个新颖的特性来提高性能并缩短训练时间,比如使用ReLU非线性激活函数、多GPU训练、局部响应归一化和重叠池化等。
- 使用了dropout方法来减少全连接层的过拟合。
AlexNet网络架构
这就是CNN架构示意图,明确显示了两个gpu之间的职责划分。一个GPU运行图顶部的层部件,而另一个运行图底部的层部件。
下面描述一下AlexNet的网络的架构图的概念
神经网络架构
- 输入层: 224 x 224 x 3 图像
↓ - 第1卷积层: 96个11x11的卷积核, 步长4, ReLU激活
↓ - 最大池化层: 3x3, 步长2
↓ - 局部响应归一化
↓ - 第2卷积层: 256个5x5的卷积核, ReLU激活
↓ - 最大池化层: 3x3, 步长2
↓ - 局部响应归一化
↓ - 第3卷积层: 384个3x3的卷积核, ReLU激活
↓ - 第4卷积层: 384个3x3的卷积核, ReLU激活
↓ - 第5卷积层: 256个3x3的卷积核, ReLU激活
↓ - 最大池化层: 3x3, 步长2
↓ - 第1全连接层: 4096个神经元, ReLU激活
↓- Dropout
↓
- Dropout
- 第2全连接层: 4096个神经元, ReLU激活
↓- Dropout
↓
- Dropout
- 第3全连接层: 1000个神经元, Softmax输出
↓ - 输出层: 1000类预测
数据集介绍
Oxford-IIIT Pets 是一个宠物图像数据集。 该数据集涉及37 个类别(其中犬类25 类,猫类12 类),每个类别大约有200 张图片。 这些图像在比例、姿势和光照条件方面有很大的差异。 所有图像都有一个相应的ground truth 标注,包括品种、头部ROI 和像素级的trimap 分割。
下面将一步步教你如何实现基于Alex Net的动物识别。
数据的预处理
这个数据集下载下来,打开后是这样的:
所以我们需要对图片进行分类,不难发现同一类别下划线前面的字符都是一样的,我们可以据此为突破口写个脚本对图片进行分类。
#整理数据集,对图片进行分类
import os
import shutil
def organize_images_by_prefix(source_dir):
"""
遍历指定的文件夹,根据图片文件名中"_"前的字符分类,将同类图片放入以该字符命名的文件夹中。
参数:
source_dir (str): 图片存放的源目录路径。
"""
# 确保源目录存在
if not os.path.exists(source_dir):
print("指定的目录不存在,请检查路径。")
return
# 遍历源目录中的所有文件
for filename in os.listdir(source_dir):
if filename.endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
# 获取文件名中"_"前的部分
prefix = filename.split('_')[0]
# 创建目标文件夹路径
target_dir = os.path.join(source_dir, prefix)
# 如果目标文件夹不存在,则创建
if not os.path.exists(target_dir):
os.makedirs(target_dir)
# 构建源文件的完整路径
source_file = os.path.join(source_dir, filename)
# 构建目标文件的完整路径
target_file = os.path.join(target_dir, filename)
# 移动文件
shutil.move(source_file, target_file)
print(f"已移动 {filename} 到 {target_dir}")
# 指定你的源目录路径
source_directory = './datasets/images/'
organize_images_by_prefix(source_directory)
分好了就这样:
但发现少了两类,经查看,是因为像这种被分为一类了。
需要重新改一下脚本,但是我觉得太麻烦,少两类就少两类吧。大类是正确的就好。
按指定的比例分配图像到训练集、验证集和测试集中
#按指定的比例分配图像到训练集、验证集和测试集中
import os
import shutil
import numpy as np
def split_data(source_dir, train_dir, val_dir, test_dir, train_size=0.8, val_size=0.2):
"""
从每个类别的源文件夹中抽取图像并按比例分配到训练集、验证集和测试集中。
参数:
source_dir (str): 包含所有类别子文件夹的根目录。
train_dir (str): 训练集的目标根目录。
val_dir (str): 验证集的目标根目录。
test_dir (str): 测试集的目标根目录。
train_size (float): 分配到训练集的比例。
val_size (float): 分配到验证集的比例。
"""
categories = os.listdir(source_dir)
for category in categories:
category_dir = os.path.join(source_dir, category)
images = os.listdir(category_dir)
np.random.shuffle(images)
# 计算训练集、验证集和测试集的切分点
n_total = len(images)
n_train = int(n_total * train_size)
n_val = int(n_total * val_size)
# 创建目标文件夹
os.makedirs(os.path.join(train_dir, category), exist_ok=True)
os.makedirs(os.path.join(val_dir, category), exist_ok=True)
os.makedirs(os.path.join(test_dir, category), exist_ok=True)
# 分配图像到训练集
for image in images[:n_train]:
shutil.copy(os.path.join(category_dir, image), os.path.join(train_dir, category))
# 分配图像到验证集
for image in images[n_train:n_train+n_val]:
shutil.copy(os.path.join(category_dir, image), os.path.join(val_dir, category))
# 分配图像到测试集
for image in images[n_train+n_val:]:
shutil.copy(os.path.join(category_dir, image), os.path.join(test_dir, category))
# 设置源目录和目标目录
source_directory = './datasets/images' # 源图像文件夹
train_directory = './datasets1/train' # 训练集目标文件夹
val_directory = './datasets1/val' # 验证集目标文件夹
test_directory = './datasets1/test' # 测试集目标文件夹
# 执行数据分割
split_data(source_directory, train_directory, val_directory, test_directory)
加载数据集
import os
import torch
from torchvision import datasets, transforms
from torchvision.datasets import ImageFolder
def get_data_loaders(data_dir, batch_size=32):
# 定义图像的转换操作
transform = transforms.Compose([
transforms.Resize((227, 227)), # 将图像调整为227x227
transforms.ToTensor(), # 转换为torch.Tensor并归一化到[0,1]
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # 归一化
])
# 定义一个简单的文件验证函数,排除非图像文件和.ipynb_checkpoints目录
def is_valid_file(x):
return x.endswith(('.jpg', '.jpeg', '.png', '.ppm', '.bmp', '.pgm', '.tif', '.tiff', '.webp')) and not '.ipynb_checkpoints' in x
# 定义训练集、验证集和测试集的文件夹路径
train_dir = os.path.join(data_dir, 'train')
val_dir = os.path.join(data_dir, 'val')
test_dir = os.path.join(data_dir, 'test')
# 使用ImageFolder加载数据集,加入is_valid_file函数过滤文件
train_dataset = ImageFolder(train_dir, transform=transform, is_valid_file=is_valid_file)
val_dataset = ImageFolder(val_dir, transform=transform, is_valid_file=is_valid_file)
test_dataset = ImageFolder(test_dir, transform=transform, is_valid_file=is_valid_file)
# 创建DataLoader
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
return train_loader, val_loader, test_loader
# 设置数据目录和批量大小
data_dir = './datasets1'
batch_size = 128
# 获取数据加载器
train_loader, val_loader, test_loader = get_data_loaders(data_dir, batch_size=batch_size)
但是这段代码运行的时候,出现了如下错误:
.ipynb_checkpoints问题:是因为Jupyter Notebook在保存文件时会自动创建这个目录来存储备份,导致ImageFolder尝试将它作为一个类别目录加载。所以,不急,写两行代码,干掉他们:
import os
import shutil
def remove_unwanted_dirs(directory, unwanted_dirs=['.ipynb_checkpoints']):
"""
从给定目录中递归删除不需要的文件夹。
参数:
directory (str): 要清理的根目录。
unwanted_dirs (list): 不需要的目录名称列表。
"""
for root, dirs, files in os.walk(directory, topdown=False):
for name in dirs:
if name in unwanted_dirs:
dir_path = os.path.join(root, name)
shutil.rmtree(dir_path)
print(f"Removed directory: {dir_path}")
# 设置数据目录
data_dir = './datasets1'
# 删除不需要的目录
remove_unwanted_dirs(data_dir)
再运行上面代码,即可解决问题。
定义神经网络
下面就简简单单根据论文来搭建神经网络:
#定义神经网络
import torch
import torch.nn as nn
class AlexNet(nn.Module):
def __init__(self, num_classes=35):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=0),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(96, 256, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(256, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
alexnet = AlexNet(num_classes=35)
#尝试不同的权重初始化策略:
# 初始化权重
def initialize_weights(m):
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.Linear):
nn.init.xavier_normal_(m.weight)
nn.init.constant_(m.bias, 0)
alexnet.apply(initialize_weights)
print(alexnet)
设置损失函数和优化器
#设置损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(alexnet.parameters(), lr=0.001) # 使用Adam优化器
开始训练
#训练并且过程可视化
import torch
import matplotlib.pyplot as plt
def train_model(model, criterion, optimizer, train_loader, val_loader, num_epochs=100):
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
train_losses, val_losses = [], []
for epoch in range(num_epochs):
model.train() # 设置模型为训练模式
running_loss = 0.0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad() # 梯度归零
outputs = model(inputs) # 前向传播
loss = criterion(outputs, labels)
loss.backward() # 反向传播
optimizer.step() # 更新权重
running_loss += loss.item() * inputs.size(0)
epoch_loss = running_loss / len(train_loader.dataset)
train_losses.append(epoch_loss)
# 验证阶段
model.eval() # 设置模型为评估模式
with torch.no_grad():
running_loss = 0.0
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
loss = criterion(outputs, labels)
running_loss += loss.item() * inputs.size(0)
epoch_val_loss = running_loss / len(val_loader.dataset)
val_losses.append(epoch_val_loss)
print(f'Epoch {epoch+1}/{num_epochs}, Train Loss: {epoch_loss:.4f}, Validation Loss: {epoch_val_loss:.4f}')
return train_losses, val_losses
# 运行训练函数并可视化结果
train_losses, val_losses = train_model(alexnet, criterion, optimizer, train_loader, val_loader, num_epochs=100)
# 绘制损失曲线
plt.figure(figsize=(10, 5))
plt.plot(train_losses, label='Training Loss')
plt.plot(val_losses, label='Validation Loss')
plt.title('Training and Validation Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
# 保存模型权重
torch.save(alexnet.state_dict(), 'alexnet_trained.pth')
print("Saved trained model weights to 'alexnet_trained.pth'.")
模型评估
#模型评估
def evaluate_model(model, test_loader):
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
model.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in test_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
accuracy = 100 * correct / total
print(f'Accuracy on test set: {accuracy:.2f}%')
# 加载模型权重
alexnet.load_state_dict(torch.load('alexnet_trained.pth'))
evaluate_model(alexnet, test_loader)
模型预测
from PIL import Image
from torchvision.transforms import ToTensor
def predict_image(image_path, model):
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
model.eval()
img = Image.open(image_path)
img = ToTensor()(img).unsqueeze(0) # 图像预处理和添加批次维度
img = img.to(device)
with torch.no_grad():
outputs = model(img)
_, predicted = torch.max(outputs, 1)
return predicted.item()
# 示例用法
image_path = 'your_image.jpg'
prediction = predict_image(image_path, alexnet)
print(f'Predicted class index: {prediction}')
总结
AlexNet虽然在当时是一个划时代的模型,但其参数效率较低,模型较大,需要较多的计算资源。反正给我的感觉就是训练起来挺考验个人经验的。后续会为大家介绍一种更小的模型且能达到更快的训练速度。