%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 F
from torchvision import datasets, transforms
from sklearn.metrics import confusion_matrix
from datetime import timedelta
torch.__version__
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.')
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)
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_fmaps
def set_return_fmaps(self, v=True):
self.return_fmaps = v
def 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, fmaps
else:
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.')
We define the cross-entropy loss for this task.
交叉熵是分类任务中使用的一种损失函数。该损失函数是一个可微的函数,始终为正,并在模型的预测完全匹配目标值时达到最小值。在训练过程中,我们的模型权重会被更新以最小化该损失函数。
PyTorch内置了一个用于计算交叉熵损失的函数。需要注意的是,该函数在内部将softmax函数和交叉熵损失结合为单个操作,以提高效率,因此在模型定义中我们不需要手动使用softmax函数。
cross_entropy = nn.CrossEntropyLoss()
We define the Adam optimization method in this task.
现在我们有一个需要最小化的损失函数,我们可以创建一个优化器。在这种情况下,我们使用Adam优化器,它是梯度下降的一种高级变体。
lr = 0.001
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
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 loss
loss.backward() # Backward the loss and calculate gradients for parameters.
optimizer.step() # Update the parameters.
return loss
def 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 = 0
correct = 0
with 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 loss
pred = output.argmax(dim=1, keepdim=True) # Get the index of the max log-probability
correct += 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)))
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)
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)
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)
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)
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个小图像的第一个网格),它们看起来像是模糊版本的输入图像,并突出显示了不同的特征。