携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情
创建一个机器学习模型,基于 pytorch 实现图像分类,在整个分享过程中会用到 PyTorch 这个深度学习框架和 Torchvision 库
MLP 可以看作前馈神经网络,MLP 算是一个比较基础的神经网络。这里数据集采用 MNIST 的数据,关于这个数据集我这里就不做过多介绍了,一个深度学习入门级数据集。
引入依赖
torch
torch.nn
和troch.nn.functional
提供定义神经网络的基础torch.optim
更新神经网络可学习参数的优化器torch.utils.data
数据集相关torchvision.transforms
对数据进行转换来进行数据增强torchvision.dataset
加载数据集sklearn
对结果进行评估,例如混淆矩阵, decomposition 和 manifold 用于在 2 维空间观察神经网络的表现matplotlib
用于绘制
# torch
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch.utils.data as data
# torchvision
import torchvision.transforms as transforms
import torchvision.datasets as datasets
# sklearn
from sklearn import metrics
from sklearn import decomposition
from sklearn import manifold
# progress bar
from tqdm.notebook import trange, tqdm
import numpy as np
import copy
import random
import time
设置随机种子,确保整个过程一致性
SEED = 1234
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.backends.cudnn.deterministic = True
数据集
-
首先要做的就是加载数据集
接下来就是对数据进行标准化,表示让数据集样本均值为 0 方差为 1,为什么需要标准化,标准化作用是为了让网络训练更快,避免落在局部最小值,可靠性更强。
首先要做的就是计算数据均值和方差,这里参与计算均值和方差的样本仅是涉及训练数据集,而不会涉及到测试数据集,这是因为在测试之前我们不想向模型泄漏任何测试数据集的信息。
mean = train_data.data.float().mean()/255
std = train_data.data.float().std() / 255
print(f'Calculated mean:{mean}')
print(f'Calculated std: {std}')
Calculated mean:0.13066047430038452
Calculated std: 0.30810779333114624
计算完了数据均值和方差,好现在就是 Torchvision 的 transforms
登场的时候了。transform
可以对数据进行增强,通过对现有数据进行变换来得到更多样本,这样处理可以进行组合,在 torchvision 中提供了 transforms.Compose
这个方法来将一系列的转换进行组合后应用到图像上。
今天我们会采用那些变换,下面就将这些变换一一列出
-
RandomRotation
随机在(-x,+x)
度数之间对图像进行旋转,这里设置为x=5
注意fill=(0,)
在 torchvision 的一些版本可能导致程序 bug -
RandomCrop
首先会对图像四周添加 2 像素边框,对图像进行放大,然后再去裁剪回到图像原有 28×28 ,也就是先将图像通过添加边框放大 32 x 32 然后再去随机裁剪会原有尺寸 -
ToTensor
将图像从 PIL 图像格式转换为 Pytorch 的 Tensor -
Normalize
指定的均值和方差对数据集进行标准化
这里值得注意 ToTensor
可以应用在 RandomRotation
和 RandomCrop
之前,而 Normalize
需要应用在 ToTensor
之后
我们分别为训练数据集和测试数据集创建各自的 transform ,对于训练数据集我们需要更多样本所以使用 RandomRotation
和 RandomCrop
train_transforms = transforms.Compose([
transforms.RandomRotation(5,fill=(0,)),
transforms.RandomCrop(28,padding=2),
transforms.ToTensor(),
transforms.Normalize(mean=[mean],std=[std])
])
test_transforms = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[mean],std=[std])
])
train_data = datasets.MNIST(root=ROOT,
train=True,
download=True,
transform=train_transforms)
test_data = datasets.MNIST(root=ROOT,
train=False,
download=True,
transform=test_transforms)
print(f"Number of training examples: {len(train_data)}")
print(f"Number of testing examples: {len(test_data)}")
Number of training examples: 60000
Number of testing examples: 10000
查看一下数据集中图像
import matplotlib.pyplot as plt
def plot_images(images):
n_images = len(images)
rows = int(np.sqrt(n_images))
cols = int(np.sqrt(n_images))
fig = plt.figure()
for i in range(rows * cols):
ax = fig.add_subplot(rows, cols, i +1)
ax.imshow(images[i].view(28,28).cpu().numpy(),cmap='bone')
ax.axis('off')
N_IMAGES = 25
images = [image for image,label in [train_data[i] for i in range(N_IMAGES)]]
plot_images(images)
在 MNIST 数据集,只提供了训练集和测试集,但没有验证集。我们想用验证集来检查模型在训练过程中表现。这里我们为什么不能用测试集作为验证集来使用呢?答案很简单我们需要保持测试集是在训练完成后对模型评估,所以在训练过程我们不能透漏半点关于测试集的信息。这里我们可以自己创建一个验证集,也就是从训练集中取 10% 样本作为验证集,为什么不从测试集中取呢? 答案也很简单就是,为了以后和其他训练结果做对比,想象一下,如果你测试结果是基于少了一些样本的数据集,那么你的测试结果也就是没有和其他测试结果有可比性。
VALID_RATIO = 0.9
n_train_examples = int(len(train_data) * VALID_RATIO)
n_valid_examples = len(train_data) - n_train_examples
train_data, valid_data = data.random_split(train_data,[n_train_examples,n_valid_examples])
print(f"Number of training examples: {len(train_data)}")
print(f"Number of valid examples: {len(valid_data)}")
print(f"Number of testing examples: {len(test_data)}")
Number of training examples: 54000
Number of valid examples: 6000
Number of testing examples: 10000
这里我们还需要考虑一点,就是由于验证集是从变换后的训练集截取一个部分数据,所以这些数据也进行随机旋转和裁剪,如果我们希望让验证集在训练过程中做测试集代理,那么这些样本应该是没有进行任何数据增强处理的原始数据
N_IMAGES = 25
images = [image for image,label in [valid_data[i] for i in range(N_IMAGES)]]
plot_images(images)
解决方案也比较简单,就是用测试集的 transforms 来替换验证集上 transforms,因为验证集是训练集的一部分,为了不将对验证集上数据的 transform 修改影响训练数据集,我们可以对验证集进行一次深 copy 然后再去修改其 transform。
valid_data = copy.deepcopy(valid_data)
valid_data.dataset.transform = test_transforms
再次确认一下
N_IMAGES = 25
images = [image for image,label in [valid_data[i] for i in range(N_IMAGES)]]
plot_images(images)
加下来就是通过 DataLoader 来分别加载训练数据、验证集和测试集。在加载过程自需要对与训练数据集进行打乱顺序来进行加载,希望不同 epoch 中每个 batch 都是不同,从而增加一些随机性让网络具有一定泛化能力。因为不用验证集和测试集数据来更新模型所以这两集合不需要打乱顺序
BATCH_SIZE = 64
train_iterator = data.DataLoader(train_data,
shuffle=True,
batch_size=BATCH_SIZE)
valid_iterator = data.DataLoader(valid_data,
batch_size=BATCH_SIZE)
test_iterator = data.DataLoader(test_data,
batch_size=BATCH_SIZE)
定义模型
具体来说,首先我们把 1×28×281 times 28 times 28 展平为 784 个元素向量,也可以理解为 874 维特征,MLP 中还不涉及到 2 维或者更高维的数据,接下里连续经过两个隐含层,将维度变换为 100 ,接下来就是输出层,维度为 10 对应 10 个类别。这两隐含层为全连接层,类似对数据进行一个线性变换。
关于这里具体要采用几个隐藏层,以及每个隐藏层神经元具体数量,这里也没有什么计算公式,或者规则来帮助我们根据问题来选择应该设置多少隐藏层,以及每个隐藏层中神经元的个数。通常我们因为浅层会提取一般,相对简单的特征例如(线、曲线和边缘),而后面的层则将从一层提取的特征组合成为更高层次的特征。
class MLP(nn.Module):
def __init__(self,input_size,output_size):
super().__init__()
self.input_fc = nn.Linear(input_size,200)
self.hidden_fc = nn.Linear(200,100)
self.output_fc = nn.Linear(100,output_size)
def forward(self,x):
batch_size = x.shape[0]
x = x.view(batch_size,-1)
h_1 = F.relu(self.input_fc(x))
h_2 = F.relu(self.hidden_fc(h_1))
y_pred = self.output_fc(h_2)
return y_pred, h_2
INPUT_DIM = 28 * 28
OUTPUT_DIM = 10
model = MLP(INPUT_DIM,OUTPUT_DIM)
创建函数来计算可训练的参数(权重和偏置)在模型中的数量
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"The model has {count_parameters(model):,} trainable parameters")
The model has 222,360 trainable parameters
784 * 200 + 200 + 200 * 100 + 100 + 100 * 10 + 10