题目描述:
在这个问题中,你将面临一个经典的机器学习分类挑战——猫狗大战。你的任务是建立一个分类模型,能够准确地区分图像中是猫还是狗。
预期解决方案:
你的目标是通过训练一个机器学习模型,使其在给定一张图像时能够准确地预测图像中是猫还是狗。模型应该能够推广到未见过的图像,并在测试数据上表现良好。我们期待您将其部署到模拟的生产环境中——这里推理时间和二分类准确度(F1分数)将作为评分的主要依据。
数据集:
链接:百度网盘 请输入提取码
提取码:jc34
通过问题来看,我们要进行环境配置、数据处理、搭建VGG模型、模型训练、测试集验证几个步骤。
环境配置
对于这个问题如果要用英特尔oneAPI Developer Cloud提供的免安装基于Jupyter Lab或SSH的环境,需要进行环境配置。
首先注册一个新的英特尔用户账号,网页地址:https://devcloud.intel.com/oneapi/home/
按照网页地址的提示信息完成注册后,进行激活。激活画面如下图
就可以通过点击网页左侧“Get Started”进入该选项的页面,在“Get Started”选项页面中, 用户可以 通过点击页面最左下角 Connect with Jupyter* Lab 中的“Launch JupyterLab*”按钮直接启动 Jupypter 服 务。
进入Jupyter* Lab后选择pytroch2024版就完成环境的配置了。如果你使用其他类型的pytroch也是可以完成这个实验的
二、数据集处理:
猫狗大战数据集共分为test和train两个文件夹,test文件夹里面的图片没有标签,因此我们仅使用train文件夹内的图片,部分图片如下,可以看到图片的标签为文件名的前三个字母,猫为cat,狗为dog。如图:
我们将猫狗大战的数据集下载下来后,上传到Jupyter* Lab平台上,上传后需要解压才能使用。如下图:
进行到这一步我们就可以进行代码的编写了:
加载包:
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.optim import Adam
from torchvision import transforms
from torchvision import models
from torchvision.io import read_image
from torch.nn import functional as F
from torch.utils.data import Dataset, DataLoader
import numpy as np
from sklearn.model_selection import StratifiedShuffleSplit
import osos.environ['TORCH_HOME']='dogandcat/download_model'
# 获取训练集图片路径
train_path = '/home/u209887/dogandcat/train/'
image_file_path = np.array([train_path + i for i in os.listdir(train_path)])
# 根据文件名获取图片对应的标签
labels = np.array([
0 if name.split('/')[-1].startswith('cat') else 1
for name in image_file_path
])
print(len(image_file_path),len(labels))
)
结果如图:
# 查看结果
image_file_path[:10], labels[:10]
结果如图:
使用 StratifiedShuffleSplit 进行随机分层抽样,将25000张图片按 8:1:1 的比例分为训练集、验证集和测试集,且每个数据集中猫和狗的占比均相同
# 分层抽样
# 将数据集按照 8:2 的比例分为训练集和其他数据集
train_val_sss = StratifiedShuffleSplit(n_splits=1, test_size=0.1, random_state=2021)
for train_index, val_index in train_val_sss.split(image_file_path, labels):
train_path, val_path = image_file_path[train_index],image_file_path[val_index]
train_labels, val_labels = labels[train_index],labels[val_index]
# 将其他数据集按照 1:1 的比例分为验证集和测试集
val_test_sss = StratifiedShuffleSplit(n_splits=1, test_size=0.5, random_state=2021)
for val_index, test_index in val_test_sss.split(val_path, val_labels):
val_path, test_path = val_path[val_index],val_path[test_index]
val_labels, test_labels = val_labels[val_index],val_labels[test_index]
# 最终我们得到 8:1:1 的训练集、验证集和测试集
# 每个数据集中猫和狗所占比例相同
查看分层抽样结果
# 由于猫的标签是0,狗的标签是1,因此我们可以用.sum()函数获取到狗的个数
print(len(train_path),len(train_labels),train_labels.sum())
print(len(val_path),len(val_labels),val_labels.sum())
print(len(test_path),len(test_labels),test_labels.sum())
结果如图:
- 定义Pytorch数据加载类
- # 定义Dataset加载方式
-
class MyData(Dataset):
def __init__(self, filepath, labels=None, transform=None):
self.filepath = filepath
self.labels = labels
self.transform = transformdef __getitem__(self, index):
image = read_image(self.filepath[index]) # 读取后的图片维度是[channl, height, width]
image = image.to(torch.float32) / 255. # 转换为float32类型,除以255进行归一化
if self.transform is not None:
image = self.transform(image)
if self.labels is not None:
return image, self.labels[index]
return imagedef __len__(self):
return self.filepath.shape[0] -
定义一些指标
image_size = [224, 224] # 图片大小
batch_size = 64 # 批大小
定义图像增强器
transform = transforms.Compose([
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.RandomRotation(30), # 随机旋转
transforms.Resize([256, 256]), # 设置图片大小
transforms.RandomCrop(image_size), # 将图片随机裁剪为所需图片大小
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 使用ImageNet的标准化参数
])
获取数据集对象
ds1 = MyData(train_path, train_labels, transform)
ds2 = MyData(val_path, val_labels, transform)
ds3 = MyData(test_path, test_labels, transform)train_ds = DataLoader(ds1, batch_size=batch_size, shuffle=True)
val_ds = DataLoader(ds2, batch_size=batch_size, shuffle=True)
test_ds = DataLoader(ds3, batch_size=batch_size, shuffle=True)
针对单张图片,查看图像增强的效果
# 对一张图片查看增强效果image = read_image(train_path[0])
image = image.to(torch.float32) / 255. # 读取单张图片,并进行归一化
image = image.unsqueeze(0) # 为单张图片加入batch维度plt.figure(figsize=(12,12))
mean = np.array([0.485, 0.456, 0.406]) # 图像增强所用的标准化参数
std = np.array([0.229, 0.224, 0.225])
for i in range(25):
plt.subplot(5,5,i+1)
img = transform(image)
img = img[0,:,:,:].data.numpy().transpose([1,2,0]) # 将图片从[channel,height,width]变为[height,width,channel]
img = std * img + mean # 反标准化
img = np.clip(img, 0, 1) # 将像素值限制在0~1之间
plt.imshow(img)
plt.axis('off')
plt.show()结果如图
-
查看训练集一个批次的图片
# 查看训练集一个batch的图片
for step, (bx, by) in enumerate(train_ds):
if step > 0:
break
plt.figure(figsize=(16,16))
for i in range(len(by)):
plt.subplot(8,8,i+1)
image = bx[i,:,:,:].data.numpy().transpose([1,2,0])
image = std * image + mean # 反标准化
image = np.clip(image, 0, 1)
plt.imshow(image)
plt.axis('off')
plt.title('cat' if by[i].data.numpy()==0 else 'dog')
plt.show() -
构建模型
此次采用的是VGG模型 -
model = models.vgg16(pretrained=True)
# 如果需要微调,可以解冻最后几层
for param in vgg16_model.features.parameters():
param.requires_grad = False# 修改分类层
num_features = vgg16_model.classifier[6].in_features
vgg16_model.classifier[6] = nn.Sequential(
nn.Linear(num_features, 512),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(512, 2)
)
打印模型
print(model)将模型迁移到GPU上,以提高训练速度
# 如果有GPU则使用GPU,否则使用CPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
定义优化器和损失函数
optimizer = Adam(model.parameters(), lr=0.0001)
loss_fun = F.cross_entropy
训练模型
定义训练函数
def train(model, epoch, train_ds):
model.train() # 开启训练模式
total_num = len(train_ds.dataset) # 训练集总个数
train_loss = 0 # 训练损失
correct_num = 0 # 分类正确的个数for image, label in train_ds:
image = image.to(device) # 将图片和标签都迁移到GPU上进行加速
label = label.to(device)
label = label.to(torch.long) # 将标签从int32类型转化为long类型,否则计算损失会报错output = model(image) # 获取模型输出
loss = loss_fun(output, label) # 计算交叉熵损失
train_loss += loss.item() * label.size(0) # 累计总训练损失
optimizer.zero_grad() # 清除优化器上一次的梯度
loss.backward() # 进行反向传播
optimizer.step() # 更新优化器predict = torch.argmax(output, dim=-1) # 获取预测类别
correct_num += label.eq(predict).sum() # 统计预测正确的个数train_loss = train_loss / total_num # 计算一个轮次的训练损失
train_acc = correct_num / total_num # 计算一个轮次的训练准确率
# 打印相关信息
print('epoch: {} --> train_loss: {:.6f} - train_acc: {:.6f} - '.format(
epoch, train_loss, train_acc), end='')
定义测试函数
def evaluate(model, eval_ds, mode='val'):
model.eval() # 开启测试模式total_num = len(eval_ds.dataset)
eval_loss = 0
correct_num = 0for image, label in eval_ds:
image = image.to(device)
label = label.to(device)
label = label.to(torch.long)
output = model(image)
loss = loss_fun(output, label)
eval_loss += loss.item() * label.size(0)predict = torch.argmax(output, dim=-1)
correct_num += label.eq(predict).sum()
eval_loss = eval_loss / total_num
eval_acc = correct_num / total_num
print('{}_loss: {:.6f} - {}_acc: {:.6f}'.format(
mode, eval_loss, mode, eval_acc)) -
开始训练,训练20个epoch
for epoch in range(20):
train(model, epoch, train_ds)
evaluate(model, val_ds)
在测试集上测试模型
- import time
start_time = time.time()
# 测试模型
evaluate(model, test_ds, mode='test')
end_time = time.time() # 记录结束时间
predict_time = end_time - start_time
print(f'推理时间为: {predict_time:.3f} seconds') - 所以最后所得推理时间和f1值为:
因为用oneapi平台上的pytroch没有GPU,所以在测试集上的时间有点大,但在预测结果上还行
- 优化模型,调参
- # 解冻特征提取层
for name, m in model.named_parameters():
if name.split('.')[0] != 'fc':
m.requires_grad_(True)
# 使用一个学习率更小的优化器
optimizer = Adam(model.parameters(), lr=0.00001) -
所得推理时间和f1值为:
但从结果来看优化的并不理想,只是在损失函数和运行时间上缩短了一点点
- 图片预测:
-
import matplotlib.pyplot as plt
import numpy as np# 选择一张 test_ds 中的图片
sample_image, true_label = next(iter(test_ds))# 将图片传递给模型进行预测
sample_image = sample_image.to(device)
with torch.no_grad():
model_output = model(sample_image)# 获取预测结果
_, predicted_label = torch.max(model_output, 1)# 转换为 NumPy 数组
sample_image = sample_image.cpu().numpy()[0] # 将数据从 GPU 移回 CPU 并取出第一张图片
predicted_label = predicted_label[0].item()true_label = true_label[0].item() # 直接获取标量值
# 获取类别标签
class_labels = ['cat', 'dog']# 显示图像
plt.imshow(np.transpose(sample_image, (1, 2, 0))) # 转置图片的维度顺序
plt.title(f'TRUE LABEL IS: {class_labels[true_label]}, PREDICT LABEL IS: {class_labels[predicted_label]}')
plt.axis('off')
plt.show() -
预测结果为:
-
-