前言

本文记录在学习pytorch的一些心得

手写数字识别的数据来自pytorch的自带数据集中 api是 torchvision.datasets.MNIST(root='/files/', train=True, download=True, transform=)

参数有

  1. root参数表示数据存放的位置
  2. train:bool类型,表示是使用训练集的数据还是测试集的数据
  3. download:bool类型,表示是否需要下载数据到root目录
  4. transform:实现的对图片的处理函数

而MNIST是由Yann LeCun等人提供的免费的图像识别的数据集,其中包括60000个训练样本和10000个测试样本,其中图拍了的尺寸已经进行的标准化的处理,都是黑白的图像,大小为28X28

数据加载知识的了解

当我们获取了这个数据集时,我们还需要了解一下torch的数据加载是个什么流程

在torch中提供了数据集的基类torch.utils.data.Dataset,继承这个基类,我们能够进行数据的快速加载

源码如下:

class Dataset(object):
"""An abstract class representing a Dataset.

All other datasets should subclass it. All subclasses should override
``__len__``, that provides the size of the dataset, and ``__getitem__``,
supporting integer indexing in range from 0 to len(self) exclusive.
"""

def __getitem__(self, index):
raise NotImplementedError

def __len__(self):
raise NotImplementedError

def __add__(self, other):
return ConcatDataset([self, other])

可知:我们需要在自定义的数据集类中继承Dataset类,同时还需要实现两个方法:

  1. __len__方法,能够实现通过全局的len()方法获取其中的元素个数
  2. __getitem__方法,能够通过传入索引的方式获取数据,例如通过dataset[i]获取其中的第i条数据

这里可以写个例子

数据加载案例

读取SMS Spam Collection

数据来源:http://archive.ics.uci.edu/ml/datasets/SMS+Spam+Collection

数据介绍:SMS Spam Collection是用于骚扰短信识别的经典数据集,完全来自真实短信内容,包括4827条正常短信和747条骚扰短信,总数据条数 5574。

正常短信和骚扰短信保存在一个文本文件中。 每行完整记录一条短信内容,每行开头通过ham和spam标识正常短信和骚扰短信

数据实例:

mark

实现读取代码如下:

from torch.utils.data import Dataset, DataLoader
import pandas as pd

data_path = './data/SMSSpamCollection'


class my_data_set(Dataset):
def __init__(self):
self.lines = open(data_path, encoding='utf-8').readlines()

def __getitem__(self, index):
line_cut = self.lines[index].strip()
# 分割前4个字符 -->ham\t或者spam
lable = line_cut[:4].strip()
# 后面全部字-->具体内容
content = line_cut[4:].strip()
return lable, content

def __len__(self):
return len(self.lines)


if __name__ == '__main__':
data = my_data_set()
print(len(data))#5574

但是这样的数据加载在很多时候不能满足我们的需要

我们有时候需要打乱数据,同时对于数据进行分块

这个时候我们就需要torch.utils.data.DataLoader这个api了

使用api过程如下

dataload = DataLoader(dataset=data, batch_size=10, shuffle=True, num_workers=2, drop_last=True)
  1. dataset:提前定义的dataset的实例
  2. batch_size:传入数据的batch的大小,常用128,256等等
  3. shuffle:bool类型,表示是否在每次获取数据的时候提前打乱数据
  4. num_workers:加载数据的线程数(一般可以写作0,当开启时可能报错,报错内容可以百度解决也可以参考这个博客 https://www.cnblogs.com/20183544-wangzhengshuai/p/14814459.html)
  5. drop_last:丢弃最后一个不足batch_size的数据

当我们数据处理完成后,我们可以进行打印观察

for index, (lable, content) in enumerate(dataload):
print(index, lable, content)
print("*" * 100)

运行结果如图

mark

我们把10个数据当做一块输入,所以我们输出数据也就是10个ham或spam拼成的一条数据

同时我们设置了drop_last这样我们重新加载出的数据条数只有556条

mark

因为总数据是5574,最后4个数据无法满足batch_size的大小,就被舍弃了

这样我们的数据就加载完成了

这也是为我们的手写数字识别案例的数据加载做了前置知识准备

手写数字识别案例

我们可以先加载一下数字识别案例的数据集看看是什么样子的

from torchvision.datasets import MNIST

data=MNIST(root='data',train=True,download=False)
print(data[0])#(<PIL.Image.Image image mode=L size=28x28 at 0x2492DA59408>, 5) 图片数据 和 目标值
img=data[0][0]
img.show()

运行会发现打开了一张图片,显示为5

mark

之前我们在写MNIST的api的时候写到了transforms参数,这里我们需要知道transforms的图形处理方法

一般用到的有这3个api:

  1. ToTensor:作用把一个取值范围是[0,255]PIL.Image或者shape(H,W,C)numpy.ndarray,转换成形状为[C,H,W],其中(H,W,C)意思为(高,宽,通道数),黑白图片的通道数只有1,其中每个像素点的取值为[0,255],彩色图片的通道数为(R,G,B),每个通道的每个像素点的取值为[0,255],三个通道的颜色相互叠加,形成了各种颜色
  2. Normalize:作用把数据标准化
  3. Compose:作用将多个transform组合起来使用。

下面分别举例子来使用上面这些api

为了模拟和数据集相似的数据类型,所以在这个例子中数据通道也为1

import numpy as np
from torchvision import transforms
#随机生成数据
data=np.random.randint(0,255,size=6)
#定义数据形状
img=data.reshape(1,2,3)
print(img)
print("------------")
#转换数据形状
img=transforms.ToTensor()(img)
print(img)
#标准化处理
for x,y in enumerate(img):
y=y.float()
y_mean=y.mean().item()
y_std= y.std().item()
#这里的mean和std都是直接从数据中求得的,但是要注意数据通道为几,normalize的参数就要写多少次
#例如通道为3-->Normalize((10,10,10), (1,1,1))(img)
nor_img=transforms.Normalize((y_mean),(y_std))(img.float())
print(nor_img)

结果为:

原数据 [[[172  47 117]
[192 67 251]]]
------------
变化过的 tensor([[[172, 192]],

[[ 47, 67]],

[[117, 251]]], dtype=torch.int32)
标准化后的 tensor([[[-0.1266, 0.0844]],

[[-1.4459, -1.2348]],

[[-0.7071, 0.7071]]])

这样我们差不多就对于数据的处理就有了前置的知识准备

准备数据

def data_load(train=True,BATCH_SIZE=BATCH_SIZE):
# 构建dataset
dataset = MNIST(root='./data', train=train, transform=torchvision.transforms.Compose(
# 转换数据形状和标准化数据
[torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean=(0.1307,), std=(0.3081,))
]
))
# 打乱分割数据
data_loader = DataLoader(dataset=dataset, batch_size=BATCH_SIZE, shuffle=True)
# 返回处理好的数据集
return data_loader

这里的0.1307和0.3081都是之前算出来的,计算结果大概如下:

import torchvision.transforms
from torchvision.datasets import MNIST
from torch.utils.data import Dataset, DataLoader

transforms = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
# 加载数据
train_data = MNIST('data', train=True, download=False, transform=transforms)
# 数据打乱,分块 60000是为了一次性读取所有的数据分成1块 原数据大概是5w9条数据
train_loader = DataLoader(dataset=train_data, shuffle=True, batch_size=60000)
for batch_idx, data in enumerate(train_loader):
inputs, targets = data
x = inputs.view(-1, 28 * 28)
x_std = x.std().item()
x_mean = x.mean().item()
print(x_std)
print(x_mean)

结果为:

0.30810782313346863
0.13066047430038452

构建模型

模型中使用全连接层:

当前一层的神经元和前一层的神经元相互链接,其核心操作就是y=wxy = wx,即矩阵的乘法,实现对前一层的数据的变换

模型的构建使用了一个三层的神经网络,其中包括两个全连接层和一个输出层,第一个全连接层会经过激活函数的处理,将处理后的结果交给下一个全连接层,进行变换后输出结果

这里补充一下激活函数的api

Relu激活函数由import torch.nn.functional as F提供,F.relu(x)即可对x进行处理

模型中数据形状

原数据形状在经过数据加载处理后是:[batch_size,1,28,28]

进行形状的修改:[batch_size,28*28] ,(全连接层是在进行矩阵的乘法操作,所以我们要转换数据形状)

第一个全连接层的输出形状:`[batch_size,28]

激活函数不会修改数据的形状

第二个全连接层的输出形状:[batch_size,10],因为手写数字有10个类别

构建模型代码

# 构建模型
class mnist_module(nn.Module):
def __init__(self):
# 继承父类
super(mnist_module, self).__init__()
# 定义线性模型的输入和输出特征
self.fc1 = nn.Linear(28 * 28 * 1, 28)
self.fc2 = nn.Linear(28, 10)

# 实现forward方法
def forward(self, input):
# 数据形状变换
X = input.view(-1, 1 * 28 * 28)
# 第一层全连接层
X = self.fc1(X)
# 激活函数处理后输入第二层
X = F.relu(X)
# 第二层全连接层
out = self.fc2(X)
# 返回out-->为损失计算做准备 也可以写成 return F.log_softmax(out, dim=-1) 不过后续计算损失算法不同
return out

损失函数

当前我们手写字体识别的问题是一个多分类的问题,所谓多分类对比的是之前学习的2分类

我们在逻辑回归中,我们使用sigmoid进行计算对数似然损失,来定义我们的2分类的损失。

  • 在2分类中我们有正类和负类,正类的概率为P(x)=11+ex=ex1+exP(x) = \frac{1}{1+e^{-x}} = \frac{e^x}{1+e^x},那么负类的概率为1P(x)1-P(x)

  • 将这个结果进行计算对数似然损失ylog(P(x))-\sum y log(P(x))就可以得到最终的损失

而在多分类的过程中我们不能够再使用sigmoid函数来计算当前样本属于某个类别的概率,而应该使用softmax函数。softmax和sigmoid的区别在于我们需要去计算样本属于每个类别的概率,需要计算多次,而sigmoid只需要计算一次

softmax的公式如下:

σ(z)j=ezjk=1KezK,j=1k\sigma(z)_j = \frac{e^{z_j}}{\sum^K_{k=1}e^{z_K}} ,j=1 \cdots k

对于这个softmax输出的结果,是在[0,1]区间,我们可以把它当做概率

和前面2分类的损失一样,多分类的损失只需要再把这个结果进行对数似然损失的计算即可

J=Ylog(P),其中P=ezjk=1KezK,Y表示真实值\begin{aligned} & J = -\sum Y log(P) &, 其中 P = \frac{e^{z_j}}{\sum^K_{k=1}e^{z_K}} ,Y表示真实值 \end{aligned}

最后,会计算每个样本的损失,即上式的平均值

我们把softmax概率传入对数似然损失得到的损失函数称为交叉熵损失

在pytorch中有两种方法实现交叉熵损失

criterion = nn.CrossEntropyLoss()
loss = criterion(input,target)
#1. 对输出值计算softmax和取对数
output = F.log_softmax(x,dim=-1)
#2. 使用torch中带权损失
loss = F.nll_loss(output,target)

带权损失定义为:ln=wixil_n = -\sum w_{i} x_{i},其实就是把log(P)log(P)作为xix_i,把真实值Y作为权重

训练模型

  1. 实例化模型,设置模型为训练模式
  2. 实例化优化器类,实例化损失函数
  3. 获取,遍历dataloader
  4. 梯度置为0
  5. 进行向前计算(反向传播)
  6. 计算损失
  7. 反向传播
  8. 更新参数
# 实例化模型
model = mnist_module()
# 实例优化器
optim = Adam(model.parameters(), lr=lr)
#实例化损失对象
criterion = nn.CrossEntropyLoss()

# 训练模型
def train(epoch):
# 加载数据
dataload = data_load(BATCH_SIZE=BATCH_SIZE)
# 遍历数据集
for idx, (input, target) in enumerate(dataload):
# 梯度置为0
optim.zero_grad()
# 得到预测值 input的形状是[batch_size, 1, 28, 28] output的形状是[batch_size,10]
output = model(input)
# 得到loss 交叉熵loss
# loss = F.nll_loss(output, target)
loss=criterion(output,target)
# 反向传播
loss.backward()
# 更新梯度
optim.step()
# 查看数据
if idx % 10 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, idx * len(input), len(dataload.dataset),
100 * idx / len(dataload), loss.item()))

模型保存和加载

当我们模型训练成功肯定是要保存模型数据的

保存模型api如下:

torch.save(model.state_dict(), "./model/model.pk1")
torch.save(optim.state_dict(), "./model/optimizer.pk1")

加载模型api如下:

model.load_state_dict(torch.load("./model/model.pk1"))
optim.load_state_dict(torch.load("./model/optimizer.pk1"))

模型评估

评估的过程和训练的过程相似,但是:

  1. 不需要计算梯度
  2. 需要收集损失和准确率,用来计算平均损失和平均准确率
  3. 损失的计算和训练时候损失的计算方法相同
  4. 准确率的计算:
    • 模型的输出为[batch_size,10]的形状
    • 其中最大值的位置就是其预测的目标值(预测值进行过sotfmax后为概率,sotfmax中分母都是相同的,分子越大,概率越大)
    • 最大值的位置获取的方法可以使用torch.max,返回最大值和最大值的位置
    • 返回最大值的位置后,和真实值([batch_size])进行对比,相同表示预测成功

代码如下:

def test():
test_dataload = data_load(train=False)
loss_dict = []
acc_dict = []
for idx, (input, target) in enumerate(test_dataload):
with torch.no_grad():
output = model(input)
# 计算损失
cur_loss = criterion(output, target)
loss_dict.append(cur_loss)
# 计算准确率
predict = output.max(dim=-1)[-1]
cur_acc = predict.eq(target).float().mean()
acc_dict.append(cur_acc)
print("平均准确率{},平均损失{}".format(np.mean(acc_dict), np.mean(loss_dict)))

完整流程

完整代码如下:

import numpy as np
import torch
import os
import torchvision.transforms
from torchvision.datasets import MNIST
from torch.utils.data import DataLoader
from torch import nn
import torch.nn.functional as F
from torch.optim import Adam

BATCH_SIZE = 100
TEST_BATCH_SIZE = 1000
lr = 0.001


# 准备数据
def data_load(train=True, BATCH_SIZE=BATCH_SIZE):
# 构建dataset
dataset = MNIST(root='./data', train=train, transform=torchvision.transforms.Compose(
# 转换数据形状和标准化数据
[torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean=(0.1307,), std=(0.3081,))
]
))
# 打乱分割数据
data_loader = DataLoader(dataset=dataset, batch_size=BATCH_SIZE, shuffle=True)
# 返回处理好的数据集
return data_loader


# 构建模型
class mnist_module(nn.Module):
def __init__(self):
# 继承父类
super(mnist_module, self).__init__()
# 定义线性模型的输入和输出特征
self.fc1 = nn.Linear(28 * 28 * 1, 28)
self.fc2 = nn.Linear(28, 10)

# 实现forward方法
def forward(self, input):
# 数据形状变换
X = input.view(-1, 1 * 28 * 28)#[batch_size,28*28]
# 第一层全连接层
X = self.fc1(X)#[batch_size,28]
# 激活函数处理后输入第二层
X = F.relu(X) #[batch_size,28]
# 第二层全连接层
out = self.fc2(X)#[batch_size,10]
# 返回out
# 也可以写成return F.log_softmax(out, dim=-1)
return out


# 实例化模型
model = mnist_module()
# 实例优化器
optim = Adam(model.parameters(), lr=lr)
# 实例化损失对象
criterion = nn.CrossEntropyLoss()
# 原有模型加载
if os.path.exists("./model/model.pk1"):
model.load_state_dict(torch.load("./model/model.pk1"))
optim.load_state_dict(torch.load("./model/optimizer.pk1"))


# 训练模型
def train(epoch):
# 加载数据
dataload = data_load(BATCH_SIZE=BATCH_SIZE)
# 遍历数据集
for idx, (input, target) in enumerate(dataload):
# 梯度置为0
optim.zero_grad()
# 得到预测值 input的形状是[batch_size, 1, 28, 28] output的形状是[batch_size,10]
output = model(input)
# 得到loss 交叉熵loss
# loss = F.nll_loss(output, target)
loss = criterion(output, target)
# 反向传播
loss.backward()
# 更新梯度
optim.step()
# 查看数据
if idx % 10 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, idx * len(input), len(dataload.dataset),
100 * idx / len(dataload), loss.item()))
# 保存训练模型
if idx % 100 == 0:
# 模型保存
torch.save(model.state_dict(), "./model/model.pk1")
torch.save(optim.state_dict(), "./model/optimizer.pk1")


def test():
test_dataload = data_load(train=False)
loss_dict = []
acc_dict = []
for idx, (input, target) in enumerate(test_dataload):
with torch.no_grad():
output = model(input)
# 计算损失
cur_loss = criterion(output, target)
loss_dict.append(cur_loss)
# 计算准确率
predict = output.max(dim=-1)[-1]
cur_acc = predict.eq(target).float().mean()
acc_dict.append(cur_acc)
print("平均准确率{},平均损失{}".format(np.mean(acc_dict), np.mean(loss_dict)))


if __name__ == '__main__':
test()
#训练3轮
for i in range(3):
train(i)
test()

运行结果部分截图

mark