PyTorch框架实战系列(1)——CNN图像分类器

在研究深度学习技术的过程中,选择了PyTorch框架作为技能树的基础。在经过几个实际项目的实践后,发现不仅需要继续巩固数学基础,还需要研究更复杂模型在框架中对应的高级实现。于是决定精研PyTorch框架,计划出一个框架实战系列,以官方教程中的实例为基础,以工程化方法实现。

本系列作为个人研究学习记录,不考虑让初学者看懂。

官方教程:https://pytorch.org/tutorials/

中文翻译教程:http://pytorch123.com/

中文教程有些翻译比较水,而且相比官网不太全,更新也不及时。建议英文基础比较好的,直接看官方原版教程。

 

本例教程:https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#

中文教程:http://pytorch123.com/SecondSection/training_a_classifier/

1、使用torchvision加载并且归一化CIFAR10的训练和测试数据集

# -*- coding: utf-8 -*-
from torchvision import datasets, transforms

data_path = './data'    # 数据集

# 数据集转化为张量,并标准化
# input[channel] = (input[channel] - mean[channel]) / std[channel]
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# transform = None
# 下载数据集
trainset = datasets.CIFAR10(root=data_path, train=True, download=True, transform=transform)
testset = datasets.CIFAR10(root=data_path, train=False, download=True, transform=transform)

# 查看数据集大小
print('trainset', len(trainset))
print('testset', len(testset))

CIFAR10数据集保存的是PIL.Image格式数据,图片尺寸32*32,RGB三通道,可以通过注释transform查看数据集的数据类型。

使用transforms先转化成PyTorch模型使用的张量[3, 32, 32],其数值范围为[0, 1],模型处理需要将其归一化为[-1, 1]的数值范围。Normalize第一个参数为每个通道的平均值列表,第二个参数为每个通道的标准差列表,transforms按下列公式做归一化处理:

input[channel] = (input[channel] - mean[channel]) / std[channel]

2、构造迭代器

from torch.utils.data import DataLoader

batch_size = 10    # mini-batch
# 构造迭代器
trainloader = DataLoader(dataset=trainset, batch_size=batch_size, shuffle=True)
testloader = DataLoader(dataset=testset, batch_size=batch_size, shuffle=True)

# 迭代器输出的张量
for train, label in trainloader:
    print(train.size(), label.size())
    break

DataLoader可以很方便的通过数据集构造训练用的数据迭代器,batch_size指每次输入的样本个数,shuffle可以打乱样本顺序。查看一下迭代器每次输出的数据集和标签的张量:

torch.Size([10, 3, 32, 32]) torch.Size([10])

可以看到数据集张量的第四维度变成了样本的个数。

3、定义一个卷积神经网络

新建模型定义模块:CNN.py

使用教程定义的模型:

import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

这个模型效果一般,主要是超参数的设置不太合理,后面再优化修改,本例主要感受模型训练损失的变化。

4、定义损失函数和优化器

import torch.optim as optim
import torch.nn as nn
from CNN import Net

net = Net()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

损失函数选择交叉熵损失函数,优化器使用Adam,相比教程中的SGD收敛效果要好一些。

5、训练模型

# 在训练集上循环训练
for epoch in range(2):
    # 损失值
    running_loss = 0.0
    for i, data in enumerate(trainloader):
        # 获得输入张量和标签
        inputs, labels = data
        # 模型梯度归零,否则会不断累积。optimizer.zero_grad()也可以
        net.zero_grad()
        # 数据经过模型计算
        outputs = net(inputs)
        # 计算模型预测与实际标签的损失值
        loss = criterion(outputs, labels)
        # 梯度自动反向传播
        loss.backward()
        # 优化器执行优化,按梯度下降原则调整参数
        optimizer.step()
        
        running_loss += loss.item()
        # 每1000轮输出一次损失值
        if (i+1) % 1000 == 0:
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 1000))
            running_loss = 0.0

print('Finished Training')

输出:

[1,  1000] loss: 1.854
[1,  2000] loss: 1.553
[1,  3000] loss: 1.464
[1,  4000] loss: 1.411
[1,  5000] loss: 1.361
[2,  1000] loss: 1.299
[2,  2000] loss: 1.276
[2,  3000] loss: 1.234
[2,  4000] loss: 1.226
[2,  5000] loss: 1.192
Finished Training

可以看到loss在收敛,说明训练是有效果的。但因为模型的原因,还有训练次数不够,没有达到最佳效果。可以通过改进模型、增加训练次数和增加验证集的方式改进训练。

6、测试评估模型效果

import torch
import numpy as np
from sklearn import metrics

predict_all = np.array([], dtype=int)
labels_all = np.array([], dtype=int)
# 测试预测时取消自动梯度,即向前计算时不自动求导
with torch.no_grad():
    for images, labels in testloader:
        outputs = net(images)
        labels = labels.data.numpy()
        predicted = torch.max(outputs.data, 1)[1].numpy()
        labels_all = np.append(labels_all, labels)
        predict_all = np.append(predict_all, predicted)

confusion = metrics.confusion_matrix(labels_all, predict_all)
print(u'混淆矩阵:')
print(confusion)
# 验证集准确度
acc = metrics.accuracy_score(labels_all, predict_all)
print(u'10000测试集图片的准确率: {0:.2%}'.format(acc))

可以先注释训练过程,看一下未训练的初始化模型的测试结果:

混淆矩阵:
[[   0  995    0    0    0    0    5    0    0    0]
 [   0  991    0    0    0    0    9    0    0    0]
 [   0  995    0    0    0    0    5    0    0    0]
 [   0  999    0    0    0    0    1    0    0    0]
 [   0 1000    0    0    0    0    0    0    0    0]
 [   0  996    0    0    0    0    4    0    0    0]
 [   0 1000    0    0    0    0    0    0    0    0]
 [   0  996    0    0    0    0    4    0    0    0]
 [   0  990    0    0    0    0   10    0    0    0]
 [   0  992    0    0    0    0    8    0    0    0]]
10000测试集图片的准确率: 9.91%

结果训练后的测试结果:

混淆矩阵:
[[629  46  84  20  24  17  15  44  75  46]
 [ 37 701  10  13  16  10  20  34  13 146]
 [ 58   9 381  47 159 157  97  74  10   8]
 [ 16  13  77 244  68 327 160  62  15  18]
 [ 20   3 103  35 457  94 136 135  12   5]
 [ 11   2  72  91  68 598  76  65   6  11]
 [  8   9  41  47 102  47 709  27   4   6]
 [ 11   3  42  36  62 134  24 670   4  14]
 [211  88  47  21   8  18   9  13 526  59]
 [ 49 132  13  20  10  12  47  65  23 629]]
10000测试集图片的准确率: 55.44%

可以看到准确率从10%到55%,得到了提升。说明模型经过训练的确是学到了东西。

 

完整代码如下:train.py

# -*- coding: utf-8 -*-
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import torch.optim as optim
import torch.nn as nn
from CNN import Net
import torch
import numpy as np
from sklearn import metrics


data_path = './data'    # 数据集

# 数据集转化为张量,并标准化
# input[channel] = (input[channel] - mean[channel]) / std[channel]
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# transform = None
# 下载数据集
trainset = datasets.CIFAR10(root=data_path, train=True, download=True, transform=transform)
testset = datasets.CIFAR10(root=data_path, train=False, download=True, transform=transform)

# 查看数据集大小
print('trainset', len(trainset))
print('testset', len(testset))

batch_size = 10    # mini-batch
# 构造迭代器
trainloader = DataLoader(dataset=trainset, batch_size=batch_size, shuffle=True)
testloader = DataLoader(dataset=testset, batch_size=batch_size, shuffle=True)

# 迭代器输出的张量
for train, label in trainloader:
    print(train.size(), label.size())
    break

# 模型实例化,指定损失函数和优化器,学习率0.001
net = Net()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

# 未经训练的模型测试效果
predict_all = np.array([], dtype=int)
labels_all = np.array([], dtype=int)
# 测试预测时取消自动梯度,即向前计算时不自动求导
with torch.no_grad():
    for images, labels in testloader:
        outputs = net(images)
        labels = labels.data.numpy()
        predicted = torch.max(outputs.data, 1)[1].numpy()
        labels_all = np.append(labels_all, labels)
        predict_all = np.append(predict_all, predicted)

confusion = metrics.confusion_matrix(labels_all, predict_all)
print(u'混淆矩阵:')
print(confusion)
# 验证集准确度
acc = metrics.accuracy_score(labels_all, predict_all)
print(u'10000测试集图片的准确率: {0:.2%}'.format(acc))

# 在训练集上循环训练
for epoch in range(2):
    # 损失值
    running_loss = 0.0
    for i, data in enumerate(trainloader):
        # 获得输入张量和标签
        inputs, labels = data
        # 模型梯度归零,否则会不断累积。optimizer.zero_grad()也可以
        net.zero_grad()
        # 数据经过模型计算
        outputs = net(inputs)
        # 计算模型预测与实际标签的损失值
        loss = criterion(outputs, labels)
        # 梯度自动反向传播
        loss.backward()
        # 优化器执行优化,按梯度下降原则调整参数
        optimizer.step()

        running_loss += loss.item()
        # 每1000轮输出一次损失值
        if (i+1) % 1000 == 0:
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 1000))
            running_loss = 0.0

print('Finished Training')

# 训练后的模型测试效果
predict_all = np.array([], dtype=int)
labels_all = np.array([], dtype=int)
# 测试预测时取消自动梯度,即向前计算时不自动求导
with torch.no_grad():
    for images, labels in testloader:
        outputs = net(images)
        labels = labels.data.numpy()
        predicted = torch.max(outputs.data, 1)[1].numpy()
        labels_all = np.append(labels_all, labels)
        predict_all = np.append(predict_all, predicted)

confusion = metrics.confusion_matrix(labels_all, predict_all)
print(u'混淆矩阵:')
print(confusion)
# 验证集准确度
acc = metrics.accuracy_score(labels_all, predict_all)
print(u'10000测试集图片的准确率: {0:.2%}'.format(acc))

后期个人对此模型进行了优化,详见:PyTorch框架实战系列(2)——图像分类器优化

 

问题:

PyTorch当完成训练后,程序并不能自动停止。只要使用backward()函数反向传播计算梯度,进程就不会自动释放,而且不管是点stop按钮,还是加quit(0)或sys.exit(0)都不管用,每次必须用资源管理强制杀掉python的进程。

一直没有解决这个问题,网上也没找到实际能够解决的方法。简直逼死强迫症!

如果有哪位解决了这个问题,请留言分享一下,万分感谢。

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值