一、前言
猫狗识别是CNN网络一个入门级的任务,通过实现猫狗识别,可以更好的了解CNN网络的结构以及运行效果,更可贵的是,猫狗识别实现简单,效果显著,可以很好的激发学习动力。
Dogs vs. Cat | kaggle连接:www.kaggle.com/c/dogs-vs-c…
二、准备数据集
从kaggle中,我们可以下载到25000张猫狗的图片,其中,猫和狗的图片各有12500张。
下载kaggle数据集有个小技巧:
- 先用谷歌浏览器进行下载,谷歌浏览器会转到谷歌的镜像站(应该是),然后复制这个下载链接,在迅雷等下载工具中打开,下载速度就翻了好几倍,减少下载等待时间。
将下载完的数据解压到项目文件的train目录下
三、拆分数据
import os
import shutil
def get_address():
"""获取所有图片路径"""
data_file = os.listdir('./train/')
dog_file = list(filter(lambda x: x[:3] == 'dog', data_file))
cat_file = list(filter(lambda x: x[:3] == 'cat', data_file))
root = os.getcwd()
return dog_file, cat_file, root
def arrange():
"""整理数据,移动图片位置"""
dog_file, cat_file, root = get_address()
print('开始数据整理')
# 新建文件夹
for i in ['dog', 'cat']:
for j in ['train', 'val']:
try:
os.makedirs(os.path.join(root,j,i))
except FileExistsError as e:
pass
# 移动10%(1250)的狗图到验证集
for i, file in enumerate(dog_file):
ori_path = os.path.join(root, 'train', file)
if i < 0.9*len(dog_file):
des_path = os.path.join(root, 'train', 'dog')
else:
des_path = os.path.join(root, 'val', 'dog')
shutil.move(ori_path, des_path)
# 移动10%(1250)的猫图到验证集
for i, file in enumerate(cat_file):
ori_path = os.path.join(root, 'train', file)
if i < 0.9*len(cat_file):
des_path = os.path.join(root, 'train', 'cat')
else:
des_path = os.path.join(root, 'val', 'cat')
shutil.move(ori_path, des_path)
print('数据整理完成')
复制代码
由于kaggle未提供验证集,因此我们可以从训练集中划分出一部分,作为验证集。监督学习可遵循8:1:1的原则,我们划分10%的数据作为验证集,即猫狗图片各1250张。
要注意,这里取出的2500张是不能再返回训练集进行训练的,若训练集与验证集相重合,会导致过拟合的发生(结果很好看,实战用不了)。
四、转为可读入数据
"""get_data.py"""def get_data(input_size, batch_size):
"""获取文件数据并转换"""
from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader
# 串联多个图片变换的操作(训练集)
# transforms.RandomResizedCrop(input_size) 先随机采集,然后对裁剪得到的图像缩放为同一大小
# RandomHorizontalFlip() 以给定的概率随机水平旋转给定的PIL的图像
# transforms.ToTensor() 将图片转换为Tensor,归一化至[0,1]
# transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) 归一化处理(平均数,标准偏差)
transform_train = transforms.Compose([
transforms.RandomResizedCrop(input_size),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])
# 获取训练集(通过上面的方面操作)
train_set = ImageFolder('train', transform=transform_train)
# 封装训练集
train_loader = DataLoader(dataset=train_set,
batch_size=batch_size,
shuffle=True)
# 串联多个图片变换的操作(验证集)
transform_val = transforms.Compose([
transforms.Resize([input_size, input_size]), # 注意 Resize 参数是 2 维,和 RandomResizedCrop 不同
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])
# 获取验证集(通过上面的方面操作)
val_set = ImageFolder('val', transform=transform_val)
# 封装验证集
val_loader = DataLoader(dataset=val_set,
batch_size=batch_size,
shuffle=False)
# 输出
return transform_train, train_set, train_loader, transform_val, val_set, val_loader
复制代码
读取数据方面,我使用的是pytorch自带的读取函数。除了读取数据,它还能在读取时对数据进行一个统一的处理。
此处使用pytorch 中的 ImageFolder 可以直接读取图片集数据(第一个参数决定文件夹地址),但是每个图片大小各异,且需要转化为可识别的数据。需要对读取的图片进行变换操作(即transform参数),除图片缩放外,还需进行归一化处理以减小数据复杂度和方便数据处理。通过transforms.Compose函数,可将这些图片变化操作串联,并通过ImageFolder的调用,快速获取到所需要的数据。例如上面,我就使用了它封装好的随机裁剪至相同大小,随机旋转,归一化等操作。即方便了数据丢入网络中进行训练,又能对图片扩大特征(一只旋转了的狗还是一只狗)。
五、构建网络
Resnet-18:残差网络(18指定的是带有权重的18层,包括卷积层和全连接层,不包括池化层和BN层)
(Resnet网络的详细介绍之后也许会单独出一篇进行介绍,这里就先不细说了,简单来说它是一种有所改进的CNN网络)
下载resnet18的网络模型以及其预训练模型。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 选择训练模式
# pretrained=True 使用预训练模型
# 使用resnet18模型
transfer_model = models.resnet18(pretrained=True)
for param in transfer_model.parameters():
# 屏蔽预训练模型的权重,只训练最后一层的全连接的权重
param.requires_grad = False
# 修改最后一层维数,即把原来的全连接层替换成输出维数为2的全连接层
# 提取fc层中固定的参数
dim = transfer_model.fc.in_features
# 设置网络中的全连接层为2
transfer_model.fc = nn.Linear(dim, 2)
# 构建神经网络
net = transfer_model.to(device)
复制代码
由于我们要解决的是分类问题,而且是二分类问题,我们需要将全连接层的输出设置为2。其他的网络结构我们保留不同就行。
六、设置训练参数
input_size = 224
batch_size = 128 # 一次训练所选取的样本数(直接影响到GPU内存的使用情况)
save_path = './weights.pt' # 训练参数储存地址
lr = 1e-3 # 学习率(后面用)
n_epoch = 10 # 训练次数(后面用)
复制代码
设置训练需要的参数:
input_size: 输入图片大小 (裁剪成这样的正方形)
batch_size:一次训练所选取的样本数(直接影响到GPU内存的使用情况)
save_path:训练参数储存地址
lr:学习率
n_epoch = 10:训练次数
七、开始训练
def train(net, optimizer, device, criterion, train_loader):
"""训练"""
net.train()
batch_num = len(train_loader)
running_loss = 0.0
for i, data in enumerate(train_loader, start=1):
# 将输入传入GPU(CPU)
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
# 参数梯度置零、向前、反向、优化
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 计算误差并显示
running_loss += loss.item()
if i % 10 == 0:
print('batch:{}/{} loss:{:.3f}'.format(i, batch_num, running_loss / 20))
running_loss = 0.0
复制代码
- optimizer.zero_grad():梯度置零(因为梯度计算是累加的)。
- outputs = net(inputs):向前传播,求出预测值。
- loss = criterion(outputs, labels):计算损失。
- loss.backward():反向传播,计算当前梯度。
- optimizer.step() :根据梯度更新网络参数。
大体上没什么好说的,就是一个按批次投入数据,进行运算,计算出损失函数并进行反向传递,更新网络参数,最终逐渐收敛出猫和狗的图像特征的过程。
八、验证函数
def validate(net, device, val_loader):
"""验证函数"""
net.eval() # 测试,需关闭dropout
correct = 0
total = 0
with torch.no_grad():
for data in val_loader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('测试图像的网络精度: %d %%' %
(100 * correct / total))
复制代码
在验证中,要注意的是需要用net.eval()启动验证模式,关闭dropout,否则,会改变我们已经训练好的网络,导致网络被破坏。
九、开始训练
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.fc.parameters(), lr=lr)
# optimizer = torch.optim.Adam(net.parameters(), lr=lr)
for epoch in range(n_epoch):
print('第{}次训练'.format(epoch+1))
f.train(net, optimizer, device, criterion, train_loader)
f.validate(net, device, val_loader)
# 保存模型参数
torch.save(net.state_dict(), save_path)
复制代码
我选用的优化器是随机梯度下降方法(Adam都使用过,SGD效果略优于其他)。
由于是分类问题,这里选用的是交叉熵损失函数(后面有空也会单独一章进行介绍)。
十、训练结果
该网络单次训练后准确度可达95%,经过十次训练,准确度达到97%。
下面是我使用tk对网络进行了一个简单的封装,结果输出如下: