分享

利用PyTorch实现LeNet-5

 taotao_2016 2020-05-12

利用PyTorch实现LeNet-5

在本文中,我将简要描述LeNet-5体系结构,并展示如何在PyTorch中实现LeNet-5。本文假定您对卷积神经网络的基础知识有一定的了解,包括卷积层、池化层、全连接层等概念。

理论介绍

LeNet-5是一个7层卷积神经网络,是在大小为32x32像素的灰度图像上进行训练的。

利用PyTorch实现LeNet-5

命名约定:

  • Cx:卷积层,
  • Sx:subsampling(池化)层,
  • Fx:全连接层,
  • x :层的索引。

鉴于LeNet-5是相对简单的现代标准,我们可以分别检查每一层,以获得对架构的良好理解。但是,在继续之前,有必要提醒一下卷积层输出大小的计算公式。

公式为:(W−F+2P)/S+1,其中W是输入的高度/宽度(通常图像是正方形,因此不需要区分两者),F是filter/核的大小,P是padding,S是stride。

看了上面的体系结构架构和公式后,我们可以遍历LeNet-5的每一层。

  1. 第1层(C1):第一个卷积层,具有6个大小为5×5的kernel, stride为1。给定输入大小(32×32×1),该层的输出大小为28×28×6。
  2. 第2层(S2):具有6个2×2大小的核且stride为2的子采样/池化层。原始体系结构中的子采样层比传统使用的最大(或平均)池化层复杂一些。
  3. 第3层(C3):第二个卷积层的配置与第一个相同,但是这次有16个filters。该层的输出为10×10×16。
  4. 第4层(S4):第二个池化层。逻辑与上一个相同,但是该层具有16个filters。该层的输出大小为5×5×16。
  5. 第5层(C5):具有120个5×5核的卷积层。假设该层的输入大小为5×5×16,核大小为5×5,则输出为1×1×120,因此,S4层和C5层是全连接的,这就是为什么在LeNet-5的某些实现中实际上使用全连接层而不是卷积层作为第五层的原因。将此层保留为卷积层的原因是,如果网络的输入大于论文中使用的输入(32×32),则该层将不是全连接的,每个核的输出将不是1×1。
  6. 第6层(F6):全连接层,输入120个units,返回84个units。在最初的论文中,作者使用了一个定制的激活函数:tanh激活函数的变体。
  7. 第7层(F7):最后一个是dense层,输出10个单位。论文作者使用欧几里德径向基函数神经元作为该层的激活函数。

PyTorch实现

现在,我将展示如何在PyTorch中实现LeNet-5(略有简化)。我们将在MNIST数据集上训练网络。我们首先导入必需的Python库。

import numpy as npfrom datetime import datetime import torchimport torch.nn as nnimport torch.nn.functional as Ffrom torch.utils.data import DataLoaderfrom torchvision import datasets, transformsimport matplotlib.pyplot as plt# check deviceDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'

此外,我们还检查了GPU是否可用,并相应地设置DEVICE变量。在下一步中,我们设置一些参数(例如随机种子、学习率、batch size、epochs数等),稍后将在建立神经网络时使用这些参数。

%config InlineBackend.figure_format = 'retina'# parametersRANDOM_SEED = 42LEARNING_RATE = 0.001BATCH_SIZE = 32N_EPOCHS = 15IMG_SIZE = 32N_CLASSES = 10

接下来我们定义一些用于训练神经网络的辅助函数。由于大多数情况下的总体思想非常相似,您可以根据自己的需要稍微修改函数,并将它们用于训练各种网络。我们从训练部分的函数开始:

def train(train_loader, model, criterion, optimizer, device): ''' Function for the training step of the training loop ''' model.train() running_loss = 0 for X, y_true in train_loader: optimizer.zero_grad() X = X.to(device) y_true = y_true.to(device) # Forward pass y_hat, _ = model(X) loss = criterion(y_hat, y_true) running_loss += loss.item() * X.size(0) # Backward pass loss.backward() optimizer.step() epoch_loss = running_loss / len(train_loader.dataset) return model, optimizer, epoch_loss

在train函数中,对于每一批实例,我们执行以下步骤:

  1. 执行forward pass ,使用当前权重获取批次的预测
  2. 计算损失函数的值
  3. 执行backward pass,根据损失调整权重。

请注意,对于训练阶段,模型处于训练模式(model.train())中,我们还需要将每个批次的梯度归零。此外,我们还计算了训练步骤中的运行损失。

然后,我们定义负责验证的函数。

def validate(valid_loader, model, criterion, device):    '''    Function for the validation step of the training loop    '''       model.eval()    running_loss = 0        for X, y_true in valid_loader:            X = X.to(device)        y_true = y_true.to(device)        # Forward pass and record loss        y_hat, _ = model(X)         loss = criterion(y_hat, y_true)         running_loss += loss.item() * X.size(0)    epoch_loss = running_loss / len(valid_loader.dataset)            return model, epoch_loss

验证函数与训练函数非常相似,区别在于缺少实际的学习步骤(backward pass)。最后,我们在训练循环中将它们结合起来:

def get_accuracy(model, data_loader, device): ''' Function for computing the accuracy of the predictions over the entire data_loader ''' correct_pred = 0 n = 0 with torch.no_grad(): model.eval() for X, y_true in data_loader: X = X.to(device) y_true = y_true.to(device) _, y_prob = model(X) _, predicted_labels = torch.max(y_prob, 1) n += y_true.size(0) correct_pred += (predicted_labels == y_true).sum() return correct_pred.float() / ndef plot_losses(train_losses, valid_losses): ''' Function for plotting training and validation losses ''' # temporarily change the style of the plots to seaborn plt.style.use('seaborn') train_losses = np.array(train_losses) valid_losses = np.array(valid_losses) fig, ax = plt.subplots(figsize = (8, 4.5)) ax.plot(train_losses, color='blue', label='Training loss') ax.plot(valid_losses, color='red', label='Validation loss') ax.set(title='Loss over epochs', xlabel='Epoch', ylabel='Loss') ax.legend() fig.show() # change the plot style to default plt.style.use('default')def training_loop(model, criterion, optimizer, train_loader, valid_loader, epochs, device, print_every=1): ''' Function defining the entire training loop ''' # set objects for storing metrics best_loss = 1e10 train_losses = [] valid_losses = [] # Train model for epoch in range(0, epochs): # training model, optimizer, train_loss = train(train_loader, model, criterion, optimizer, device) train_losses.append(train_loss) # validation with torch.no_grad(): model, valid_loss = validate(valid_loader, model, criterion, device) valid_losses.append(valid_loss) if epoch % print_every == (print_every - 1): train_acc = get_accuracy(model, train_loader, device=device) valid_acc = get_accuracy(model, valid_loader, device=device) print(f'{datetime.now().time().replace(microsecond=0)} --- ' f'Epoch: {epoch}\t' f'Train loss: {train_loss:.4f}\t' f'Valid loss: {valid_loss:.4f}\t' f'Train accuracy: {100 * train_acc:.2f}\t' f'Valid accuracy: {100 * valid_acc:.2f}') plot_losses(train_losses, valid_losses) return model, optimizer, (train_losses, valid_losses)

在训练循环中,对于每个epoch,我们同时运行训练和验证函数,后者使用torch.no_grad()运行,以便不更新权重和节省一些计算时间。除了用于训练的损失函数之外,我们还使用定制的get_accuracy函数计算模型对于训练和验证步骤的准确性。

定义了辅助函数后,就该准备数据了。我们使用MNIST机器学习数据集。

# define transforms# transforms.ToTensor() automatically scales the images to [0,1] rangetransforms = transforms.Compose([transforms.Resize((32, 32)),                                 transforms.ToTensor()])# download and create datasetstrain_dataset = datasets.MNIST(root='mnist_data',                                train=True,                                transform=transforms,                               download=True)valid_dataset = datasets.MNIST(root='mnist_data',                                train=False,                                transform=transforms)# define the data loaderstrain_loader = DataLoader(dataset=train_dataset,                           batch_size=BATCH_SIZE,                           shuffle=True)valid_loader = DataLoader(dataset=valid_dataset,                           batch_size=BATCH_SIZE,                           shuffle=False)

在上面的代码段中,我们首先定义了一组要应用于源图像的转换。我们首先将图像调整为32×32(LeNet-5的输入大小),然后将其转换为张量。transforms.ToTensor()自动将图像缩放到[0,1]范围内。除了调整图像的大小,我们还可以对图像应用某种填充。在最简单的情况下,我们只需在原始图像的每一侧添加2个零。

第二步是定义机器学习数据集。对于训练对象,我们指定了download=True来下载数据集。

最后,我们进行DataLoaders实例化。

接下来,您可以查看来自训练集的50张图像的预览。

ROW_IMG = 10N_ROWS = 5fig = plt.figure()for index in range(1, ROW_IMG * N_ROWS + 1): plt.subplot(N_ROWS, ROW_IMG, index) plt.axis('off') plt.imshow(train_dataset.data[index], cmap='gray_r')fig.suptitle('MNIST Dataset - preview');

利用PyTorch实现LeNet-5

最后,是时候定义LeNet-5架构了。

class LeNet5(nn.Module):    def __init__(self, n_classes):        super(LeNet5, self).__init__()                self.feature_extractor = nn.Sequential(                        nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1),            nn.Tanh(),            nn.AvgPool2d(kernel_size=2),            nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1),            nn.Tanh(),            nn.AvgPool2d(kernel_size=2),            nn.Conv2d(in_channels=16, out_channels=120, kernel_size=5, stride=1),            nn.Tanh()        )        self.classifier = nn.Sequential(            nn.Linear(in_features=120, out_features=84),            nn.Tanh(),            nn.Linear(in_features=84, out_features=n_classes),        )    def forward(self, x):        x = self.feature_extractor(x)        x = torch.flatten(x, 1)        logits = self.classifier(x)        probs = F.softmax(logits, dim=1)        return logits, probs

从上面的类定义,您可以看到与原始网络相比的一些简化:

  • 使用常规的tanh激活函数代替论文中描述的自定义函数;
  • 使用平均池化层而不是原始体系结构中使用的更复杂的等效层;
  • 用softmax函数替换输出层中的欧几里德径向基函数激活。

定义类之后,我们需要实例化模型(并将其发送到正确的设备)、优化器(本例中为ADAM)和损失函数(交叉熵)。最后两个与论文中最初使用的方法不同。

torch.manual_seed(RANDOM_SEED)model = LeNet5(N_CLASSES).to(DEVICE)optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)criterion = nn.CrossEntropyLoss()

一切就绪后,我们可以通过运行以下Python代码来训练网络:

model, optimizer, _ = training_loop(model, criterion, optimizer, train_loader, valid_loader, N_EPOCHS, DEVICE)

输出如下:

利用PyTorch实现LeNet-5

利用PyTorch实现LeNet-5

为了评估机器学习模型的预测,我们可以运行以下代码,它显示来自验证集的一组数字,以及预测的标签和网络分配给该标签的概率。

ROW_IMG = 10N_ROWS = 5fig = plt.figure()for index in range(1, ROW_IMG * N_ROWS + 1): plt.subplot(N_ROWS, ROW_IMG, index) plt.axis('off') plt.imshow(valid_dataset.data[index], cmap='gray_r') with torch.no_grad(): model.eval() _, probs = model(valid_dataset[index][0].unsqueeze(0)) title = f'{torch.argmax(probs)} ({torch.max(probs * 100):.0f}%)' plt.title(title, fontsize=7)fig.suptitle('LeNet-5 - predictions');

利用PyTorch实现LeNet-5

结论

在本文中,我描述了LeNet-5的体系结构,并展示了如何使用著名的MNIST数据集来进行实现和训练。由于这是最早的CNN架构之一,因此相对简单易懂。

为了进一步提高网络性能,可能有必要尝试一些数据增强。我们可以对图像应用旋转或剪切等转换(使用torchvision.transforms),以创建更多样化的数据集。我们还应该注意,并不是所有的转换都适用于数字识别,例如通过翻转图像来创建镜像反射。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多