目录
简介
这是一个简单的resnet实现猫狗分类例子,项目代码可在github获取,希望你能够学习如下内容:
1)神经网络实现分类的原理
2)了解模型优化流程:数据处理、损失计算、反向传播等
3)熟悉如何使用torch加载图片数据、搭建模型、训练模型。
图像分类
定义:给定一张图像,判断图像中目标类别
深度学习分类原理
在深度学习中,通过卷积神经网络对图像进行特征提取,得到一个代表图像特征的向量,然后通过全连接层将特征向量映射到类别输出,最后通过softmax层得到类别概率输出,整体流程如下。通过训练,不断的更新优化模型的参数,让模型具备预测新数据的能力。
卷积层
作用:通过滑动窗口(卷积核)在特征图上进行滑动并进行计算,实现图像的特征提取。卷积的特性包括局部感知机制、权值共享等,这些特性使得卷积神经网络能够有效地处理图像数据,同时减少模型的参数数量,防止过拟合。
图像在计算机中就是一个多维向量,比如(3x224x224),经过卷积网络后假设得到:(7x7x512)大小的特征图(32倍下采样)。
全连接层
简单来说,全连接层就是一个线性分类层,图像经过卷积网络得到的特征图经过池化或者flatten后,变成一维向量,比如长度512的向量,然后经过全连接层进行线性映射,得到类别输出。比如二分类,得到长度为2的向量。举一个简单的例子:
Softmax 函数
作用就是将一组数值转换为对应的概率,且概率和为 1。公式:
比如,经过全连接层得到每个类别输出值为[-0.3241, -0.8814],经过 Softmax 之后,得到类别概率[0.6358, 0.3642],原始的输出转换成一组概率,并且概率的和为 1 。原始输出中值最大的类别具有最大的概率。
Loss计算
深度学习模型训练流程:前向传播计算模型输出、模型输出和标签根据损失函数计算损失、通过损失计算模型参数的梯度、优化器根据参数梯度更新模型参数。损失函数在模型优化中比较重要,它影响模型参数的梯度计算。Softmax对应使用交叉熵损失函数,其计算公式如下:
上式中, a是softmax的计算结果,y是训练样本的标签(one-hot编码),表示该样本正确的分类类型,如果以向量表示的话,其中只有一个元素为1,其余元素都为0:
基于这个特性,所以损失大小只与网络判断正确分类的概率有关。所以损失的公式可以简化为(t是正确的那个类别):
Torch代码实战
数据集
使用Kaggle的猫狗数据集进行分类,该数据集总共25000图片,猫和狗数量各占一半,图片大小不固定。随机挑选1000张猫和1000张狗的图片作为验证集。训练集真理成如下目录结构:
部分数据集示例:
数据集下载连接:https://www.microsoft.com/en-us/download/details.aspx?id=54765
数据加载
Torch中数据加载涉及两个类torch.utils.data中的Dataset和DataLoader。
Dataset:数据集类。需要实现__getitem__方法来获取数据集中的单个样本,实现__len__方法来获取数据集的大小,数据预处理也在该类实现。
DataLoader:数据加载类,设置适当的参数来加载数据,比如一次加载的批次大小,加载数据线程数等。它是一个可迭代的对象,每次迭代会返回一个批次的图片数据
使用示例:
train_dataset = build_data_set(224, data) # 调用dataset.py中的build_data_set()方法
train_loader = DataLoader(train_dataset, 10, shuffle=True, num_workers=1)
for i, (images, target) in enumerate(train_loader):
print(images.shape, target)
break
输出:
torch.Size([10, 3, 224, 224]) tensor([0, 1, 1, 1, 1, 1, 1, 0, 1, 1])
在这个案例中,我们使用datasets.ImageFolder来代替Dataset对象,ImageFolder类会根据给定文件夹中的子文件读取图片并根据子文件顺序,自动生成类别索引。比如上述训练集文件,Cat、Dog对应类别分别为0、1。
ImageFolder类同时接收一个transform对象,用来定义图片的预处理方式,通常图片数据在进入模型之前需要进行如下处理:1)图片缩放固定大小;2)转为tensor,并归一化到[0,1];3)标准化到正态分布。
# 构建dataset
transform = transforms.Compose([
transforms.Resize((img_size, img_size)), # 图片缩放到(224,224)
transforms.ToTensor(), # 将图像数据格式转换为Tensor格式。所有数除以255,将数据归一化到[0,1]
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) # 标准正态分布变换,三通道归一化到[-1,1]。(input-mean)/std
])
标准正态分布参考:https://www.cnblogs.com/oliyoung/p/transforms_Normalize.html
模型搭建
模型使用resnet18,这里没有自己搭建,而是使用了官方的预选连模型,然后把全连接层输出设置为自己的类别数。有时间的话可以自己搭建一个,熟悉网络结构。注意模型fc输出结果是没有经过softmax的。
# 构建一个分类模型
model = torchvision.models.resnet18(pretrained=True)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, num_class) # 将全连接层输出设置为自己的类别数
return model
损失函数
交叉熵损失
# 损失函数选择
criterion = nn.CrossEntropyLoss()
优化器
优化器决定了模型参数的更新方式,这个案例使用SGD(随机梯度下降),学习率设置为0.001,动量设置为0.9,权重衰减为0.0001。
# 优化器
optimizer = torch.optim.SGD(model.parameters(), args.lr, momentum=args.momentum, weight_decay=args.weight_decay)
优化器详细参考:
一个框架看懂优化算法之异同 SGD/AdaGrad/Adam:https://zhuanlan.zhihu.com/p/32230623
训练流程
训练的基本流程:前向传播、计算损失、参数梯度归零、损失反向传播计算梯度、优化器更具梯度更新参数。训练过程,每训练一完一个epcoh,验证模型一次,保存一次模型权重。优于训练集样本较多,且分类任务比较简单,模型训练2-3轮后,就能在测试集上达到一个不错的效果。
# 正式训练
for epoch in range(args.epochs): # args.epochs,epochs=10
print(f'Epoch {epoch}/{args.epochs}')
# switch to train mode
model.train()
for i, (images, target) in enumerate(tqdm(train_loader)):
images, target = images.to(device), target.to(device)
# 计算输出
output = model(images)
# 计算loss
loss = criterion(output, target)
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 梯度优化
optimizer.step()
# 一轮验证一次模型
val(model, device, test_loader, criterion)
# 模型保存,一轮保存一次
if not os.path.exists(args.checkpoint_dir):
os.mkdir(args.checkpoint_dir)
torch.save(model.state_dict(), os.path.join(args.checkpoint_dir, 'checkpoint_epoch_{}.pth'.format(epoch)))
print("model saved success")
训练输出:
模型预测
模型训练好后,保存的权重文件在./ckpts/下,可以加载模型权重,对单张图片进行预测,判断类别。
# 读取图像
img = Image.open(img_path)
img1 = img.copy()
# 图片预处理,保证和训练是一样
transform = transforms.Compose([
transforms.Resize((224, 224)), # 图片缩放到(224,224)
transforms.ToTensor(), # 将图像数据格式转换为Tensor格式。所有数除以255,将数据归一化到[0,1]
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) # 标准正态分布变换,三通道归一化到[-1,1]。(input-mean)/std
])
image = transform(img) # (3, 224, 224)
image = image.unsqueeze(0) # 增加batch维度 (1, 3, 224, 224)
# print(image.shape)
# 模型加载
model = buid_model(2)
model.load_state_dict(torch.load(model_path), strict=True)
model.eval() # 必须是eval模式,否则模型预测不准
# 预测
out = model(image) # out: (1, 2)
pro = F.softmax(out, 1) # 结果进行softmax,得到每个类别的预测概率 pro: (1, 2)
_, pred = torch.max(out.data, 1) # 获取模型输出结果最大值索引,得到预测类别
cls_index = int(pred) # 类别索引
cls_pro = round(float(pro[0][cls_index]), 2) # 类别概率,保留两位小数
print(cls_index, cls_pro)
# 可视化模型分类结果
# 创建一个可以在给定图片上绘图的对象
cl_dic = {0: "Cat", 1: "Dog"}
draw = ImageDraw.Draw(img1)
# 定义字体和大小
font = ImageFont.truetype("arial.ttf", 10)
# 写字
draw.text((10, 10), "class:" + cl_dic[cls_index]+" pro:"+str(cls_pro), font=font, fill=(0, 255, 255))
# 保存新的图片
img1.show()
预测结果: