数据下载

一、下载数据集并创建以下形式文件目录

  train.py: 用于创建并训练模型,并生成训练完成的参数文件。

  setting.py: 用于存放训练配置、超参数,包括学习率,训练次数,裁剪图片大小,每次训练图片数量,参数保存地址。

  train: 存放下载的数据集(共25000张图片,其中猫狗各12500张)。

  func: 自定义包,存放部分操作。

├─setting.py
├─train.py
├─train
│  ├─cat
│  └─dog
└─func
    └─__init__.py

二、拆分数据,划分训练集和验证集

在func目录下新建get_address.py文件:

"""get_address"""
def get_address():
    """返回狗地址列表、猫地址列表、工作目录"""
    import os
    print('{:-^30}'.format('数据集'))
    data_file = os.listdir('./train/')
    print('图片或文件数量:', str(len(data_file)))  # 25000
    dog_file = list(filter(lambda x: x[:3] == 'dog', data_file))
    cat_file = list(filter(lambda x: x[:3] == 'cat', data_file))
    print('狗:', str(len(dog_file)), '\n猫:', str(len(cat_file)))  # 狗:12500 猫:12500
    root = os.getcwd()
    print('工作目录:', root)    # 工作目录: L:\kaggle
    print('{:-^30}'.format(''))
    return dog_file, cat_file, root

  获取猫狗图片的地址并返回。(其实,这里可与下面的arrange.py写在一起,还不太熟练)。

在func目录下新建arrange.py文件:

"""arrange.py"""
def arrange():
    """整理数据,移动图片位置"""
    import shutil
    import os
    from .get_address import get_address
    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('数据整理完成')

  通过此调用函数将train内10%的猫狗图片提取出来,并新建val目录进行存放。原来的train将作为训练集(22500张),val

作为训练集(2500张)。

将上述函数在__init__.py中进行调用(下文类似调用操作省略)

import func.arrange
import func.get_address

arrange = func.arrange.arrange
get_address = func.get_address.get_address

三、获取图片数据并转化

在func目录下新建get_data.py文件:

"""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 中的 ImageFolder 可以直接读取图片集数据(第一个参数决定文件夹地址),但是每个图片大小各异,且需要转化为可识别的数据。需要对读取的图片进行变换操作(即transform参数),除图片缩放外,还需进行归一化处理以减小数据复杂度和方便数据处理。通过transforms.Compose函数,可将这些图片变化操作串联,并通过ImageFolder的调用,快速获取到所需要的数据。

四、将上述操作整合至train.py文件中

填写将所需的参数、设置填写在setting.py中

"""setting.py"""
"""
调整设置""" input_size = 224 # 裁剪图片大小 batch_size = 128 # 一次训练所选取的样本数(直接影响到GPU内存的使用情况) save_path = './weights.pt' # 训练参数储存地址 lr = 1e-3 # 学习率(后面用) n_epoch = 10 # 训练次数(后面用)

填写train.py

import torch
from torchvision import models
from torch import nn
import func as f
from setting import input_size, batch_size, save_path, lr, n_epoch

f.arrange()     # 整理数据,移动图片位置(若已经整理完成可注释)

# 获取文件数据并转换成参数集
transform_train, train_set, train_loader, transform_val, val_set, val_loader = f.get_data(input_size, batch_size)
print('映射关系:', train_set.class_to_idx)  # {'cat': 0, 'dog': 1}
print('训练集长度:', len(train_set.imgs))  # 22500
print('训练集规格:', train_set[1][0].size())  # torch.Size([3, 224, 224])

五、构建卷积神经网络

 使用Resnet18模型(残差网络介绍

device = f.device()     # 选择训练模式(GPU)
print('训练模式:', device, '模式')
# 残差网络(18指定的是带有权重的18层,包括卷积层和全连接层,不包括池化层和BN层)
# 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)

  此处使用Resnet18模型为基础进行训练,使用预训练模型加速训练,使得模型收敛更快。另外,由于我们需要处理的是二元分类问题,全连接层输出维数需为2(损失使用交叉熵损失函数,激活使用softmax)。

在func目录下新建device.py文件:

"""device.py"""
def
device(): """自动选择训练模式,尽可能使用GPU进行运算""" import torch if torch.cuda.is_available(): return torch.device('cuda:0') else: return torch.device('cpu')

  选择训练模式,也可直接使用 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 进行替代。

六、创建训练和验证函数

在func目录下新建train.py文件

"""train.py"""
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() :根据梯度更新网络参数。

在func目录下新建validate.py文件:

"""validate.py"""
def
validate(net, device, val_loader): """验证函数""" import torch 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))

  with torch.no_grad():验证无需进行计算,停止跟踪历史记录和使用内存。

  outputs = net(images):将图片数据通过神经网络,得到输出值。

  _, predicted = torch.max(outputs.data, 1):规格化返回预测值。

七、确定优化器、损失函数,进行训练并保存

# 分类问题——交叉熵损失函数
criterion = nn.CrossEntropyLoss()
# 优化器——随机梯度下降
# 学习率lr=10^-3;
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)

  保存的模型参数储存在save_path地址中

  单次训练后准确度可达95%,经过十次训练,准确度达到97%。

八、单图片验证,并进行可视化操作

在根目录新建两个文件tk.py和test.py:

"""test.py"""
def test():
    from PIL import Image
    import torch
    from torchvision import models
    from torch import nn
    from setting import input_size, save_path
    from torchvision import transforms

    # ------------------------ 加载数据 --------------------------- #
    # 定义预训练变换
    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])
    ])

    class_names = ['0', '180', '270', '90']  # 这个顺序很重要,要和训练时候的类名顺序一致

    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    # ------------------------ 载入模型并且训练 --------------------------- #
    transfer_model = models.resnet18(pretrained=True)
    for param in transfer_model.parameters():
        param.requires_grad = False
    dim = transfer_model.fc.in_features
    transfer_model.fc = nn.Linear(dim, 2)
    # 构建神经网络
    net = transfer_model.to(device)
    net.load_state_dict(torch.load(save_path))
    net.eval()

    image_PIL = Image.open(r'test/image')

    image_tensor = transform_val(image_PIL)
    # 以下语句等效于 image_tensor = torch.unsqueeze(image_tensor, 0)
    image_tensor.unsqueeze_(0)
    # 没有这句话会报错
    image_tensor = image_tensor.to(device)

    out = net(image_tensor)
    # 得到预测结果,并且从大到小排序
    _, indices = torch.sort(out, descending=True)
    # 返回每个预测值的百分数
    percentage = torch.nn.functional.softmax(out, dim=1)[0] * 100

    if percentage[0] > percentage[1]:
        out = '此图片有{:>4.1f}%可能是只猫'.format(percentage[0])
    else:
        out = '此图片有{:>4.1f}%可能是只狗'.format(percentage[1])

    return out
"""tk.py"""
from tkinter import *
from tkinter import filedialog
from PIL import Image, ImageTk


if __name__ == "__main__":
    root = Tk()
    root.title('请选择图片')
    frame = Frame(root, bd=2, relief=SUNKEN)
    frame.grid_rowconfigure(0, weight=1)
    frame.grid_columnconfigure(0, weight=1)
    canvas = Canvas(frame, bd=0)
    canvas.grid(row=0, column=0, sticky=N+S+E+W)
    frame.pack(fill=BOTH, expand=1)

    def printcoords():
        import shutil, os
        File = filedialog.askopenfilename(parent=root, initialdir=os.getcwd(), title='选择图片.')
        img = Image.open(File)
        out = img.resize((336, 192), Image.ANTIALIAS)  # resize image with high-quality
        filename = ImageTk.PhotoImage(out)
        canvas.image = filename
        canvas.create_image(0, 0, anchor='nw', image=filename)
        # print(File)

        dir = os.getcwd() + r'\test'
        shutil.rmtree(dir, True)
        os.makedirs(os.path.join(os.getcwd(), 'test'))

        shutil.copyfile(File, dir + r'\image')
        from test import test
        result = test()
        print(result)
        root.title(result)
    Button(root, text='选择', command=printcoords).pack()
    root.mainloop()

  这部分不是重点,就不细说了。运行tk.py结果如下:

kaggle——猫狗识别(pytorch)

 思路参考             相关代码/Github地址