一、思路和流程分析
- 准备数据,需要准备DataLoader
- 构建模型,这里可以使用torch构造一个深层的神经网络
- 训练模型
- 保存模型
- 使用测试集进行模型评估
二、准备训练集和测试集
为了进行数据的处理,需要介绍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],彩色图片有3个通道(R,G,B),每个通道的像素点取值范围为[0,255],三个通道的颜色叠加后形成各种颜色
from torchvision import transforms
import numpy as np
img = np.random.randint(0, 255, size=12).reshape(2,2,3)
print(img.shape)
# 将(H,W,C)的numpy.ndarray转换成(C,H,W)的tensor
img_tensor = transforms.ToTensor()(img)
print(img_tensor)
print(img_tensor.shape)
2. torchvision.transforms.Normalize(mean, std)
根据均值、方差进行规范化处理,即:
input[channel] = (input[channel] - mean[channel]) / std[channel]
img = np.random.randint(0, 255, size=12).reshape(2,2,3)
img = transforms.ToTensor()(img)
print(img)
print('-'*100)
norm_img = transforms.Normalize(mean = (0.5, 0.5, 0.5), std = (0.5, 0.5, 0.5))(img)
print(norm_img)
3. torchvision.transforms.Compose(transforms)
作用:将多个transform
组合使用
transform.Compose([
torchvision.transforms.ToTensor(), # 转为tensor
torchvision.transforms.Normalize(mean, std) # 标准化
])
4. 准备训练集
下载数据集:
from torchvision.datasets import MNIST
MNIST(root='./data', train=True, download=True)
准备数据集:
def getDataLoader(istrain=True, batch_size=TRAIN_BATCH_SIZE):
dataset = MNIST(
root='./data',
train=istrain,
transform=Compose([
ToTensor(),
Normalize(mean=(0.1307,), std=(0.3081,))
])
)
# dataLoader里的一个元素为一个列表,列表包含特征值和目标值
# 特征值形状为BATCH_SIZE * 1 * 28 * 28
# 目标值表示此手写图片代表的数字
dataLoader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
return dataLoader
当batch_size=2
时,打印dataLoader
里其中一个元素,以及特征值的形状:
三、构建模型
补充:
全连接层: 当前一层的神经元和前一层的神经元相互连接,其核心操作就是 y = w x y=wx y=wx,即矩阵乘法,实现对前一层数据的变换
模型的构建使用了一个四层的神经网络,其中包括一个输入层,两个全连接层和一个输出层,第一个全连接层经过激活函数的处理,将处理后的结果交给下一个全连接层,进行变换后输出结果
在这个模型中需要注意:
- 激活函数的使用
- 每一层数据的形状
- 模型的损失函数
3.1 激活函数的使用
激活函数由torch.nn.functional
提供
3.2 模型中数据的形状
- 原始输入数据的形状:
batch_size * 1 * 28 * 28
- 进行形状的修改:
batch_size * 28 * 28
- 第一个全连接层的输出形状:
batch_size * 28
- 激活函数不会修改数据的形状
- 第二个全连接层的输出形状:
batch_size * 10
,因为手写数字有10个类别
构建模型的代码如下:
# 构建模型
class MnistModel(nn.Module):
def __init__(self):
super(MnistModel, self).__init__()
self.full_conn1 = nn.Linear(1*28*28, 50) # 全连接层的形状
self.full_conn2 = nn.Linear(50, 10) # 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.full_conn1(x)
# 3. 激活函数处理
x = F.relu(x)
# 4. 第二层全连接
out = self.full_conn2(x)
return out
3.3 模型的损失函数
首先,我们需要明确,当前我们手写字体识别的问题是一个多分类的问题,所谓多分类对比的是之前学习的二分类
我们在二分类问题中使用sigmoid
进行计算对数似然损失
- 在二分类中,正类的概率为 p ( x ) = 1 1 + e − x = e x 1 + e x p(x)=\frac{1}{1+e^{-x}}=\frac{e^x}{1+e^x} p(x)=1+e−x1=1+exex,那么负类的概率直接为 1 − p ( x ) 1-p(x) 1−p(x)
- 将这个结果进行计算对数似然损失 − ∑ y l o g ( p ( x ) ) -\sum ylog(p(x)) −∑ylog(p(x))就可得到最终的损失
那么在多分类问题中我们应该怎么做?
- 多分类和二分类中唯一的区别是我们不能再使用
sigmoid
函数来计算当前样本属于哪个类别的概率,而应该使用softmax
函数 softmax
和sigmoid
的区别在于我们需要去计算样本属于哪个类别,需要计算多次,而sigmoid
只需要计算一次
s o f t m a x softmax softmax公式: σ ( z ) j = e z j ∑ k = 1 K e z k \sigma(z)_j=\frac{e^{z_j}}{\sum_{k=1}^K}e^{z_k} σ(z)j=∑k=1Kezjezk
假设softmax的输入是2、3、5,那么经过softmax之后的结果如下:
这个输出结果介于[0,1]之间,我们可以把它当作概率
和前面的二分类的损失一样,多分类的损失只需要再把这个结果进行对数似然损失的计算即可:
− ∑ y l o g ( P ) -\sum ylog(P) −∑ylog(P),其中 P = e z j ∑ k = 1 K e z k P=\frac{e^{z_j}}{\sum_{k=1}^K}e^{z_k} P=∑k=1Kezjezk, y y y 表示真实值
最后,计算每个样本的损失,即上式的平均值。我们把softmax概率传入对数似然函数得到的损失函数称为交叉熵损失
在pytorch中,有两种方式实现 交叉熵损失
criterion = nn.CrossEntropyLoss()
loss = criterion(input, target)
# 对输出值计算softmax和取对数
output = F.log_softmax(x, dim=-1)
# 使用torch中的带权损失
loss = F.nll_loss(output, target)
带权损失定义为: l n = − ∑ w i x i l_n=-\sum w_ix_i ln=−∑wixi,其实就是把 l o g ( P ) log(P) log(P)作为 x i x_i xi,把真实值 y y y 作为权重 w i w_i wi
四、训练模型
训练的流程:
- 实例化模型,设置为训练模式
- 实例化优化器类,实例化损失函数
- 获取、遍历dataLoader
- 置梯度为0
- 前向传播
- 计算损失
- 反向传播
- 更新参数
# 训练模型
def train(epoch, model, optimizer):
# 获取数据加载器
dataLoader = getDataLoader()
# 开始训练
for i in range(epoch):
for index, (input, target) in enumerate(dataLoader):
# 梯度置 0
optimizer.zero_grad()
# 调用模型,得到预测值。会调用类方法forward
output = model(input)
# 计算交叉熵损失
loss = F.nll_loss(output, target)
# 反向传播
loss.backward()
# 梯度更新
optimizer.step()
if index % 100 == 0:
print(loss.item())
torch.save(model.state_dict(), './MnistModel/model.pkl')
torch.save(optimizer.state_dict(), './MnistModel/optimizer.pkl')
保存模型
torch.save(model.state_dict(), './MnistModel/model.pkl')
torch.save(optimizer.state_dict(), './MnistModel/optimizer.pkl')
加载模型
model.load_state_dict(torch.load('./MnistModel/model.pkl'))
optimizer.load_state_dict(torch.load('./MnistModel/optimizer.pkl'))
四、评估模型
评估的过程和训练的过程类似,但是:
- 无需计算梯度
- 需要收集损失和准确率,用于计算平均损失和平均准确率
- 损失的计算和训练时候损失的计算方法相同
- 准确率的计算
- 模型的输出为[batch_size,10]的形状
- 其中最大值的位置就是其预测的目标值(预测值进行过softmax后为概率,softmax中分母相同,分子越大,概率越大)
- 最大值的位置获取最大值可以用
torch.max
,同时返回最大值和最小值的位置- 返回最大值位置后,和真实值([batch_size])进行对比,相同表示成功
def test(model):
loss_list = []
accu_list = []
testDataLoader = getDataLoader(istrain=False, batch_size=TEST_BATCH_SIZE)
for feature, target in testDataLoader:
# 无需跟踪梯度,所以包含在torch.no_grad()内
with torch.no_grad():
output = model(feature) # [batch_size, 10]
cur_loss = F.nll_loss(output, target)
loss_list.append(cur_loss)
# 计算准确率
# output是存放的batch_size*10的概率,每列对应预测的一个数值
# 每一行表示一张图片,预测一个数据,找出每行10个概率中最大的概率
# 此概率的位置对应的数值,便是预测的数据
# max函数返回最大的数值,以及对应的位置
pre = output.max(dim=-1)[-1]
cur_accu = pre.eq(target).float().mean()
accu_list.append(cur_accu)
print('平均准确率={:.2f}%,平均损失={:.2f}%'.format(100*np.mean(accu_list), 100*np.mean(loss_list)))
完整代码:
import os
import numpy as np
import torch
import torch.nn.functional as F
from torch import nn
from torch.optim import Adam
from torchvision.datasets import MNIST
from torch.utils.data import Dataset, DataLoader
from torchvision.transforms import ToTensor, Normalize, Compose
TRAIN_BATCH_SIZE = 256
TEST_BATCH_SIZE = 256*4
IS_TRAIN = False
# 准备数据集
def getDataLoader(istrain=True, batch_size=TRAIN_BATCH_SIZE):
dataset = MNIST(
root='./data',
train=istrain,
transform=Compose([
ToTensor(),
Normalize(mean=(0.1307,), std=(0.3081,))
])
)
# dataLoader里的元素为特征值和目标值
# 特征值形状为BATCH_SIZE * 1 * 28 * 28
# 目标值表示此手写图片代表的数字
dataLoader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
return dataLoader
# 构建模型
class MnistModel(nn.Module):
def __init__(self):
super(MnistModel, self).__init__()
self.full_conn1 = nn.Linear(1*28*28, 50) # 全连接层的形状
self.full_conn2 = nn.Linear(50, 10) # 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.full_conn1(x)
# 3. 激活函数处理
x = F.relu(x)
# 4. 第二层全连接
out = self.full_conn2(x)
# 必须指定在哪个维度上进行softmax操作,dim为-1表示在最后一个维度操作
return F.log_softmax(out, dim=-1)
# 训练模型
def train(epoch, model, optimizer):
# 获取数据加载器
dataLoader = getDataLoader()
# 开始训练
for i in range(epoch):
for index, (feature, target) in enumerate(dataLoader):
# 梯度置0
optimizer.zero_grad()
# 调用模型,得到预测值。会调用类方法forward
output = model(feature)
# 计算交叉熵损失
loss = F.nll_loss(output, target)
# 反向传播
loss.backward()
# 梯度更新
optimizer.step()
if index % 100 == 0:
print(loss.item())
torch.save(model.state_dict(), './MnistModel/model.pkl')
torch.save(optimizer.state_dict(), './MnistModel/optimizer.pkl')
def test(model):
loss_list = []
accu_list = []
testDataLoader = getDataLoader(istrain=False, batch_size=TEST_BATCH_SIZE)
for feature, target in testDataLoader:
# 无需跟踪梯度,所以包含在torch.no_grad()内
with torch.no_grad():
output = model(feature) # [batch_size, 10]
cur_loss = F.nll_loss(output, target)
loss_list.append(cur_loss)
# 计算准确率
# output是存放的batch_size*10的概率,每列对应预测的一个数值
# 每一行表示一张图片,预测一个数据,找出每行10个概率中最大的概率
# 此概率的位置对应的数值,便是预测的数据
# max函数返回最大的数值,以及对应的位置
pre = output.max(dim=-1)[-1]
cur_accu = pre.eq(target).float().mean()
accu_list.append(cur_accu)
print('平均准确率={:.2f}%,平均损失={:.2f}%'.format(100*np.mean(accu_list), 100*np.mean(loss_list)))
def main():
# 实例化模型
model = MnistModel()
# 实例化优化器
optimizer = Adam(model.parameters(), lr=1e-3)
# 从文件加载模型、优化器
if os.path.exists('./MnistModel/model.pkl'):
model.load_state_dict(torch.load('./MnistModel/model.pkl'))
optimizer.load_state_dict(torch.load('./MnistModel/optimizer.pkl'))
# 训练100轮
epoch = 100
# 传入模型,优化器,开始训练
if IS_TRAIN:
train(epoch, model, optimizer)
else:
test(model)
if __name__ == '__main__':
main()