[Deep Leaning] [Tutorial] Classification on MNIST Dataset
文章目录
- Importing Packages
- Data Loading
- Defining Dataloaders
- Sanity Check of the Dataset
- Model Definition
- Loss Function Definition
- Optimization Method
- Training and Testing Procedures
- Runtime
- Performance before any optimization
- Performance after 1 iteration of optimization
- Performance after 100 iterations of optimization
- Performance after 5 epochs of optimization
- Visualize Feature Maps in CNNs
Importing Packages
%matplotlib inline
from __future__ import print_function
import matplotlib.pyplot as plt
import numpy as np
import time
import math
import torch
import torch.nn as nn
import torch.nn.functional as Ffrom torchvision import datasets, transforms
from sklearn.metrics import confusion_matrix
from datetime import timedeltatorch.__version__
Data Loading
Defining Dataloaders
use_cuda = True if torch.cuda.is_available() else False
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print('We are using GPU.' if use_cuda else 'We are using CPU.')
MNIST数据集大小约为12MB,如果在给定路径下找不到该数据集,它将被自动下载。该数据集包含70,000个图像和相应的标签。
我们需要定义两个数据加载器,一个用于训练,一个用于测试。在训练过程中,批处理大小设置为16。
kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}
kwargs['batch_size'] = 16
'''
设置num_workers为1和pin_memory为True是为了提高数据加载到GPU的速度。
这两个参数和PyTorch的DataLoader类有关,它负责在训练过程中有效地加载数据。num_workers:
这个参数决定了用于数据加载的子进程的数量。
将num_workers设置为1意味着使用一个子进程来加载数据。
增加num_workers的值可以进一步提高数据加载速度,但同时也会占用更多的CPU资源。
选择合适数字取决于CPU资源和I/O限制。
为了充分利用GPU,可以尝试逐渐增加num_workers的值,直到达到最佳性能。
请注意,设置num_workers为0将在主进程中进行数据加载,这可能会降低整体性能。pin_memory:
将此参数设置为True可以将数据存储在固定(或锁定)内存中。
这意味着当数据从CPU传输到GPU时,不会发生内存拷贝,从而减少了数据加载时间。
这在使用GPU训练模型时特别有用。
然而,锁定内存会占用系统的可用RAM,因此需要权衡资源利用率。综上所述,可以尝试将num_workers设置为其他正整数值以优化数据加载速度,但要注意不要耗尽CPU资源。
同时,pin_memory在使用GPU时通常应设置为True,以提高数据传输效率。
在CPU训练时,将pin_memory设置为False可以节省RAM资源。
'''# Using torch.utils.data.DataLoader for efficient dataloading during runtime.
train_loader = torch.utils.data.DataLoader(datasets.MNIST('../data', train=True, download=True,transform=transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,))])),shuffle=True, **kwargs)
'''
torch.utils.data.DataLoader:
这是PyTorch提供的一个数据加载器类,用于加载数据并将其分成批次(batches)。datasets.MNIST():
这是一个用于加载MNIST手写数字数据集的类。它有以下参数:root='../data':数据集的根目录。在这里,数据集将被下载到当前目录下的"data"文件夹中。train=True:表示加载训练数据。如果设置为False,则加载测试数据。download=True:表示如果数据集不存在,则自动下载数据集。transform=transforms.Compose([...]):这里定义了一个图像预处理的pipeline。在这个例子中,有两个预处理步骤:transforms.ToTensor():将图像转换为PyTorch张量(Tensor)。transforms.Normalize((0.1307,), (0.3081,)):对图像进行归一化。这里使用的均值是0.1307,标准差是0.3081。shuffle=True:在每个训练周期(epoch)开始时,随机打乱数据集的顺序。
'''
test_loader = torch.utils.data.DataLoader(datasets.MNIST('../data', train=False, transform=transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,))])),shuffle=False, **kwargs)
print('Dataloaders initialized.')
Sanity Check of the Dataset
Print some basic information about the dataset.
print('{} examples in the training set.'.format(len(train_loader) * 16))
print('{} examples in the testing set.'.format(len(test_loader) * 16))b_imgs, b_labels = next(iter(train_loader))
'''
next() 和 iter() 是 Python 中用于处理迭代器(iterator)的内置函数。
通过使用 iter() 函数,train_loader 被转换为一个迭代器对象。
然后,next() 函数被调用,以获取迭代器的下一个元素,即训练数据的批次。
这样,b_imgs 和 b_labels 就分别包含了训练数据批次的图像和标签。
'''
print('A batch of imgs shape:', b_imgs.size())
print('A batch of labels shape:', b_labels.size())
print('label batch:', b_labels)
Show some images and labels to ensure they are paired.
def plot_images(images, cls_true, img_shape=None, cls_pred=None):assert len(images) == len(cls_true) == 9# Create figure with 3x3 sub-plots.fig, axes = plt.subplots(3, 3)fig.subplots_adjust(hspace=0.3, wspace=0.3)for i, ax in enumerate(axes.flat):# Plot image.ax.imshow(images[i].reshape((28,28)), cmap='binary')# Show true and predicted classes.if cls_pred is None:xlabel = "True: {0}".format(cls_true[i])else:xlabel = "True: {0}, Pred: {1}".format(cls_true[i], cls_pred[i])# Show the classes as the label on the x-axis.ax.set_xlabel(xlabel)# Remove ticks from the plot.ax.set_xticks([])ax.set_yticks([])# Ensure the plot is shown correctly with multiple plots# in a single Notebook cell.plt.show()# Plot a few images to see if data is correct
images = b_imgs[:9].numpy()
cls_true = b_labels[:9].numpy()plot_images(images=images, cls_true=cls_true)
Model Definition
Models defined using PyTorch toolkit should be a class inheriting from torch.nn.Module.
在__init__方法中,我们应该实例化在前向传播过程中将使用的子模块(例如nn.Conv2d,nn.Linear)。这些子模块应该作为成员变量通过self引用,例如self.conv1,self.fc1。
nn.Conv2d层应该使用参数(in_channels,out_channels,kernel_size,stride=1,padding=0,dilation=1,groups=1,bias=True,padding_mode=‘zeros’)进行实例化。in_channels表示该层的输入通道数。out_channels表示该层的输出通道数。kernel_size是卷积核的大小。可以在这里找到该模块的详细信息。
nn.Linear层应该使用参数(in_features,out_features,bias=True)进行实例化。可以在这里找到该模块的详细信息。
请注意,在nn.Dropout2d中,参数是元素被置零的概率,而不是保留的概率。
forward方法定义了当输入x被馈送到模型中时,该模型的运行时行为。不包含可学习权重的函数,如ReLU、MaxPooling,可以直接在此forward方法中使用(例如F.relu,F.max_pool2d),而不是在__init__中实例化。
class MnistConvNet(nn.Module):def __init__(self, return_fmaps=False):super(MnistConvNet, self).__init__()'''这个操作是PyTorch模型定义的必须操作。调用super(MnistConvNet, self).init()相当于调用nn.Module的构造函数__init__(),这样模型就可以拥有nn.Module的基本功能和属性,例如计算图的构建、反向传播、参数优化等。调用super()函数时,需要传递当前类的名称和实例对象作为参数。这是因为super()函数需要确定当前类的方法解析顺序(MRO, Method Resolution Order),以便在调用父类方法时正确地查找继承链中的下一个类。'''self.conv1 = nn.Conv2d(1, 32, 7, stride=1, padding=3)'''Conv1d(一维卷积):一维卷积主要用于处理序列数据,如时间序列、文本或音频信号。在一维卷积中,卷积核沿着输入数据的一个维度(通常是长度)滑动。Conv2d(二维卷积):torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)二维卷积主要用于处理图像数据。在二维卷积中,卷积核沿着输入数据的两个维度(通常是高度和宽度)滑动。Conv3d(三维卷积):三维卷积主要用于处理体数据(volumetric data)和视频数据。在三维卷积中,卷积核沿着输入数据的三个维度(通常是深度、高度和宽度)滑动。'''self.conv2 = nn.Conv2d(32, 64, 5, stride=1, padding=2)self.dropout1 = nn.Dropout2d(0.25)self.dropout2 = nn.Dropout(0.5)'''nn.Dropout2d()通常用于二维特征图,主要用在卷积神经网络的卷积层。其作用是随机将整个通道的值置为0。nn.Dropout()则是在所有的输入特征上独立地工作,将每个元素以一定的概率置为0。这种方法通常用于全连接层或者一维的特征向量。'''self.fc1 = nn.Linear(7*7*64, 128) # 64是通道数,7x7是高度和宽度self.fc2 = nn.Linear(128, 10)self.return_fmaps = return_fmapsdef set_return_fmaps(self, v=True):self.return_fmaps = vdef forward(self, x):fmaps = []x = self.conv1(x)fmaps.append(x)print('after conv1, x.size:', x.size())x = F.relu(x) # Functions like ReLU, MaxPooling can be used in forward method as there is no weights in them to store.x = F.max_pool2d(x, 2)'''torch.nn.functional.max_pool2d(input, kernel_size, stride=None, padding=0, dilation=1, ceil_mode=False, return_indices=False)如果输入特征图的宽度和高度都是偶数,池化后的特征图尺寸将减半,即宽度和高度都除以2。如果输入特征图的宽度和高度有一个是奇数,池化后的特征图尺寸将向下取整,即宽度和高度除以2,并且最后一行或最后一列的像素将被舍弃。'''print('after pool1, x.size:', x.size())x = self.conv2(x)fmaps.append(x)print('after conv2, x.size:', x.size())x = F.relu(x)x = F.max_pool2d(x, 2)print('after pool2, x.size:', x.size())x = self.dropout1(x) # 并不会改变特征图的尺寸x = torch.flatten(x, 1)print('after flatten, x.size:', x.size())x = self.fc1(x)print('after fc1, x.size:', x.size())x = F.relu(x)'''在神经网络中,通常先计算线性变换(如全连接层或卷积层),然后应用激活函数。这种顺序使得模型能够学习非线性特征,并有助于神经网络的训练和收敛。'''x = self.dropout2(x)logits = self.fc2(x)'''logits = self.fc2(x)表示将经过第一个全连接层和ReLU激活函数处理后的输出特征x,输入到第二个全连接层中进行线性变换,得到模型的输出logits。在这个网络中,self.fc2(x)表示将第一个全连接层的输出特征x输入到第二个全连接层中,得到模型的输出logits。此时,logits是一个二维张量,其中的每一行代表了一个输入样本对应的预测概率分布,每个元素代表了该样本属于相应类别的概率。模型的输出logits将会被用于计算模型的损失函数和进行模型的预测。需要注意的是,在这个网络中,模型的输出层并没有经过激活函数处理。这是因为在使用交叉熵损失函数进行多类别分类时,通常会将模型的输出层视作未归一化的对数概率(logits)输出,而不是使用softmax等激活函数对输出进行归一化处理。这种做法可以提高数值稳定性和训练效率,同时避免了softmax中的数值溢出问题。'''print('logits.size:', logits.size())if self.return_fmaps:return logits, fmapselse:return logits
通过指定适当的 start_dim 参数,你可以控制从哪个维度开始展平,以便根据需要重新组织张量的形状。
如果不提供 start_dim 参数,则默认从第一个维度(索引为 0)开始展平。
# 输入张量的形状是 (2, 3, 4)
a = torch.tensor([[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],[[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]])print(a)
# 输出:
# tensor([[[ 1, 2, 3, 4],
# [ 5, 6, 7, 8],
# [ 9, 10, 11, 12]],
#
# [[13, 14, 15, 16],
# [17, 18, 19, 20],
# [21, 22, 23, 24]]])flattened = torch.flatten(a, start_dim=1)
'''
通过指定适当的 start_dim 参数,你可以控制从哪个维度开始展平,以便根据需要重新组织张量的形状。
如果不提供 start_dim 参数,则默认从第一个维度(索引为 0)开始展平。
'''
print(flattened)
# 输出:
# tensor([[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
# [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]])print(flattened.shape)
# 输出:
# torch.Size([2, 12])
通过 torch.flatten(a, start_dim=1),我们从索引为 1 的维度开始展平。
展平后,输出张量 flattened 的形状为 (2, 12),其中第一个维度保持不变,而第二个维度将 3 和 4 这两个维度展平为一维,形成了长度为 12 的新维度。
Now we can instantiate our CNN model.
model = MnistConvNet().to(device)
print('Model initialized.')
Loss Function Definition
We define the cross-entropy loss for this task.
交叉熵是分类任务中使用的一种损失函数。该损失函数是一个可微的函数,始终为正,并在模型的预测完全匹配目标值时达到最小值。在训练过程中,我们的模型权重会被更新以最小化该损失函数。
PyTorch内置了一个用于计算交叉熵损失的函数。需要注意的是,该函数在内部将softmax函数和交叉熵损失结合为单个操作,以提高效率,因此在模型定义中我们不需要手动使用softmax函数。
cross_entropy = nn.CrossEntropyLoss()
Optimization Method
We define the Adam optimization method in this task.
现在我们有一个需要最小化的损失函数,我们可以创建一个优化器。在这种情况下,我们使用Adam优化器,它是梯度下降的一种高级变体。
lr = 0.001
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
Training and Testing Procedures
We now define training and testing procedures which handles the runtime of the optimization.
首先,我们定义了训练的单个迭代过程,它使用一批数据来训练模型。数据批次被输入到模型中,根据模型的输出计算损失函数。然后,通过将损失函数进行反向传播(loss.backward)计算参数的梯度,并更新参数(optimizer.step)。
然后,我们定义了一个训练的周期(epoch),它将整个数据集正向和反向地通过模型一次。
def train_iter(log_interval, model, device, optimizer, loss_func, data, target):'''Train the model for a single iteration.An iteration is when a single batch of data is passed forward and backward through the neural network.'''data, target = data.to(device), target.to(device) # Move this batch of data to the specified device.optimizer.zero_grad() # Zero out the old gradients (so we only use new gradients for a new update iteration).output = model(data) # Forward the data through the model.loss = loss_func(output, target) # Calculate the lossloss.backward() # Backward the loss and calculate gradients for parameters.optimizer.step() # Update the parameters.return lossdef train_epoch(log_interval, model, device, train_loader, optimizer, epoch, loss_func):'''Train the model for an epoch.An epoch is when the entire dataset is passed forward and backward through the neural network for once.The number of batches in a dataset is equal to number of iterations for one epoch.'''model.train()for batch_idx, (data, target) in enumerate(train_loader): # Iterate through the entire dataset to form an epoch.loss = train_iter(log_interval, model, device, optimizer, loss_func, data, target) # Train for an iteration.if batch_idx % log_interval == 0:print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(epoch, batch_idx * len(data), len(train_loader.dataset),100. * batch_idx / len(train_loader), loss.item()))
The testing procedure is by taking the predictions of our model on the test set and calculate the accuracy.
def test(model, device, test_loader, loss_func):'''Testing the model on the entire test set.'''model.eval() # Switch the model to evaluation mode, which prevents the dropout behavior.test_loss = 0correct = 0with torch.no_grad(): # Because this is testing and no optimization is required, the gradients are not needed.for data, target in test_loader: # Iterate through the entire test set.data, target = data.to(device), target.to(device) # Move this batch of data to the specified device.output = model(data) # Forward the data through the model.test_loss += target.size(0)*loss_func(output, target).item() # Sum up batch losspred = output.argmax(dim=1, keepdim=True) # Get the index of the max log-probabilitycorrect += pred.eq(target.view_as(pred)).sum().item() # Count the correct predictions.test_loss /= len(test_loader.dataset) # Average the loss on the entire testing set.print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(test_loss, correct, len(test_loader.dataset),100. * correct / len(test_loader.dataset)))
Runtime
Performance before any optimization
We first show the accuracy of a randomly initialized model on test set. The accuracy is around 10% as it is just a random guess.
test(model, device, test_loader, cross_entropy)
Performance after 1 iteration of optimization
log_interval = 1
train_data_iter = iter(train_loader)model.train()
data, target = next(train_data_iter)
train_iter(log_interval, model, device, optimizer, cross_entropy, data, target)
test(model, device, test_loader, cross_entropy)
Performance after 100 iterations of optimization
log_interval = 10
model.train()
for batch_idx in range(100):data, target = next(train_data_iter)loss = train_iter(log_interval, model, device, optimizer, cross_entropy, data, target)if batch_idx % log_interval == 0:print('Train iter: {}\tLoss: {:.6f}'.format(batch_idx, loss.item()))
test(model, device, test_loader, cross_entropy)
Performance after 5 epochs of optimization
log_interval = 200
for epoch in range(5):train_epoch(log_interval, model, device, train_loader, optimizer, epoch, cross_entropy)test(model, device, test_loader, cross_entropy)
Visualize Feature Maps in CNNs
def plot_feature_maps(fmaps):assert len(fmaps) == 25# Create figure with 5x5 sub-plots.fig, axes = plt.subplots(5, 5, figsize=(7,7))fig.subplots_adjust(hspace=0.1, wspace=0.1)for i, ax in enumerate(axes.flat):# Normalize the feature maps for plotting.f_min, f_max = fmaps[i].min(), fmaps[i].max()normed_fmap = (fmaps[i] - f_min) / (f_max - f_min)# Plot image.ax.imshow(normed_fmap, cmap='binary')# Remove ticks from the plot.ax.set_xticks([])ax.set_yticks([])# Ensure the plot is shown correctly with multiple plots# in a single Notebook cell.plt.show()model.set_return_fmaps(True) # Set return feature maps.
b_imgs, _ = next(iter(train_loader))
_, b_fmaps = model(b_imgs.to(device))# For Conv1 feature maps:
fmaps_conv1 = b_fmaps[0][0, :25].detach().cpu().numpy() # Convert the pytorch variable to a numpy array.
plot_feature_maps(fmaps_conv1)# For Conv2 feature maps:
fmaps_conv2 = b_fmaps[1][0, :25].detach().cpu().numpy() # Convert the pytorch variable to a numpy array.
plot_feature_maps(fmaps_conv2)
正如我们所预期的,较低层的特征图具有更高的分辨率,而较深层的特征图具有较低的分辨率。对于特征图的不同通道(例如,25个小图像的第一个网格),它们看起来像是模糊版本的输入图像,并突出显示了不同的特征。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
