[深度学习-Pytorch]Resnet18-猫狗识别

一、前言

猫狗识别是CNN网络一个入门级的任务,通过实现猫狗识别,可以更好的了解CNN网络的结构以及运行效果,更可贵的是,猫狗识别实现简单,效果显著,可以很好的激发学习动力。

Dogs vs. Cat | kaggle连接:www.kaggle.com/c/dogs-vs-c…

二、准备数据集

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对网络进行了一个简单的封装,结果输出如下:

image.png

项目地址:github.com/1224667889/…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享