第十四周:pytorch入门实践+深度学习基础
摘要
本周我使用PyTorch框架成功实现了手写数字识别模型。手写数字识别是计算机视觉领域的重要任务之一,具有广泛的应用前景。在本文中,我将介绍如何使用PyTorch构建一个简单的神经网络模型来识别手写数字。首先,我将解释数据集的准备过程,然后详细讲解模型的架构和参数设置。接下来,我会介绍训练过程,包括损失函数的选择和优化器的使用。最后,我会展示一些测试结果。通过这个项目,可以了解如何使用PyTorch构建和训练一个简单但有效的手写数字识别模型以及对于pytorch中的数据加载、迭代数据集。
Abstract
This week I successfully implemented a handwritten digit recognition model using the PyTorch framework. Handwritten digit recognition is one of the important tasks in the field of computer vision and has a wide range of applications. In this article, I will introduce how to build a simple neural network model to recognize handwritten digits using PyTorch. First, I will explain the process of preparing the dataset, and then explain in detail the architecture and parameter settings of the model. Next, I will introduce the training process, including the choice of loss function and the use of optimizer. Finally, I will show some test results. Through this project, you can understand how to use PyTorch to build and train a simple but effective handwritten digit recognition model as well as for data loading, iterating dataset in pytorch.
1. pytorch中的数据加载
在torch中提供了数据集的基类 torch.utils.data.Dataset,继承这个基类,我们能够非常快速的实现对数据的加载。
torch.utils.data.Dataset 的源码如下:
```python
class Dataset(object):
def __getitem__(self, index):
raise NotImplementedError
def __len__(self):
raise NotImplementedError
def __add__(self, other):
return ConcatDataset([self, other])
可知: 我们需要在自定义的数据集中继承 Dataset 类,同时还需要实现两个方法:
- __ len __ 方法,能够实现通过全局的 len() 方法获取其中的元素个数
- __ getitem __ 方法,能够通过传入索引的方式获取数据,例如通过 dataset[i] 获取其中的第 i 条数据
2. 数据加载案例(Dataset)
下面通过一个例子来看看如何使用 Dataset 来加载数据。
数据来源:http://archive.ics.uci.edu/dataset/228/sms+spam+collection
数据介绍:SMS Spam Collection 是用于骚扰短信识别的经典数据集,完全来自真实短信内容。每行完整记录一条短信内容,每行开头通过ham和spam标识正确短信和骚扰短信。
数据实例:
之后对 Dataset 进行实例化,可以获取其中的数据,实现如下:
import torch
from torch.utils.data import Dataset
data_path = r"C:\Users\10245\Desktop\pytorch练习\数据集实例\data\SMSSpamCollection"
# 完成数据集
class MyDataset(Dataset):
def __init__(self):
self.lines = open(data_path, encoding= 'utf-8').readlines()
def __getitem__(self, index):
# 获取索引对应位置的一条数据
cur_line = self.lines[index].strip()
label = cur_line[:4].strip()
content = cur_line[4:].strip()
return label, content
def __len__(self):
# 返回数据的总数量
return len(self.lines)
if __name__ == '__main__':
my_dataset = MyDataset()
print(my_dataset[0])
print(len(my_dataset))
输出如下:
但是在深度学习中,数据量通常非常多,非常大的,如此大的数据量,不可能一次性的在模型中进行向前的计算和反向传播,经常我们会对整个数据进行随机的打乱顺序,把数据处理成一个个的batch,同时还会对数据进行预处理。
3. 迭代数据集(DataLoader)
使用上述的方法能够进行数据的读取,但是其中还有很多内容没有实现:
- 批处理数据(Batching the data)
- 打乱数据(Shuffling the data)
- 使用多线程 multiprocessing 并行加载数据
在pytorch中 torch.utils.data.DataLoader 提供了上述的所用方法
DataLoader 的使用方法示例:
import torch
from torch.utils.data import Dataset,DataLoader
data_path = r"C:\Users\10245\Desktop\pytorch练习\数据集实例\data\SMSSpamCollection"
# 完成数据集
class MyDataset(Dataset):
def __init__(self):
self.lines = open(data_path, encoding= 'utf-8').readlines()
def __getitem__(self, index):
# 获取索引对应位置的一条数据
cur_line = self.lines[index].strip()
label = cur_line[:4].strip()
content = cur_line[4:].strip()
return label, content
def __len__(self):
# 返回数据的总数量
return len(self.lines)
my_dataset = MyDataset()
data_loader = DataLoader(dataset=my_dataset, batch_size=2, shuffle=True)
if __name__ == '__main__':
for i in data_loader:
print(i)
其中参数含义:
- dataset:提前定义的 dataset 的实例
- batch_size:传入数据的 batch 的大小,常用128,256等
- shuffle:bool类型,表示是否在每次获取数据的时候提前打乱数据
- num_workers:加载数据的线程数
输出如下:
4. pytorch自带的数据集
pytorch中自带的数据集由两个上层api提供,分别是 torchvision 和 torchtext。
其中:
- torchvision 提供了对图片数据处理相关的api和数据
数据位置:torchvision.datasets,例如:torchvision.datasets.MNIST(手写数字图片数据) - torchtext 提供了对文本数据处理相关的API和数据
数据位置:torchtext.datasets,例如:torchtext.datasets.IMDB(电影评论文本数据)
下面我们以MNIST手写数字为例,来看看pytorch如何加载其中自带的数据集。
使用方法和之前一样:
- 准备好 Dataset 实例
- 把dataset交给dataloader打乱顺序,组成batch
torchvision.datasets中的数据集类都是继承自Dataset,这意味着直接对torchvision.datasets.MNIST进行实例化就可以得到Dataset的实例,但是MNIST API中的参数需要注意一下:
torchvision.datasets.MNIST(root='/files/', train=True, download=True, transform=)
- root:表示数据存放的位置
- train:bool类型,表示使用训练集的数据还是测试集的数据
- download:bool类型,表示是否需要下载数据到root目录
- transform:实现对图片的处理函数
5. MNIST数据集的介绍
数据集的原始地址:http://yann.lecun.com/exdb/mnist/
MNIST是由 Yann LeCun 等人提供的免费的图像识别的数据集,其中包括60000个训练样本和10000个测试样本,其中图的尺寸已经进行了标准化处理,都是黑白图像,大小为28*28
执行代码,下载数据,观察数据类型:
import torch
from torchvision.datasets import MNIST
minst = MNIST(root="./data", train=True, download=False)
print(minst)
print(dataset[0])
下载的数据如下:
6. 使用pytorch实现手写数字识别
6.1 思路
- 准备数据,这需要准备DataLoader
- 构建模型,这里可以使用torch构造一个深层的神经网络
- 模型的训练
- 模型的保存,保存模型,后续持续使用
- 模型的评估,使用测试集,观察模型的好坏
6.2 MNIST数据的处理
准备数据集的方法前面已经了解过了,但是通过前面的内容可知,调用MNIST返回的结果中图形数据是一个image对象,需要对其进行处理。
为了进行数据的处理,接下来我们了解 torchvision.transforms的方法
1). torchvision.transforms.ToTensor
把一个取值范围是 [0,255] 的 PIL.Image或者shape为(H,W,C)的numpy.ndarray,转换成形状为 [C,H,W],取值范围是 [0,1,0] 的 torch.FloatTensor。
其中(H,W,C)意思为(高,宽,通道数),黑白图片的通道数只有1,其中每个像素点的取值为 [0,255],彩色图片的通道数为(R,G,B),每个通道的每个像素点的取值为 [0,255],三个通道的颜色相互叠加,形成了各种颜色。
示例如下:
from torchvision import transforms
import numpy as np
minst = MNIST(root="./data", train=True, download=False)
data = np.random.randint(0,255,size=12)
img = data.reshape(2,2,3)
print(img)
print(img.shape)
img_tensor = transforms.ToTensor()(img)
print(img_tensor)
print(img_tensor.shape)
运行结果:
2). torchvision.transforms.Normalize(mean, std)
给定均值:mean,shape和图片的通道数相同(指的是每个通道的均值),方差:std,和图片的通道数相同(指的是每个通道的方差),将会把 Tensor 规范化处理。
即:Normalized_image = (image - mean) / std
例如:
import torchvision
from torchvision import transforms
import numpy as np
data = np.random.randint(0, 255, size=12)
img = data.reshape(2, 2, 3)
img = transforms.ToTensor()(img).float()
print(img)
print("*" * 100)
norm_img = transforms.Normalize((10, 10, 10), (1, 1, 1))(img)
print(norm_img)
运行结果:
3). torchvision.transforms.Compose(transforms)
将多个transform组合起来使用
transforms.Compose([
torchvision.transforms.ToTensor() #先转化为Tensor
torchvision.transforms.Normalize(mean,std) #再进行正则化
])
6.3 准备MNIST数据集的Dataset和DataLoader
导入项目依赖包
from torchvision.datasets import MNIST
from torchvision.transforms import Compose,Normalize,ToTensor
from torch.utils.data import DataLoader
准备训练集
# 准备数据集,其中0.1307,0.3081为MNIST数据的均值和标准差,这样操作能够对其进行标准化
# 因为MNIST只有一个通道(黑白图片),所以元组中只有一个值
def get_dataloader(train=True):
transform_fn = Compose([
ToTensor(),
Normalize(mean=(0.1307),std=(0.3081))
])
dataset = MNIST(root='./data', train=train, transform=transform_fn)
# 准备数据迭代器
data_loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)
return data_loader
6.4 构建模型
模型的构建使用了一个四层的神经网络,其中包括两个全连接层和一个输出层,第一个全连接层会经过激活函数的处理,将处理后的结果交给下一个全连接层,进行变换后输出结果。
那么在这个模型中有三个地方需要我们注意:
- 激活函数如何使用
- 每一层数据的形状变化
- 模型的损失函数
6.4.1 激活函数的使用
Relu激活函数是由 import torch.nn.functional as F 提供,F.relu(x) 即可对 x 进行处理。
6.4.2 模型中数据的形状(添加形状变化图形)
- 原始输入数据的形状:[batch_size, 1, 28, 28]
- 进行形状的修改:[batch_size, 28*28],(全连接层是在进行矩阵乘法的操作)
- 第一个全连接层的输出形状:[batch_size, 28]
- 激活函数不会修改数据的形状
- 第二个全连接层的输出形状:[batch_size, 10],因为手写数字有10个类别
构建模型的代码如下:
import torch.nn.functional as F
import torch.nn as nn
class MnistModel(nn.Module):
def __init__(self):
super(MnistModel, self).__init__()
self.fc1 = nn.Linear(1*28*28, 28)
self.fc2 = nn.Linear(28, 10)
def forward(self, input):
"""
:param input: [batch_size,1,28,28]
:return:
"""
# 1. 修改形状
x = input.view(input.size(0), 1*28*28)
# 2. 进行全连接的操作
x = self.fc1(x)
# 3. 进行激活函数处理,形状没有变化
F.relu(x)
# 4. 输出层
out = self.fc2(x)
return out
6.5 模型的损失函数
我们需要明确,当我们手写数字识别的问题是一个多分类问题,所谓多分类对比的是之前我们学习的二分类。
回顾我们之前所学习的内容,我们在逻辑回归中,我们使用 sigmod 进行计算对数似然损失,以此来定义我们的二分类的损失。
- 在二分类中我们有正类和负类,正类的概率为 P ( x ) = 1 1 + e − x P(x)=\frac{1}{1+e^{-x}} P(x)=1+e−x1,那么负类的概率为 1 − P ( x ) 1-P(x) 1−P(x)
- 将这个结果进行计算对数似然损失 − ∑ y l o g ( P ( x ) ) -\sum ylog(P(x)) −∑ylog(P(x))就可以得到最终损失
那么在多分类的过程中我们应该怎么做呢?
- 多分类和二分类唯一的区别是我们不能够再使用sigmod函数来计算当前样本属于某个类别的概率,而应该使用softmax函数
- softmax和sigmod的区别在于我们需要去计算样本属于每个类别的概率,需要计算多次,而sigmod只需要计算一次
例如下图:
假如加入softmax之前的输出结果是 2.3,4.1,5.6
,那么经过softmax之后的结果是什么呢?
Y
1
=
e
2.3
e
2.3
+
e
4.1
+
e
5.6
Y_{1} = \frac{e^{2.3}}{e^{2.3}+e^{4.1}+e^{5.6}}
Y1=e2.3+e4.1+e5.6e2.3
Y
2
=
e
4.1
e
2.3
+
e
4.1
+
e
5.6
Y_{2} = \frac{e^{4.1}}{e^{2.3}+e^{4.1}+e^{5.6}}
Y2=e2.3+e4.1+e5.6e4.1
Y
3
=
e
5.6
e
2.3
+
e
4.1
+
e
5.6
Y_{3} = \frac{e^{5.6}}{e^{2.3}+e^{4.1}+e^{5.6}}
Y3=e2.3+e4.1+e5.6e5.6对于这个softmax输出的结果,是在[0,1]区间,我们可以把它当作概率。和前面二分类的损失一样,多分类的损失只需要再把这个结果进行对数似然损失的计算即可,即:
J
=
−
∑
Y
l
o
g
(
P
)
J=-\sum Ylog(P)
J=−∑Ylog(P)其中
P
=
e
z
j
∑
k
=
1
K
e
z
k
P=\frac{e^{z_{j}}}{ {\textstyle \sum_{k=1}^{K}} e^{z_{k}}}
P=∑k=1Kezkezj(也就是上式我们计算的
Y
1
,
Y
2
,
Y
3
Y_1,Y_2,Y_3
Y1,Y2,Y3),
Y
Y
Y表示真实值,最后,会计算每个样本的损失,即上式的平均值,我们把softmax概率传入对数似然损失得到的损失函数称为交叉熵损失。
在pytorch中有两种方法实现交叉熵损失:
方法1
criterion = nn.CrossEntropyLoss()
loss = criterion(input, target)
方法2
# 1. 对输出值计算softmax和取对数
output = F.log_softmax(x, dim=-1)
# 2. 使用torch中带权损失
loss = F.nll_loss(output, target)
带权损失定义为: l n = − ∑ w i x i l_{n}=-\sum w_{i}x_{i} ln=−∑wixi,其实就是把 l o g ( P ) log(P) log(P) 作为 x x x,把真实值 Y Y Y 作为权重。
6.6 模型的训练
训练的流程:
- 实例化模型,设置模型为训练模式
- 实例化优化器类,实例化损失函数
- 获取并遍历dataloader
- 梯度置0
- 进行前向计算
- 计算损失
- 反向传播
- 更新参数
代码如下:
model = MnistModel()
optimizer = Adam(model.parameters(), lr=0.001)
def train(epoch):
"""
实现训练的过程
:param epoch: 训练的轮数
:return:
"""
data_loader = get_dataloader()
for idx, (input,target) in enumerate(data_loader):
optimizer.zero_grad() # 梯度置0
output = model(input) # 调用模型,得到预测值
loss = F.nll_loss(output, target) # 得到损失
loss.backward() # 反向传播
optimizer.step() # 梯度的更新
if idx%10 == 0:
print(epoch,idx,loss.item())
6.7 模型的保存和加载
模型的保存
torch.save(mnist_net.state_dict()), "model/mnist_net.pt") # 保存模型参数
torch.save(optimizer.state_dict(), "results/mnist_optimizer.pt") # 保存优化器参数
模型的加载
mnist_net.load_state_dict(torch.load("model/mnist_net.pt"))
optimizer.load_state_dict(torch.load("results/mnist_optimizer.pt"))
6.8 模型的评估
评估的过程和训练的过程相似,但是:
- 不需要计算梯度
- 需要收集损失和准确率,用来计算平均损失和平均准确率
- 损失的计算和训练计算方法相同
- 准确率的计算
- 模型的输出为[batch_size, 10]的形状
- 其中最大值的位置就是其预测的目标值(预测值进行过softmax后为概率)
- 最大值的位置获取的方法可以使用 torch.max 返回最大值和最大值的位置
- 返回最大值的位置后,和真实值([batch_size])进行对比,相同则表示预测成功
代码如下:
def test():
loss_list = []
acc_list = []
test_dataloader = get_dataloader(train=False, batch_size=TEST_BATCH_SIZE)
for idx,(input,target) in enumerate(test_dataloader):
with torch.no_grad():
output = model(input) # 调用模型,得到预测值
cur_loss = F.nll_loss(output, target) # 得到损失
loss_list.append(cur_loss)
# 计算准确率
# output [batch_size, 10] target: [batch_size, 1]
pred = output.max(dim=-1)[-1]
cur_acc = pred.eq(target).float().mean()
acc_list.append(cur_acc)
print("平均准确率,平均损失",np.mean(acc_list),np.mean(loss_list))
7. 完整代码
# 使用pytorch完成手写数字识别
from torchvision.datasets import MNIST
from torchvision.transforms import Compose,Normalize,ToTensor
from torch.utils.data import DataLoader
from torch.optim import Adam
import torch.nn.functional as F
import torch.nn as nn
import torch
import os
import numpy as np
BATCH_SIZE = 128
TEST_BATCH_SIZE = 1000
# 1. 准备数据集
def get_dataloader(train=True, batch_size=BATCH_SIZE):
transform_fn = Compose([
ToTensor(),
Normalize(mean=(0.1307),std=(0.3081))
])
dataset = MNIST(root='./data', train=train, transform=transform_fn)
data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
return data_loader
# 2. 构建模型
class MnistModel(nn.Module):
def __init__(self):
super(MnistModel, self).__init__()
self.fc1 = nn.Linear(1*28*28, 28)
self.fc2 = nn.Linear(28, 10)
def forward(self, input):
"""
:param input: [batch_size,1,28,28]
:return:
"""
# 1. 修改形状
x = input.view(input.size(0), 1*28*28)
# 2. 进行全连接的操作
x = self.fc1(x)
# 3. 进行激活函数处理,形状没有变化
F.relu(x)
# 4. 输出层
out = self.fc2(x)
return F.log_softmax(out, dim=-1)
model = MnistModel()
optimizer = Adam(model.parameters(), lr=0.001)
if os.path.exists("./model/model.pkl"):
model.load_state_dict(torch.load("./model/model.pkl")) # 加载模型参数
optimizer.load_state_dict(torch.load("./model/optimizer.pkl")) # 加载优化器参数
def train(epoch):
"""
实现训练的过程
:param epoch: 训练的轮数
:return:
"""
data_loader = get_dataloader()
for idx, (input,target) in enumerate(data_loader):
optimizer.zero_grad() # 梯度置0
output = model(input) # 调用模型,得到预测值
loss = F.nll_loss(output, target) # 得到损失
loss.backward() # 反向传播
optimizer.step() # 梯度的更新
if idx%100 == 0:
print(epoch,idx,loss.item())
# 模型的保存
if idx%100 == 0:
torch.save(model.state_dict(), "./model/model.pkl")
torch.save(optimizer.state_dict(), "./model/optimizer.pkl")
def test():
loss_list = []
acc_list = []
test_dataloader = get_dataloader(train=False, batch_size=TEST_BATCH_SIZE)
for idx,(input,target) in enumerate(test_dataloader):
with torch.no_grad():
output = model(input) # 调用模型,得到预测值
cur_loss = F.nll_loss(output, target) # 得到损失
loss_list.append(cur_loss)
# 计算准确率
# output [batch_size, 10] target: [batch_size, 1]
pred = output.max(dim=-1)[-1]
cur_acc = pred.eq(target).float().mean()
acc_list.append(cur_acc)
print("平均准确率,平均损失",np.mean(acc_list),np.mean(loss_list))
if __name__ == '__main__':
for i in range(10):
train(i)
test()
总结
通过实现手写数字识别案例,让我了解了如何去将之前学习的理论进行实现,也让我对pytorch中常用的API有了初步的了解,以及对一个案例,该如何去部署。下周我将通过pytorch实现深度学习网络,如CNN、RNN的构建,同时下周再继续巩固机器学习的基础。