Machine-Learning-Mastery-PyTorch-教程-三-

77 阅读1小时+

Machine Learning Mastery PyTorch 教程(三)

原文:Machine Learning Mastery

协议:CC BY-NC-SA 4.0

使用 PyTorch 进行深度学习(9 天迷你课程)

原文:machinelearningmastery.com/deep-learning-with-pytorch-9-day-mini-course/

深度学习是一个迷人的研究领域,其技术在一系列具有挑战性的机器学习问题上取得了世界级的成果。开始深入学习深度学习可能有些困难。

你应该使用哪个库,以及应该专注于哪些技术?

在这个由 9 部分组成的速成课程中,你将会使用易于使用且强大的 PyTorch 库发现 Python 中的应用深度学习。这个迷你课程旨在为已经熟悉 Python 编程且了解基本机器学习概念的实践者提供帮助。让我们开始吧。

这是一篇长而实用的文章。你可能想要打印出来。

让我们开始吧。

使用 PyTorch 进行深度学习(9 天迷你课程)

照片由Cosmin Georgian拍摄。部分权利保留。

这个迷你课程适合谁?

在我们开始之前,让我们确保你在正确的地方。下面的列表提供了一些关于这门课程设计对象的一般指导方针。如果你不完全符合这些点,不要惊慌,你可能只需要在某个领域或另一个领域进行一些复习以跟上节奏。

  • 知道如何写一点代码的开发者。这意味着对于你来说,用 Python 完成任务并在工作站上设置生态系统并不是什么大问题(这是一个先决条件)。这并不意味着你是编程巫师,但确实意味着你不怕安装软件包和编写脚本。

  • 了解一点机器学习的开发者。这意味着你了解机器学习的基础知识,如交叉验证、一些算法和偏差-方差权衡。这并不意味着你是机器学习博士,只是说你知道里程碑或知道在哪里查找它们。

这个迷你课程不是一本深度学习的教科书。

它将带领你从一个在 Python 中略懂机器学习的开发者,变成一个能够产生结果并将深度学习的力量引入自己项目的开发者。

迷你课程概述

这个迷你课程分为 9 部分。

每一课设计为一般开发者约 30 分钟完成。有些课可能会更快完成,有些你可能会选择深入学习,花更多时间。

您可以根据自己的节奏完成每一部分。一个舒适的时间表可能是每天完成一课,共九天。强烈推荐。

在接下来的 9 课中,您将学习以下主题:

  • 第 1 课: PyTorch 简介

  • 第 2 课: 构建你的第一个多层感知器模型

  • 第 3 课: 训练一个 PyTorch 模型

  • 第 4 课: 使用 PyTorch 模型进行推断

  • 第 5 课: 从 Torchvision 加载数据

  • 第 6 课: 使用 PyTorch DataLoader

  • 第 7 课:卷积神经网络

  • 第 8 课:训练图像分类器

  • 第 9 课:使用 GPU 训练

这将会非常有趣。

你需要做一些工作,包括一点阅读、一些研究和一点编程。你想学习深度学习,对吧?

在评论中发布你的结果;我会为你加油!

坚持下去,别放弃。

第 01 课:PyTorch 简介

PyTorch 是由 Facebook 创建和发布的一个用于深度学习计算的 Python 库。它源自早期的库 Torch 7,但完全重写了。

这是最受欢迎的两个深度学习库之一。PyTorch 是一个完整的库,具备训练深度学习模型的能力,同时支持在推理模式下运行模型,并支持使用 GPU 以加速训练和推理。它是一个我们不能忽视的平台。

在本课中,你的目标是安装 PyTorch,并熟悉 PyTorch 程序中使用的符号表达式的语法。

例如,你可以使用pip安装 PyTorch。在撰写本文时,PyTorch 的最新版本是 2.0。PyTorch 为每个平台提供了预构建版本,包括 Windows、Linux 和 macOS。只要有一个有效的 Python 环境,pip会为你处理这些,以提供你平台上的最新版本。

除了 PyTorch,还有torchvision库,它通常与 PyTorch 一起使用。它提供了许多有用的函数来帮助计算机视觉项目。

sudo pip install torch torchvision

一个可以作为起点的小示例 PyTorch 程序如下所示:

# Example of PyTorch library
import torch
# declare two symbolic floating-point scalars
a = torch.tensor(1.5)
b = torch.tensor(2.5)
# create a simple symbolic expression using the add function
c = torch.add(a, b)
print(c)

PyTorch 主页上了解更多关于 PyTorch 的信息。

你的任务

重复以上代码以确保你已正确安装 PyTorch。你还可以通过运行以下 Python 代码行来检查你的 PyTorch 版本:

import torch
print(torch.__version__)

在下一课中,你将使用 PyTorch 构建一个神经网络模型。

第 02 课:构建你的第一个多层感知器模型

深度学习是构建大规模神经网络的过程。神经网络的最简单形式称为多层感知器模型。神经网络的构建块是人工神经元或感知器。这些是简单的计算单元,具有加权输入信号,并使用激活函数产生输出信号。

感知器被排列成网络。一排感知器被称为一个层,一个网络可以有多个层。网络中感知器的架构通常称为网络拓扑。一旦配置完成,神经网络需要在你的数据集上进行训练。经典且仍然首选的神经网络训练算法称为随机梯度下降。

简单神经元模型

简单神经元模型

PyTorch 允许你用极少的代码行开发和评估深度学习模型。

在接下来的内容中,你的目标是使用 PyTorch 开发你的第一个神经网络。使用来自 UCI 机器学习库的标准二分类数据集,如Pima Indians 数据集

为了保持简单,网络模型仅由几层全连接感知机组成。在这个特定模型中,数据集有 12 个输入或预测变量,输出是一个 0 或 1 的单一值。因此,网络模型应有 12 个输入(在第一层)和 1 个输出(在最后一层)。你的第一个模型将按如下方式构建:

import torch.nn as nn

model = nn.Sequential(
  nn.Linear(8, 12),
  nn.ReLU(),
  nn.Linear(12, 8),
  nn.ReLU(),
  nn.Linear(8, 1),
  nn.Sigmoid()
)
print(model)

这是一个包含 3 层全连接层的网络。每一层都是使用nn.Linear(x, y)语法在 PyTorch 中创建的,其中第一个参数是输入到该层的数量,第二个参数是输出的数量。在每一层之间,使用了修正线性激活函数,但在输出层,应用了 sigmoid 激活函数,使得输出值介于 0 和 1 之间。这是一个典型的网络。深度学习模型通常会在模型中包含许多这样的层。

你的任务

重复上述代码并观察打印的模型输出。尝试在上述第一个Linear层之后添加另一个输出 20 个值的层。你应该如何修改nn.Linear(12, 8)这一行以适应这个添加的层?

在下一课中,你将看到如何训练这个模型。

课程 03: 训练一个 PyTorch 模型

在 PyTorch 中构建神经网络并没有说明你应该如何为特定任务训练模型。实际上,在这方面有很多变种,这些变种由超参数描述。在 PyTorch 或所有深度学习模型中,你需要决定以下内容来训练模型:

  • 数据集是什么,特别是输入和目标的样子如何?

  • 什么是评估模型拟合数据优度的损失函数?

  • 用于训练模型的优化算法是什么,以及优化算法的参数如学习率和次数是什么?

    训练的迭代次数

在上一课中,使用了 Pima Indians 数据集,并且所有输入都是数字。这将是最简单的情况,因为你不需要对数据进行任何预处理,因为神经网络可以直接处理数字。

由于这是一个二分类问题,因此损失函数应该是二元交叉熵。这意味着模型输出的目标值是 0 或 1,用于分类结果。但在实际中,模型可能输出介于两者之间的任何值。离目标值越近越好(即,损失越低)。

梯度下降是优化神经网络的算法。梯度下降有许多变种,而 Adam 是最常用的算法之一。

实现上述所有内容,加上在上一课中构建的模型,以下是训练过程的代码:

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim

dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

loss_fn = nn.BCELoss() # binary cross-entropy
optimizer = optim.Adam(model.parameters(), lr=0.001)

n_epochs = 100
batch_size = 10
for epoch in range(n_epochs):
    for i in range(0, len(X), batch_size):
        Xbatch = X[i:i+batch_size]
        y_pred = model(Xbatch)
        ybatch = y[i:i+batch_size]
        loss = loss_fn(y_pred, ybatch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    print(f'Finished epoch {epoch}, latest loss {loss}')

上述的 for 循环用于获取一个批次的数据并将其输入到模型中。然后观察模型的输出并计算损失函数。根据损失函数,优化器将对模型进行一步微调,以便更好地匹配训练数据。经过若干次更新步骤后,模型应该足够接近训练数据,以便能够以较高的准确率预测目标。

你的任务

运行上述训练循环,并观察随着训练循环的进行,损失如何减少。

在下一课中,你将看到如何使用训练好的模型。

课程 04:使用 PyTorch 模型进行推断

一个训练好的神经网络模型是一个记住了输入和目标之间关系的模型。然后,该模型可以在给定另一个输入的情况下预测目标。

在 PyTorch 中,一个训练好的模型可以像函数一样运行。假设你已经在前一课中训练了这个模型,你可以简单地如下使用它:

i = 5
X_sample = X[i:i+1]
y_pred = model(X_sample)
print(f"{X_sample[0]} -> {y_pred[0]}")

但实际上,更好的推断方法是如下:

i = 5
X_sample = X[i:i+1]
model.eval()
with torch.no_grad():
    y_pred = model(X_sample)
print(f"{X_sample[0]} -> {y_pred[0]}")

一些模型在训练和推断之间表现不同。model.eval() 这一行是为了告诉模型意图是进行推断。with torch.no_grad() 这一行是为了创建一个运行模型的上下文,以便 PyTorch 知道计算梯度是不必要的。这可以减少资源消耗。

这也是你可以评估模型的方式。模型输出一个 sigmoid 值,该值在 0 和 1 之间。你可以通过将值四舍五入到最接近的整数(即布尔标签)来解释这个值。通过比较四舍五入后的预测与目标匹配的频率,你可以为模型分配一个准确率百分比,如下:

model.eval()
with torch.no_grad():
    y_pred = model(X)
accuracy = (y_pred.round() == y).float().mean()
print(f"Accuracy {accuracy}")

你的任务

运行上述代码,看看你得到的准确率是多少。你应该大致达到 75%。

在下一课中,你将学习关于 torchvision 的内容。

课程 05:从 Torchvision 加载数据

Torchvision 是 PyTorch 的姊妹库。在这个库中,有专门用于图像和计算机视觉的函数。正如你所期望的,有帮助你读取图像或调整对比度的函数。但可能最重要的是提供一个易于获取一些图像数据集的接口。

在下一课中,你将构建一个深度学习模型来分类小图像。这是一个使计算机能够识别图像内容的模型。正如你在前面的课程中看到的,拥有数据集来训练模型是非常重要的。你将要使用的数据集是 CIFAR-10。它是一个包含 10 种不同物体的数据集。还有一个更大的数据集叫做 CIFAR-100。

CIFAR-10 数据集可以从互联网下载。但是如果你已经安装了 torchvision,你只需执行以下操作:

import matplotlib.pyplot as plt
import torchvision

trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True)

fig, ax = plt.subplots(4, 6, sharex=True, sharey=True, figsize=(12,8))
for i in range(0, 24):
    row, col = i//6, i%6
    ax[row][col].imshow(trainset.data[i])
plt.show()

torchvision.datasets.CIFAR10 函数帮助你将 CIFAR-10 数据集下载到本地目录。数据集分为训练集和测试集。因此,上面的两行代码是为了获取它们。然后你绘制从下载的数据集中获得的前 24 张图像。数据集中的每张图像是 32×32 像素的以下任意一种:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船或卡车。

你的任务

根据上述代码,你能找到一种方法来分别计算训练集和测试集中总共有多少张图像吗?

在下一课中,你将学习如何使用 PyTorch DataLoader。

课程 06:使用 PyTorch DataLoader

上一课中的 CIFAR-10 图像确实是 numpy 数组格式。但为了供 PyTorch 模型使用,它需要是 PyTorch 张量。将 numpy 数组转换为 PyTorch 张量并不难,但在训练循环中,你仍然需要将数据集划分为批次。PyTorch DataLoader 可以帮助你使这个过程更加顺畅。

返回到上一课中加载的 CIFAR-10 数据集,你可以做以下操作以实现相同的效果:

import matplotlib.pyplot as plt
import torchvision
import torch
from torchvision.datasets import CIFAR10

transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
trainset = CIFAR10(root='./data', train=True, download=True, transform=transform)
testset = CIFAR10(root='./data', train=False, download=True, transform=transform)

batch_size = 24
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=True)

fig, ax = plt.subplots(4, 6, sharex=True, sharey=True, figsize=(12,8))
for images, labels in trainloader:
    for i in range(batch_size):
        row, col = i//6, i%6
        ax[row][col].imshow(images[i].numpy().transpose([1,2,0]))
    break  # take only the first batch
plt.show()

在这段代码中,trainset 是通过 transform 参数创建的,这样数据在提取时会转换为 PyTorch 张量。这是在 DataLoader 后续的行中执行的。DataLoader 对象是一个 Python 可迭代对象,你可以提取输入(即图像)和目标(即整数类别标签)。在这种情况下,你将批量大小设置为 24,并迭代第一个批次。然后你展示批次中的每张图像。

你的任务

运行上面的代码,并与你在上一课中生成的 matplotlib 输出进行比较。你应该会看到输出不同。为什么?在DataLoader行中有一个参数导致了这个差异。你能找出是哪一个吗?

在下一课中,你将学习如何构建深度学习模型来分类 CIFAR-10 数据集中的图像。

课程 07:卷积神经网络

图像是二维结构。你可以通过将其展开为一维向量来轻松地转换它们,并构建神经网络模型对其进行分类。但已知保留二维结构更为合适,因为分类涉及的是图像中的内容,这具有平移不变性

处理图像的神经网络标准方法是使用卷积层。使用卷积层的神经网络称为卷积神经网络。示例如下:

import torch.nn as nn

model = nn.Sequential(
    nn.Conv2d(3, 32, kernel_size=(3,3), stride=1, padding=1),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Conv2d(32, 32, kernel_size=(3,3), stride=1, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=(2, 2)),
    nn.Flatten(),
    nn.Linear(8192, 512),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(512, 10)
)
print(model)

在上述内容中,我们使用了多次 Conv2d 层以及 ReLU 激活。卷积层用于学习和提取图像的 特征。你添加的卷积层越多,网络可以学习到更多的高级特征。最终,你会使用一个池化层(上面的 MaxPool2d)来对提取的特征进行分组,将它们展平为一个向量,然后传递给一个多层感知机网络进行最终分类。这是图像分类模型的常见结构。

你的任务

运行上述代码以确保你可以正确创建一个模型。你没有在模型中指定输入图像的大小,但它实际上被固定为 32×32 像素的 RGB(即 3 个颜色通道)。这一固定设置在网络中在哪里?

在下一课中,你将使用上一课中的 DataLoader 来训练上述模型。

课程 08:训练图像分类器

配合为 CIFAR-10 数据集创建的 DataLoader,你可以使用以下训练循环来训练前一课中的卷积神经网络:

import torch.nn as nn
import torch.optim as optim

loss_fn = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

n_epochs = 20
for epoch in range(n_epochs):
    model.train()
    for inputs, labels in trainloader:
        y_pred = model(inputs)
        loss = loss_fn(y_pred, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    acc = 0
    count = 0
    model.eval()
    with torch.no_grad():
        for inputs, labels in testloader:
            y_pred = model(inputs)
            acc += (torch.argmax(y_pred, 1) == labels).float().sum()
            count += len(labels)
    acc /= count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

这将需要一些时间运行,你应该看到模型能够达到至少 70% 的准确率。

这个模型是一个多类别分类网络。输出不是一个,而是多个分数,每个类别一个。我们认为分数越高,模型越有信心图像属于某个类别。因此,使用的损失函数是 交叉熵,即多类别版本的二元交叉熵。

在上述训练循环中,你应该会看到许多你在前面课程中学到的元素,包括在模型中切换训练模式和推理模式,使用 torch.no_grad() 上下文,以及准确率的计算。

你的任务

阅读上述代码以确保你理解它的作用。运行此代码以观察随着训练的进行准确率的提高。你最终达到了什么准确率?

在下一课中,你将学习如何使用 GPU 加速同一模型的训练。

课程 09:使用 GPU 进行训练

你在上一课中进行的模型训练应该需要一段时间。如果你有支持的 GPU,你可以大大加快训练速度。

在 PyTorch 中使用 GPU 的方法是先将模型和数据发送到 GPU,然后可以选择将结果从 GPU 发送回 CPU,或者直接在 GPU 上进行评估。

修改上一课的代码以使用 GPU 并不困难。下面是需要做的内容:

import torch.nn as nn
import torch.optim as optim

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)

loss_fn = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

n_epochs = 20
for epoch in range(n_epochs):
    model.train()
    for inputs, labels in trainloader:
        y_pred = model(inputs.to(device))
        loss = loss_fn(y_pred, labels.to(device))
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    acc = 0
    count = 0
    model.eval()
    with torch.no_grad():
        for inputs, labels in testloader:
            y_pred = model(inputs.to(device))
            acc += (torch.argmax(y_pred, 1) == labels.to(device)).float().sum()
            count += len(labels)
    acc /= count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

所做的更改如下:你检查 GPU 是否可用并相应地设置 device。然后将模型发送到该设备。当输入(即一批图像)传递到模型时,它也应该首先发送到相应的设备。由于模型输出也会在那里,因此损失计算或准确率计算也应将目标首先发送到 GPU。

你的任务

你可以看到,在 CPU 和 GPU 上运行 PyTorch 的方式大致相同。如果你可以访问到 GPU,尝试比较这两者的速度。你能观察到快了多少?

这是最后一课。

结束!(看看你走了多远

你做到了。做得好!

花点时间回顾一下你走过的路程。

  • 你发现了 PyTorch 作为一个 Python 中的深度学习库。

  • 你使用 PyTorch 构建了你的第一个神经网络,并学习了如何用神经网络进行分类。

  • 你学习了深度学习的关键组成部分,包括损失函数、优化器、训练循环和评估。

  • 最后,你迈出了下一步,学习了关于卷积神经网络以及如何用于计算机视觉任务。

总结

你在迷你课程中表现如何?

你喜欢这个速成课程吗?

你有任何问题吗?有没有什么难点?

让我知道。请在下面留言。

逐步开发你的第一个 PyTorch 神经网络

原文:machinelearningmastery.com/develop-your-first-neural-network-with-pytorch-step-by-step/

PyTorch 是一个强大的用于构建深度学习模型的 Python 库。它提供了定义和训练神经网络以及用于推理的一切所需工具。你不需要写很多代码就能完成所有这些。在这篇文章中,你将了解如何使用 PyTorch 在 Python 中创建你的第一个深度学习神经网络模型。完成本文后,你将了解到:

  • 如何加载 CSV 数据集并准备用于 PyTorch 使用

  • 如何在 PyToch 中定义多层感知器模型

  • 如何在验证数据集上训练和评估 PyToch 模型

使用我的书 Deep Learning with PyTorch 快速启动你的项目。它提供了带有工作代码的自学教程

让我们开始吧!

逐步开发你的第一个 PyTorch 神经网络

照片由 drown_ in_city 拍摄。部分权利保留。

概述

需要的代码不多。你将会慢慢过一遍,这样你将来就会知道如何创建自己的模型。本文你将学到的步骤如下:

  • 加载数据

  • 定义 PyToch 模型

  • 定义损失函数和优化器

  • 运行训练循环

  • 评估模型

  • 进行预测

加载数据

第一步是定义本文中打算使用的函数和类。你将使用 NumPy 库加载你的数据集,并使用 PyTorch 库进行深度学习模型。

下面列出了所需的导入:

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim

现在你可以加载你的数据集了。

在本文中,你将使用 Pima Indians 糖尿病发作数据集。这是自该领域早期以来的标准机器学习数据集。它描述了 Pima 印第安人的患者医疗记录数据及其在五年内是否有糖尿病发作。

这是一个二元分类问题(糖尿病的发作为 1,否则为 0)。描述每个患者的所有输入变量都被转换为数值。这使得它可以直接与期望数值输入和输出的神经网络一起使用,并且是我们在 PyTorch 中首次尝试神经网络的理想选择。

你也可以在这里下载它 here

下载数据集并将其放在本地工作目录中,与你的 Python 文件位于同一位置。将其保存为文件名 pima-indians-diabetes.csv。打开文件后,你应该看到类似以下的数据行:

6,148,72,35,0,33.6,0.627,50,1
1,85,66,29,0,26.6,0.351,31,0
8,183,64,0,0,23.3,0.672,32,1
1,89,66,23,94,28.1,0.167,21,0
0,137,40,35,168,43.1,2.288,33,1
...

你现在可以使用 NumPy 函数loadtxt()将文件作为数字矩阵加载。共有八个输入变量和一个输出变量(最后一列)。你将学习一个模型来将输入变量的行(XX)映射到输出变量(yy),这通常总结为y=f(X)y = f(X)。变量总结如下:

输入变量(XX):

  1. 怀孕次数

  2. 口服葡萄糖耐量测试中 2 小时的血浆葡萄糖浓度

  3. 舒张压(mm Hg)

  4. 三头肌皮肤褶皱厚度(mm)

  5. 2 小时血清胰岛素(μIU/ml)

  6. 身体质量指数(体重 kg/(身高 m)²)

  7. 糖尿病家族史功能

  8. 年龄(岁)

输出变量(yy):

  • 类别标签(0 或 1)

一旦 CSV 文件被加载到内存中,你可以将数据列拆分为输入变量和输出变量。

数据将存储在一个二维数组中,其中第一个维度是行,第二个维度是列,例如(行,列)。你可以通过使用标准的 NumPy 切片操作符“:”将数组分割成两个数组。你可以通过切片0:8从索引 0 到索引 7 选择前八列。然后,你可以通过索引 8 选择输出列(第 9 个变量)。

...

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]

但这些数据应该先转换为 PyTorch 张量。一个原因是 PyTorch 通常使用 32 位浮点数,而 NumPy 默认使用 64 位浮点数。大多数操作中不允许混用。转换为 PyTorch 张量可以避免可能引起问题的隐式转换。你也可以借此机会纠正形状以符合 PyTorch 的预期,例如,优选n×1n\times 1矩阵而不是nn-向量。

要转换,请从 NumPy 数组创建张量:

X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

你现在已经准备好定义你的神经网络模型了。

想要开始使用 PyTorch 进行深度学习吗?

现在就来参加我的免费电子邮件速成课程(附有示例代码)。

点击注册并获取课程的免费 PDF 电子书版本。

定义模型

确实,在 PyTorch 中有两种定义模型的方法。目标是将其制作成一个接受输入并返回输出的函数。

一个模型可以定义为一系列层。你可以创建一个Sequential模型,其中列出这些层。为了确保正确,首先需要确认第一层具有正确数量的输入特征。在这个例子中,你可以为八个输入变量指定输入维度8作为一个向量。

确定层的其他参数或模型需要多少层并不是一个简单的问题。你可以使用启发式方法来帮助你设计模型,或者参考其他人在处理类似问题时的设计。通常,最佳的神经网络结构是通过试错实验过程找到的。一般来说,你需要一个足够大的网络来捕捉问题的结构,但又要足够小以提高速度。在这个例子中,我们使用一个具有三层的全连接网络结构。

在 PyTorch 中使用Linear类定义全连接层或密集层。它简单地意味着类似于矩阵乘法的操作。您可以将输入的数量指定为第一个参数,将输出的数量指定为第二个参数。输出的数量有时被称为层中的神经元数或节点数。

在该层之后,您还需要一个激活函数after。如果未提供,您只需将矩阵乘法的输出传递给下一步,或者有时您称之为线性激活,因此该层的名称如此。

在这个例子中,您将在前两个层上使用修正线性单元激活函数(称为 ReLU),并在输出层上使用 sigmoid 函数。

输出层上的 sigmoid 函数确保输出在 0 和 1 之间,这很容易映射到类 1 的概率或通过 0.5 的截止阈值划分为任一类的硬分类。过去,您可能已经在所有层上使用了 sigmoid 和 tanh 激活函数,但事实证明,sigmoid 激活可能导致深度神经网络中的梯度消失问题,而 ReLU 激活则在速度和准确性方面表现更佳。

您可以通过添加每一层来将所有这些部分组合在一起,例如:

  • 该模型期望具有 8 个变量的数据行(第一层的第一个参数设置为8

  • 第一个隐藏层有 12 个神经元,后面跟着一个 ReLU 激活函数

  • 第二个隐藏层有 8 个神经元,后面跟着另一个 ReLU 激活函数

  • 输出层有一个神经元,后面跟着一个 sigmoid 激活函数

...

model = nn.Sequential(
    nn.Linear(8, 12),
    nn.ReLU(),
    nn.Linear(12, 8),
    nn.ReLU(),
    nn.Linear(8, 1),
    nn.Sigmoid()

你可以通过以下方式打印模型:

print(model)

您将看到:

Sequential(
  (0): Linear(in_features=8, out_features=12, bias=True)
  (1): ReLU()
  (2): Linear(in_features=12, out_features=8, bias=True)
  (3): ReLU()
  (4): Linear(in_features=8, out_features=1, bias=True)
  (5): Sigmoid()
)

您可以自由更改设计并查看是否比本文后续部分获得更好或更差的结果。

但请注意,在 PyTorch 中,有一种更冗长的创建模型的方式。上面的模型可以作为从nn.Module继承的 Python class来创建:

...

class PimaClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden1 = nn.Linear(8, 12)
        self.act1 = nn.ReLU()
        self.hidden2 = nn.Linear(12, 8)
        self.act2 = nn.ReLU()
        self.output = nn.Linear(8, 1)
        self.act_output = nn.Sigmoid()

    def forward(self, x):
        x = self.act1(self.hidden1(x))
        x = self.act2(self.hidden2(x))
        x = self.act_output(self.output(x))
        return x

model = PimaClassifier()
print(model)

在这种情况下,打印出的模型将是:

PimaClassifier(
  (hidden1): Linear(in_features=8, out_features=12, bias=True)
  (act1): ReLU()
  (hidden2): Linear(in_features=12, out_features=8, bias=True)
  (act2): ReLU()
  (output): Linear(in_features=8, out_features=1, bias=True)
  (act_output): Sigmoid()
)

在这种方法中,一个类需要在构造函数中定义所有的层,因为在创建时需要准备所有的组件,但是输入尚未提供。请注意,您还需要调用父类的构造函数(super().__init__()行)来启动您的模型。您还需要在类中定义一个forward()函数,以告诉输入张量x如何生成返回的输出张量。

您可以从上面的输出中看到,模型记住了您如何调用每一层。

训练准备

一个定义好的模型已经准备好进行训练,但你需要指定训练的目标。在这个例子中,数据有输入特征XX和输出标签yy。你希望神经网络模型产生一个尽可能接近yy的输出。训练网络意味着找到将输入映射到数据集中输出的最佳权重集。损失函数是用来衡量预测距离yy的指标。在这个例子中,你应该使用二元交叉熵,因为这是一个二分类问题。

一旦你决定了损失函数,你还需要一个优化器。优化器是你用来逐步调整模型权重以产生更好输出的算法。可以选择许多优化器,在这个例子中使用的是 Adam。这个流行的梯度下降版本可以自动调整自己,并在广泛的问题中提供良好的结果。

loss_fn = nn.BCELoss()  # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.001)

优化器通常具有一些配置参数。最重要的是学习率lr。但所有优化器都需要知道优化的内容。因此,你需要传递model.parameters(),这是你创建的模型中所有参数的生成器。

训练模型

你已经定义了你的模型、损失度量和优化器。通过在一些数据上执行模型,它已经准备好进行训练。

训练神经网络模型通常需要轮次和批次。这些术语用于描述数据如何传递给模型:

  • 轮次:将整个训练数据集传递给模型一次。

  • 批次:一个或多个传递给模型的样本,从中梯度下降算法将执行一次迭代。

简而言之,整个数据集被分成批次,你通过训练循环将批次一个一个传递给模型。一旦你用完了所有批次,你就完成了一轮。然后,你可以用相同的数据集重新开始,开始第二轮,继续优化模型。这个过程会重复,直到你对模型的输出感到满意为止。

批次的大小受系统内存的限制。此外,所需的计算量与批次的大小成线性比例。多个轮次中的批次数量决定了你进行梯度下降以优化模型的次数。这是一个权衡,你希望有更多的梯度下降迭代以便产生更好的模型,但同时又不希望训练时间过长。轮次和批次的大小可以通过试验和错误的方法来选择。

训练模型的目标是确保它学习到一个足够好的输入数据到输出分类的映射。它不会是完美的,错误是不可避免的。通常,你会看到在后期轮次中错误的数量减少,但最终会趋于平稳。这被称为模型收敛。

构建训练循环的最简单方法是使用两个嵌套的 for 循环,一个用于轮次,一个用于批次:

n_epochs = 100
batch_size = 10

for epoch in range(n_epochs):
    for i in range(0, len(X), batch_size):
        Xbatch = X[i:i+batch_size]
        y_pred = model(Xbatch)
        ybatch = y[i:i+batch_size]
        loss = loss_fn(y_pred, ybatch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    print(f'Finished epoch {epoch}, latest loss {loss}')

当运行时,它将打印以下内容:

Finished epoch 0, latest loss 0.6271069645881653
Finished epoch 1, latest loss 0.6056771874427795
Finished epoch 2, latest loss 0.5916517972946167
Finished epoch 3, latest loss 0.5822567939758301
Finished epoch 4, latest loss 0.5682642459869385
Finished epoch 5, latest loss 0.5640913248062134
...

评估模型

你已经在整个数据集上训练了我们的神经网络,你可以在相同的数据集上评估网络的性能。这将只给你一个关于你如何建模数据集的概念(例如,训练准确率),但无法了解算法在新数据上的表现。这是为了简化,但理想情况下,你可以将数据分为训练和测试数据集,用于模型的训练和评估。

你可以按照训练时调用模型的方式,在训练数据集上评估你的模型。这将为每个输入生成预测,但你仍然需要计算一个评价分数。这个分数可以与你的损失函数相同,也可以不同。因为你正在进行二分类,你可以通过将输出(范围在 0 到 1 之间的浮点数)转换为整数(0 或 1)来使用准确率作为评价分数,并与我们已知的标签进行比较。

这可以如下进行:

# compute accuracy (no_grad is optional)
with torch.no_grad():
    y_pred = model(X)

accuracy = (y_pred.round() == y).float().mean()
print(f"Accuracy {accuracy}")

round() 函数将浮点数舍入到最接近的整数。== 操作符进行比较并返回一个布尔张量,这可以转换为浮点数 1.0 和 0.0。mean() 函数将提供 1 的数量(即,预测与标签匹配)除以样本总数。no_grad() 上下文是可选的,但建议使用,这样你可以让 y_pred 不用记住它是如何得出这个数的,因为你不会对其进行微分。

把所有内容整合在一起,以下是完整的代码。

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]

X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

# define the model
model = nn.Sequential(
    nn.Linear(8, 12),
    nn.ReLU(),
    nn.Linear(12, 8),
    nn.ReLU(),
    nn.Linear(8, 1),
    nn.Sigmoid()
)
print(model)

# train the model
loss_fn   = nn.BCELoss()  # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.001)

n_epochs = 100
batch_size = 10

for epoch in range(n_epochs):
    for i in range(0, len(X), batch_size):
        Xbatch = X[i:i+batch_size]
        y_pred = model(Xbatch)
        ybatch = y[i:i+batch_size]
        loss = loss_fn(y_pred, ybatch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    print(f'Finished epoch {epoch}, latest loss {loss}')

# compute accuracy (no_grad is optional)
with torch.no_grad():
    y_pred = model(X)
accuracy = (y_pred.round() == y).float().mean()
print(f"Accuracy {accuracy}")

你可以将所有代码复制到你的 Python 文件中,并将其保存为“pytorch_network.py”在与你的数据文件“pima-indians-diabetes.csv”相同的目录下。然后,你可以从命令行运行 Python 文件作为脚本。

运行这个示例时,你应该会看到每个时期的训练循环进展,最后打印出最终的准确率。理想情况下,你希望损失降低到零,准确率达到 1.0(例如,100%)。但对于大多数非平凡的机器学习问题,这是不可能的。相反,你的模型总会有一些误差。目标是选择一个模型配置和训练配置,以实现给定数据集上最低的损失和最高的准确率。

神经网络是随机算法,这意味着相同的数据上,相同的算法每次运行代码时都可以训练出不同的模型,具有不同的技能。这是一种特性,而不是错误。模型性能的差异意味着,为了获得对模型性能的合理近似,你可能需要多次训练,并计算准确率分数的平均值。例如,下面是重新运行示例五次得到的准确率分数:

Accuracy: 0.7604166865348816
Accuracy: 0.7838541865348816
Accuracy: 0.7669270634651184
Accuracy: 0.7721354365348816
Accuracy: 0.7669270634651184

你可以看到所有的准确率分数大约在 77%左右。

做出预测

你可以修改上述示例,并将其用于生成训练数据集上的预测,假装这是一个你之前未见过的新数据集。进行预测就像调用模型作为一个函数一样简单。你在输出层上使用了 sigmoid 激活函数,因此预测值将在 0 和 1 之间的范围内表示概率。你可以通过四舍五入将它们轻松转换为这个分类任务的明确二元预测。例如:

...

# make probability predictions with the model
predictions = model(X)
# round predictions
rounded = predictions.round()

另外,你可以将概率转换为 0 或 1,直接预测明确的类别;例如:

...
# make class predictions with the model
predictions = (model(X) > 0.5).int()

下面的完整示例对数据集中的每个示例进行预测,然后打印出数据集前五个示例的输入数据、预测类别和预期类别。

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]

X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

# define the model
class PimaClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden1 = nn.Linear(8, 12)
        self.act1 = nn.ReLU()
        self.hidden2 = nn.Linear(12, 8)
        self.act2 = nn.ReLU()
        self.output = nn.Linear(8, 1)
        self.act_output = nn.Sigmoid()

    def forward(self, x):
        x = self.act1(self.hidden1(x))
        x = self.act2(self.hidden2(x))
        x = self.act_output(self.output(x))
        return x

model = PimaClassifier()
print(model)

# train the model
loss_fn   = nn.BCELoss()  # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.001)

n_epochs = 100
batch_size = 10

for epoch in range(n_epochs):
    for i in range(0, len(X), batch_size):
        Xbatch = X[i:i+batch_size]
        y_pred = model(Xbatch)
        ybatch = y[i:i+batch_size]
        loss = loss_fn(y_pred, ybatch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

# compute accuracy
y_pred = model(X)
accuracy = (y_pred.round() == y).float().mean()
print(f"Accuracy {accuracy}")

# make class predictions with the model
predictions = (model(X) > 0.5).int()
for i in range(5):
    print('%s => %d (expected %d)' % (X[i].tolist(), predictions[i], y[i]))

这段代码使用了不同的构建模型的方法,但功能上应该与之前相同。模型训练完成后,会对数据集中所有示例进行预测,并打印出前五个示例的输入行和预测类别值,并与预期类别值进行比较。你可以看到大多数行的预测是正确的。实际上,根据你在上一节对模型性能的估计,你可以预期大约 77%的行会被正确预测。

[6.0, 148.0, 72.0, 35.0, 0.0, 33.599998474121094, 0.6269999742507935, 50.0] => 1 (expected 1)
[1.0, 85.0, 66.0, 29.0, 0.0, 26.600000381469727, 0.35100001096725464, 31.0] => 0 (expected 0)
[8.0, 183.0, 64.0, 0.0, 0.0, 23.299999237060547, 0.671999990940094, 32.0] => 1 (expected 1)
[1.0, 89.0, 66.0, 23.0, 94.0, 28.100000381469727, 0.16699999570846558, 21.0] => 0 (expected 0)
[0.0, 137.0, 40.0, 35.0, 168.0, 43.099998474121094, 2.2880001068115234, 33.0] => 1 (expected 1)

进一步阅读

要了解更多关于深度学习和 PyTorch 的信息,可以查看以下内容:

书籍

API

总结

在这篇文章中,你了解了如何使用 PyTorch 创建你的第一个神经网络模型。具体来说,你学习了使用 PyTorch 一步一步创建神经网络或深度学习模型的关键步骤,包括:

  • 如何加载数据

  • 如何在 PyTorch 中定义神经网络

  • 如何在数据上训练模型

  • 如何评估模型

  • 如何使用模型进行预测

使用 PyTorch 中的 LeNet5 模型进行手写数字识别

原文:machinelearningmastery.com/handwritten-digit-recognition-with-lenet5-model-in-pytorch/

深度学习技术的一个流行演示是图像数据中的对象识别。机器学习和深度学习中的“hello world”是用于手写数字识别的 MNIST 数据集。在本帖中,你将发现如何开发一个深度学习模型,以在 MNIST 手写数字识别任务中达到接近最先进的性能。完成本章后,你将了解:

  • 如何使用 torchvision 加载 MNIST 数据集

  • 如何为 MNIST 问题开发和评估基线神经网络模型

  • 如何实现和评估一个简单的卷积神经网络用于 MNIST

  • 如何为 MNIST 实现最先进的深度学习模型

通过我的书籍 《PyTorch 深度学习》 启动你的项目。它提供了自学教程实用代码

让我们开始吧!

使用 PyTorch 中的 LeNet5 模型进行手写数字识别

照片由 Johnny Wong 提供。部分权利保留。

概述

本帖分为五个部分,它们是:

  • MNIST 手写数字识别问题

  • 在 PyTorch 中加载 MNIST 数据集

  • 使用多层感知机的基线模型

  • 用于 MNIST 的简单卷积神经网络

  • LeNet5 用于 MNIST

MNIST 手写数字识别问题

MNIST 问题是一个经典问题,可以展示卷积神经网络的强大。MNIST 数据集由 Yann LeCun、Corinna Cortes 和 Christopher Burges 开发,用于评估机器学习模型在手写数字分类问题上的表现。该数据集由来自国家标准与技术研究院(NIST)的多个扫描文档数据集构成。这也是数据集名称的来源,称为 Modified NIST 或 MNIST 数据集。

数字图像来自各种扫描文档,经过大小标准化和居中处理。这使得该数据集非常适合评估模型,开发人员可以专注于机器学习,数据清理或准备工作最小化。每个图像是一个 28×28 像素的灰度方块(总共 784 像素)。数据集的标准拆分用于评估和比较模型,其中 60,000 张图像用于训练模型,另有 10,000 张图像用于测试。

这个问题的目标是识别图像上的数字。需要预测十个数字(0 到 9)或十个类别。当前最先进的预测准确率达到 99.8%,这是通过大型卷积神经网络实现的。

想要开始使用 PyTorch 进行深度学习?

现在就参加我的免费电子邮件速成课程(包含示例代码)。

点击注册并获取课程的免费 PDF 电子书版本。

在 PyTorch 中加载 MNIST 数据集

torchvision库是 PyTorch 的一个姊妹项目,提供用于计算机视觉任务的专门功能。torchvision中有一个函数可以下载 MNIST 数据集以供 PyTorch 使用。第一次调用此函数时,数据集会被下载并存储在本地,因此以后不需要再次下载。下面是一个小脚本,用于下载和可视化 MNIST 数据集训练子集中的前 16 张图像。

import matplotlib.pyplot as plt
import torchvision

train = torchvision.datasets.MNIST('./data', train=True, download=True)

fig, ax = plt.subplots(4, 4, sharex=True, sharey=True)
for i in range(4):
    for j in range(4):
        ax[i][j].imshow(train.data[4*i+j], cmap="gray")
plt.show()

多层感知器的基准模型

你真的需要像卷积神经网络这样的复杂模型来获得 MNIST 的最佳结果吗?使用一个非常简单的神经网络模型(具有单隐藏层)也可以获得良好的结果。在本节中,你将创建一个简单的多层感知器模型,其准确率达到 99.81%。你将用这个模型作为与更复杂的卷积神经网络模型比较的基准。首先,让我们检查一下数据的样子:

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision

# Load MNIST data
train = torchvision.datasets.MNIST('data', train=True, download=True)
test = torchvision.datasets.MNIST('data', train=True, download=True)
print(train.data.shape, train.targets.shape)
print(test.data.shape, test.targets.shape)

你应该会看到:

torch.Size([60000, 28, 28]) torch.Size([60000])
torch.Size([10000, 28, 28]) torch.Size([10000])

训练数据集的结构是实例、高度和宽度的三维数组。对于多层感知器模型,你必须将图像降维为像素向量。在这种情况下,28×28 大小的图像将成为 784 个像素输入向量。你可以使用reshape()函数轻松完成此转换。

像素值为 0 到 255 之间的灰度值。使用神经网络模型时,几乎总是一个好主意对输入值进行一些缩放。因为尺度是已知且行为良好的,你可以通过将每个值除以 255 的最大值来非常快速地将像素值归一化到 0 到 1 的范围内。

在接下来的步骤中,你将转换数据集,将其转换为浮点数,并通过缩放浮点值来归一化它们,你可以在下一步轻松完成归一化。

# each sample becomes a vector of values 0-1
X_train = train.data.reshape(-1, 784).float() / 255.0
y_train = train.targets
X_test = test.data.reshape(-1, 784).float() / 255.0
y_test = test.targets

输出目标y_trainy_test是形式为 0 到 9 的整数标签。这是一个多类别分类问题。你可以将这些标签转换为独热编码(one-hot encoding),或者像本例一样保持为整数标签。你将使用交叉熵函数来评估模型的性能,PyTorch 实现的交叉熵函数可以应用于独热编码的目标或整数标签的目标。

现在你可以创建你的简单神经网络模型了。你将通过 PyTorch 的Module类来定义你的模型。

class Baseline(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(784, 784)
        self.act1 = nn.ReLU()
        self.layer2 = nn.Linear(784, 10)

    def forward(self, x):
        x = self.act1(self.layer1(x))
        x = self.layer2(x)
        return x

该模型是一个简单的神经网络,具有一个隐藏层,隐藏层的神经元数量与输入数量(784)相同。隐藏层的神经元使用了 rectifier 激活函数。该模型的输出是logits,意味着它们是实数,可以通过 softmax 函数转换为类似概率的值。你不需要显式地应用 softmax 函数,因为交叉熵函数会为你完成这项工作。

你将使用随机梯度下降算法(学习率设置为 0.01)来优化这个模型。训练循环如下:

model = Baseline()

optimizer = optim.SGD(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
loader = torch.utils.data.DataLoader(list(zip(X_train, y_train)), shuffle=True, batch_size=100)

n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in loader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    y_pred = model(X_test)
    acc = (torch.argmax(y_pred, 1) == y_test).float().mean()
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

MNIST 数据集很小。这个例子应该在一分钟内完成,结果如下。这个简单的网络可以达到 92% 的准确率。

Epoch 0: model accuracy 84.11%
Epoch 1: model accuracy 87.53%
Epoch 2: model accuracy 89.01%
Epoch 3: model accuracy 89.76%
Epoch 4: model accuracy 90.29%
Epoch 5: model accuracy 90.69%
Epoch 6: model accuracy 91.10%
Epoch 7: model accuracy 91.48%
Epoch 8: model accuracy 91.74%
Epoch 9: model accuracy 91.96%

下面是上述 MNIST 数据集多层感知机分类的完整代码。

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision

# Load MNIST data
train = torchvision.datasets.MNIST('data', train=True, download=True)
test = torchvision.datasets.MNIST('data', train=True, download=True)

# each sample becomes a vector of values 0-1
X_train = train.data.reshape(-1, 784).float() / 255.0
y_train = train.targets
X_test = test.data.reshape(-1, 784).float() / 255.0
y_test = test.targets

class Baseline(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(784, 784)
        self.act1 = nn.ReLU()
        self.layer2 = nn.Linear(784, 10)

    def forward(self, x):
        x = self.act1(self.layer1(x))
        x = self.layer2(x)
        return x

model = Baseline()

optimizer = optim.SGD(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
loader = torch.utils.data.DataLoader(list(zip(X_train, y_train)), shuffle=True, batch_size=100)

n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in loader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    y_pred = model(X_test)
    acc = (torch.argmax(y_pred, 1) == y_test).float().mean()
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

简单的卷积神经网络用于 MNIST

现在你已经了解了如何使用多层感知机模型对 MNIST 数据集进行分类。接下来,让我们尝试一个卷积神经网络模型。在这一部分,你将创建一个简单的 CNN,用于 MNIST,展示如何使用现代 CNN 实现的所有方面,包括卷积层、池化层和 dropout 层。

在 PyTorch 中,卷积层应该处理图像。图像的张量应该是像素值,维度为 (sample, channel, height, width),但当你使用 PIL 等库加载图像时,像素通常以 (height, width, channel) 的维度呈现。可以使用 torchvision 库中的转换将其转换为适当的张量格式。

...
transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize((0,), (128,)),
])
train = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
test = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(train, shuffle=True, batch_size=100)
testloader = torch.utils.data.DataLoader(test, shuffle=True, batch_size=100)

你需要使用 DataLoader,因为在从 DataLoader 读取数据时会应用转换。

接下来,定义你的神经网络模型。卷积神经网络比标准的多层感知机更复杂,因此你将从使用简单结构开始,这些结构利用了所有元素以实现最先进的结果。下面总结了网络架构。

  1. 第一个隐藏层是一个卷积层,nn.Conv2d()。该层将灰度图像转换为 10 个特征图,滤波器大小为 5×5,并使用 ReLU 激活函数。这是一个输入层,期望输入的图像结构如上所述。

  2. 接下来是一个池化层,取最大值,nn.MaxPool2d()。它配置为 2×2 的池化大小,步幅为 1。它的作用是在每个通道的 2×2 像素块中取最大值,并将该值分配给输出像素。结果是每个通道的特征图为 27×27 像素。

  3. 下一个层是使用 dropout 的正则化层,nn.Dropout()。它配置为随机排除 20% 的神经元,以减少过拟合。

  4. 接下来是一个将 2D 矩阵数据转换为向量的层,使用 nn.Flatten。输入有 10 个通道,每个通道的特征图大小为 27×27。此层允许输出由标准的全连接层处理。

  5. 接下来是一个具有 128 个神经元的全连接层。使用 ReLU 激活函数。

  6. 最后,输出层有十个神经元,用于十个类别。您可以通过在其上应用 softmax 函数将输出转换为类似概率的预测。

此模型使用交叉熵损失和 Adam 优化算法进行训练。实现如下:

class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(1, 10, kernel_size=5, stride=1, padding=2)
        self.relu1 = nn.ReLU()
        self.pool = nn.MaxPool2d(kernel_size=2, stride=1)
        self.dropout = nn.Dropout(0.2)
        self.flat = nn.Flatten()
        self.fc = nn.Linear(27*27*10, 128)
        self.relu2 = nn.ReLU()
        self.output = nn.Linear(128, 10)

    def forward(self, x):
        x = self.relu1(self.conv(x))
        x = self.pool(x)
        x = self.dropout(x)
        x = self.relu2(self.fc(self.flat(x)))
        x = self.output(x)
        return x

model = CNN()

optimizer = optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()

n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in trainloader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    acc = 0
    count = 0
    for X_batch, y_batch in testloader:
        y_pred = model(X_batch)
        acc += (torch.argmax(y_pred, 1) == y_batch).float().sum()
        count += len(y_batch)
    acc = acc / count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

运行上述操作需要几分钟,并产生以下结果:

Epoch 0: model accuracy 81.74%
Epoch 1: model accuracy 85.38%
Epoch 2: model accuracy 86.37%
Epoch 3: model accuracy 87.75%
Epoch 4: model accuracy 88.00%
Epoch 5: model accuracy 88.17%
Epoch 6: model accuracy 88.81%
Epoch 7: model accuracy 88.34%
Epoch 8: model accuracy 88.86%
Epoch 9: model accuracy 88.75%

不是最佳结果,但这展示了卷积层如何工作。

下面是使用简单卷积网络的完整代码。

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision

# Load MNIST data
transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize((0,), (128,)),
])
train = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
test = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(train, shuffle=True, batch_size=100)
testloader = torch.utils.data.DataLoader(test, shuffle=True, batch_size=100)

class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(1, 10, kernel_size=5, stride=1, padding=2)
        self.relu1 = nn.ReLU()
        self.pool = nn.MaxPool2d(kernel_size=2, stride=1)
        self.dropout = nn.Dropout(0.2)
        self.flat = nn.Flatten()
        self.fc = nn.Linear(27*27*10, 128)
        self.relu2 = nn.ReLU()
        self.output = nn.Linear(128, 10)

    def forward(self, x):
        x = self.relu1(self.conv(x))
        x = self.pool(x)
        x = self.dropout(x)
        x = self.relu2(self.fc(self.flat(x)))
        x = self.output(x)
        return x

model = CNN()

optimizer = optim.Adam(model.parameters())
loss_fn = nn.CrossEntropyLoss()

n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in trainloader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    acc = 0
    count = 0
    for X_batch, y_batch in testloader:
        y_pred = model(X_batch)
        acc += (torch.argmax(y_pred, 1) == y_batch).float().sum()
        count += len(y_batch)
    acc = acc / count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

LeNet5 用于 MNIST

前一模型仅具有一个卷积层。当然,您可以添加更多层以构建更深的模型。卷积层在神经网络中的有效性最早的演示之一是“LeNet5”模型。该模型旨在解决 MNIST 分类问题。它有三个卷积层和两个全连接层,共五个可训练层。

在其开发时期,使用双曲正切函数作为激活函数很常见。因此在这里使用它。该模型实现如下:

class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=2)
        self.act1 = nn.Tanh()
        self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)

        self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0)
        self.act2 = nn.Tanh()
        self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)

        self.conv3 = nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0)
        self.act3 = nn.Tanh()

        self.flat = nn.Flatten()
        self.fc1 = nn.Linear(1*1*120, 84)
        self.act4 = nn.Tanh()
        self.fc2 = nn.Linear(84, 10)

    def forward(self, x):
        # input 1x28x28, output 6x28x28
        x = self.act1(self.conv1(x))
        # input 6x28x28, output 6x14x14
        x = self.pool1(x)
        # input 6x14x14, output 16x10x10
        x = self.act2(self.conv2(x))
        # input 16x10x10, output 16x5x5
        x = self.pool2(x)
        # input 16x5x5, output 120x1x1
        x = self.act3(self.conv3(x))
        # input 120x1x1, output 84
        x = self.act4(self.fc1(self.flat(x)))
        # input 84, output 10
        x = self.fc2(x)
        return x

与前一模型相比,LeNet5 没有 Dropout 层(因为 Dropout 层是在 LeNet5 几年后才被发明的),而是使用平均池化代替最大池化(即对 2×2 像素的区域取像素值的平均值而不是最大值)。但 LeNet5 模型最显著的特征是使用步长和填充来将图像尺寸从 28×28 像素减小到 1×1 像素,并将通道数从一个(灰度)增加到 120。

填充意味着在图像边界添加值为 0 的像素,使其稍微变大。没有填充时,卷积层的输出将比其输入小。步幅参数控制滤波器移动以生成输出中的下一个像素。通常为 1 以保持相同大小。如果大于 1,则输出是输入的下采样。因此在 LeNet5 模型中,池化层中使用步幅 2,例如将 28×28 像素图像变为 14×14。

训练该模型与训练之前的卷积网络模型相同,如下所示:

...
model = LeNet5()

optimizer = optim.Adam(model.parameters())
loss_fn = nn.CrossEntropyLoss()

n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in trainloader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    acc = 0
    count = 0
    for X_batch, y_batch in testloader:
        y_pred = model(X_batch)
        acc += (torch.argmax(y_pred, 1) == y_batch).float().sum()
        count += len(y_batch)
    acc = acc / count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

运行此代码可能会看到:

Epoch 0: model accuracy 89.46%
Epoch 1: model accuracy 93.14%
Epoch 2: model accuracy 94.69%
Epoch 3: model accuracy 95.84%
Epoch 4: model accuracy 96.43%
Epoch 5: model accuracy 96.99%
Epoch 6: model accuracy 97.14%
Epoch 7: model accuracy 97.66%
Epoch 8: model accuracy 98.05%
Epoch 9: model accuracy 98.22%

在这里,我们实现了超过 98%的准确率。

以下是完整的代码。

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision

# Load MNIST data
transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize((0,), (128,)),
])
train = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
test = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(train, shuffle=True, batch_size=100)
testloader = torch.utils.data.DataLoader(test, shuffle=True, batch_size=100)

class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=2)
        self.act1 = nn.Tanh()
        self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)

        self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0)
        self.act2 = nn.Tanh()
        self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)

        self.conv3 = nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0)
        self.act3 = nn.Tanh()

        self.flat = nn.Flatten()
        self.fc1 = nn.Linear(1*1*120, 84)
        self.act4 = nn.Tanh()
        self.fc2 = nn.Linear(84, 10)

    def forward(self, x):
        # input 1x28x28, output 6x28x28
        x = self.act1(self.conv1(x))
        # input 6x28x28, output 6x14x14
        x = self.pool1(x)
        # input 6x14x14, output 16x10x10
        x = self.act2(self.conv2(x))
        # input 16x10x10, output 16x5x5
        x = self.pool2(x)
        # input 16x5x5, output 120x1x1
        x = self.act3(self.conv3(x))
        # input 120x1x1, output 84
        x = self.act4(self.fc1(self.flat(x)))
        # input 84, output 10
        x = self.fc2(x)
        return x

model = LeNet5()

optimizer = optim.Adam(model.parameters())
loss_fn = nn.CrossEntropyLoss()

n_epochs = 10
for epoch in range(n_epochs):
    model.train()
    for X_batch, y_batch in trainloader:
        y_pred = model(X_batch)
        loss = loss_fn(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # Validation
    model.eval()
    acc = 0
    count = 0
    for X_batch, y_batch in testloader:
        y_pred = model(X_batch)
        acc += (torch.argmax(y_pred, 1) == y_batch).float().sum()
        count += len(y_batch)
    acc = acc / count
    print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

MNIST 资源

MNIST 数据集已经非常研究。以下是您可能想要查看的一些额外资源。

总结

在这篇文章中,你了解了 MNIST 手写数字识别问题以及使用 Python 和 Keras 库开发的深度学习模型,这些模型能够取得出色的结果。通过这一章节的学习,你学到了:

  • 如何在 PyTorch 中使用 torchvision 加载 MNIST 数据集

  • 如何将 MNIST 数据集转换为 PyTorch 张量,以便卷积神经网络消费

  • 如何使用 PyTorch 创建用于 MNIST 的卷积神经网络模型

  • 如何为 MNIST 分类实现 LeNet5 模型

如何评估 PyTorch 模型的性能

原文:machinelearningmastery.com/how-to-evaluate-the-performance-of-pytorch-models/

设计深度学习模型有时是一门艺术。这里有许多决策点,很难判断哪种方案最好。设计的一种方法是通过试验和错误,并在实际数据上评估结果。因此,拥有科学的方法来评估神经网络和深度学习模型的性能非常重要。事实上,这也是比较任何机器学习模型在特定用途上的方法。

在这篇文章中,你将发现用于稳健评估模型性能的工作流程。在示例中,我们将使用 PyTorch 构建我们的模型,但该方法也适用于其他模型。完成这篇文章后,你将了解:

  • 如何使用验证数据集评估 PyTorch 模型

  • 如何使用 k 折交叉验证评估 PyTorch 模型

使用我的书籍 深度学习与 PyTorch 启动你的项目。它提供了自学教程有效代码

让我们开始吧!

如何评估 PyTorch 模型的性能

图片由 Kin Shing Lai 提供。保留部分权利。

概述

本章分为四部分,它们是:

  • 模型的经验评估

  • 数据拆分

  • 使用验证集训练 PyTorch 模型

  • k 折交叉验证

模型的经验评估

在从头设计和配置深度学习模型时,需要做出很多决策。这包括设计决策,如使用多少层,每层的大小,使用什么层或激活函数。这还可能包括损失函数的选择、优化算法、训练的轮次以及模型输出的解释。幸运的是,有时你可以复制其他人的网络结构。有时,你可以通过一些启发式方法来做出选择。要判断你是否做出了正确的选择,最好的方法是通过实际数据的经验评估来比较多个备选方案。

深度学习常用于处理具有非常大数据集的问题,即数万或数十万的数据样本。这为测试提供了充足的数据。但你需要一个稳健的测试策略来估计模型在未见数据上的表现。基于此,你可以有一个指标来比较不同模型配置之间的优劣。

想要开始使用 PyTorch 进行深度学习?

现在就参加我的免费电子邮件速成课程(含示例代码)。

点击注册并获取课程的免费 PDF 电子书版本。

数据拆分

如果你有数以万计甚至更多的样本数据集,不必总是将所有数据都提供给模型进行训练。这将不必要地增加复杂性并延长训练时间。更多并不总是更好。你可能得不到最佳结果。

当你有大量数据时,应该将其中一部分作为训练集用于模型训练。另一部分作为测试集,在训练之外保留,但会用已训练或部分训练的模型进行验证。这一步通常称为“训练-测试分离”。

让我们考虑Pima Indians Diabetes 数据集。您可以使用 NumPy 加载数据:

import numpy as np
data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")

有 768 个数据样本。虽然不多,但足以演示分割。让我们将前 66% 视为训练集,剩余部分作为测试集。最简单的方法是通过对数组进行切片:

# find the boundary at 66% of total samples
count = len(data)
n_train = int(count * 0.66)
# split the data at the boundary
train_data = data[:n_train]
test_data = data[n_train:]

66% 的选择是任意的,但你不希望训练集太小。有时你可能会使用 70%-30% 的分割。但如果数据集很大,你甚至可以使用 30%-70% 的分割,如果训练数据的 30% 足够大的话。

如果按此方式拆分数据,表明数据集已被洗牌,以使训练集和测试集同样多样化。如果发现原始数据集已排序,并且仅在最后取测试集,可能会导致所有测试数据属于同一类或在某个输入特征中具有相同值。这并不理想。

当然,在拆分之前可以调用np.random.shuffle(data)来避免这种情况。但是许多机器学习工程师通常使用 scikit-learn 来实现这一点。请参阅以下示例:

import numpy as np
from sklearn.model_selection import train_test_split

data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
train_data, test_data = train_test_split(data, test_size=0.33)

但更常见的是,在分开输入特征和输出标签之后进行。请注意,这个来自 scikit-learn 的函数不仅可以在 NumPy 数组上工作,还可以在 PyTorch 张量上工作:

import numpy as np
import torch
from sklearn.model_selection import train_test_split

data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = data[:, 0:8]
y = data[:, 8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)

使用验证训练 PyTorch 模型

让我们重新审视在此数据集上构建和训练深度学习模型的代码:

import torch
import torch.nn as nn
import torch.optim as optim
import tqdm

...

model = nn.Sequential(
    nn.Linear(8, 12),
    nn.ReLU(),
    nn.Linear(12, 8),
    nn.ReLU(),
    nn.Linear(8, 1),
    nn.Sigmoid()
)

# loss function and optimizer
loss_fn = nn.BCELoss()  # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)

n_epochs = 50    # number of epochs to run
batch_size = 10  # size of each batch
batches_per_epoch = len(Xtrain) // batch_size

for epoch in range(n_epochs):
    with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar:
        bar.set_description(f"Epoch {epoch}")
        for i in bar:
            # take a batch
            start = i * batch_size
            X_batch = X_train[start:start+batch_size]
            y_batch = y_train[start:start+batch_size]
            # forward pass
            y_pred = model(X_batch)
            loss = loss_fn(y_pred, y_batch)
            # backward pass
            optimizer.zero_grad()
            loss.backward()
            # update weights
            optimizer.step()
            # print progress
            bar.set_postfix(
                loss=float(loss)
            )

在这段代码中,每次迭代从训练集中提取一个批次,并在前向传播中发送到模型。然后在反向传播中计算梯度并更新权重。

虽然在这种情况下,您在训练循环中使用二元交叉熵作为损失指标,但您可能更关心预测准确性。计算准确性很容易。您将输出(在 0 到 1 的范围内)四舍五入到最接近的整数,以便获得二进制值 0 或 1。然后计算您的预测与标签匹配的百分比,这给出了准确性。

但你的预测是什么?它是上面的y_pred,这是您当前模型在X_batch上的预测。将准确性添加到训练循环变成了这样:

for epoch in range(n_epochs):
    with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar:
        bar.set_description(f"Epoch {epoch}")
        for i in bar:
            # take a batch
            start = i * batch_size
            X_batch = X_train[start:start+batch_size]
            y_batch = y_train[start:start+batch_size]
            # forward pass
            y_pred = model(X_batch)
            loss = loss_fn(y_pred, y_batch)
            # backward pass
            optimizer.zero_grad()
            loss.backward()
            # update weights
            optimizer.step()
            # print progress, with accuracy
            acc = (y_pred.round() == y_batch).float().mean()
            bar.set_postfix(
                loss=float(loss)
                acc=float(acc)
            )

然而,X_batchy_batch被优化器使用,优化器将微调你的模型,使其能够从X_batch预测y_batch。现在你使用准确率检查y_pred是否与y_batch匹配。这就像作弊一样,因为如果你的模型以某种方式记住了解决方案,它可以直接向你报告y_pred,而无需真正从X_batch中推断y_pred,并获得完美的准确率。

实际上,一个深度学习模型可能复杂到你无法确定你的模型只是记住了答案还是推断了答案。因此,最好的方法是不要X_batchX_train中的任何内容计算准确率,而是从其他地方:你的测试集。让我们在每个时期结束后使用X_test添加准确率测量:

for epoch in range(n_epochs):
    with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar:
        bar.set_description(f"Epoch {epoch}")
        for i in bar:
            # take a batch
            start = i * batch_size
            X_batch = X_train[start:start+batch_size]
            y_batch = y_train[start:start+batch_size]
            # forward pass
            y_pred = model(X_batch)
            loss = loss_fn(y_pred, y_batch)
            # backward pass
            optimizer.zero_grad()
            loss.backward()
            # update weights
            optimizer.step()
            # print progress
            acc = (y_pred.round() == y_batch).float().mean()
            bar.set_postfix(
                loss=float(loss),
                acc=float(acc)
            )
    # evaluate model at end of epoch
    y_pred = model(X_test)
    acc = (y_pred.round() == y_test).float().mean()
    acc = float(acc)
    print(f"End of {epoch}, accuracy {acc}")

在这种情况下,内部 for 循环中的acc只是一个显示进展的度量。与显示损失度量没有太大区别,只是它不参与梯度下降算法。你期望准确率随着损失度量的改善而提高。

在外部 for 循环中,每个时期结束时,你从X_test计算准确率。工作流程类似:你将测试集提供给模型并请求其预测,然后统计与测试集标签匹配的结果数量。但这正是你需要关注的准确率。它应该随着训练的进展而提高,但如果你没有看到它的提升(即准确率增加)甚至有所下降,你必须中断训练,因为它似乎开始过拟合。过拟合是指模型开始记住训练集而不是从中学习推断预测。一个迹象是训练集的准确率不断提高,而测试集的准确率却下降。

以下是实现上述所有内容的完整代码,从数据拆分到使用测试集进行验证:

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import tqdm
from sklearn.model_selection import train_test_split

data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = data[:, 0:8]
y = data[:, 8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)

model = nn.Sequential(
    nn.Linear(8, 12),
    nn.ReLU(),
    nn.Linear(12, 8),
    nn.ReLU(),
    nn.Linear(8, 1),
    nn.Sigmoid()
)

# loss function and optimizer
loss_fn = nn.BCELoss()  # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.0001)

n_epochs = 50    # number of epochs to run
batch_size = 10  # size of each batch
batches_per_epoch = len(X_train) // batch_size

for epoch in range(n_epochs):
    with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0) as bar: #, disable=True) as bar:
        bar.set_description(f"Epoch {epoch}")
        for i in bar:
            # take a batch
            start = i * batch_size
            X_batch = X_train[start:start+batch_size]
            y_batch = y_train[start:start+batch_size]
            # forward pass
            y_pred = model(X_batch)
            loss = loss_fn(y_pred, y_batch)
            # backward pass
            optimizer.zero_grad()
            loss.backward()
            # update weights
            optimizer.step()
            # print progress
            acc = (y_pred.round() == y_batch).float().mean()
            bar.set_postfix(
                loss=float(loss),
                acc=float(acc)
            )
    # evaluate model at end of epoch
    y_pred = model(X_test)
    acc = (y_pred.round() == y_test).float().mean()
    acc = float(acc)
    print(f"End of {epoch}, accuracy {acc}")

上面的代码将打印如下内容:

End of 0, accuracy 0.5787401795387268
End of 1, accuracy 0.6102362275123596
End of 2, accuracy 0.6220472455024719
End of 3, accuracy 0.6220472455024719
End of 4, accuracy 0.6299212574958801
End of 5, accuracy 0.6377952694892883
End of 6, accuracy 0.6496062874794006
End of 7, accuracy 0.6535432934761047
End of 8, accuracy 0.665354311466217
End of 9, accuracy 0.6614173054695129
End of 10, accuracy 0.665354311466217
End of 11, accuracy 0.665354311466217
End of 12, accuracy 0.665354311466217
End of 13, accuracy 0.665354311466217
End of 14, accuracy 0.665354311466217
End of 15, accuracy 0.6732283234596252
End of 16, accuracy 0.6771653294563293
End of 17, accuracy 0.6811023354530334
End of 18, accuracy 0.6850393414497375
End of 19, accuracy 0.6889764070510864
End of 20, accuracy 0.6850393414497375
End of 21, accuracy 0.6889764070510864
End of 22, accuracy 0.6889764070510864
End of 23, accuracy 0.6889764070510864
End of 24, accuracy 0.6889764070510864
End of 25, accuracy 0.6850393414497375
End of 26, accuracy 0.6811023354530334
End of 27, accuracy 0.6771653294563293
End of 28, accuracy 0.6771653294563293
End of 29, accuracy 0.6692913174629211
End of 30, accuracy 0.6732283234596252
End of 31, accuracy 0.6692913174629211
End of 32, accuracy 0.6692913174629211
End of 33, accuracy 0.6732283234596252
End of 34, accuracy 0.6771653294563293
End of 35, accuracy 0.6811023354530334
End of 36, accuracy 0.6811023354530334
End of 37, accuracy 0.6811023354530334
End of 38, accuracy 0.6811023354530334
End of 39, accuracy 0.6811023354530334
End of 40, accuracy 0.6811023354530334
End of 41, accuracy 0.6771653294563293
End of 42, accuracy 0.6771653294563293
End of 43, accuracy 0.6771653294563293
End of 44, accuracy 0.6771653294563293
End of 45, accuracy 0.6771653294563293
End of 46, accuracy 0.6771653294563293
End of 47, accuracy 0.6732283234596252
End of 48, accuracy 0.6732283234596252
End of 49, accuracy 0.6732283234596252

k 折交叉验证

在上面的例子中,你从测试集计算了准确率。它被用作模型在训练过程中进展的评分。你希望在这个分数达到最大值时停止。实际上,仅仅通过比较这个测试集的分数,你就知道你的模型在第 21 个时期之后表现最佳,并且之后开始过拟合。对吗?

如果你构建了两个不同设计的模型,是否应该仅仅比较这些模型在同一测试集上的准确率,并声称一个比另一个更好?

实际上,你可以认为即使在提取测试集之前已经打乱了数据集,测试集也不够具有代表性。你也可以认为,偶然间,一个模型可能更适合这个特定的测试集,但不一定总是更好。为了更强有力地论证哪个模型更好,不依赖于测试集的选择,你可以尝试多个测试集并计算准确率的平均值。

这就是 k 折交叉验证的作用。它是决定哪种设计效果更好的过程。它通过多次从头开始训练过程来工作,每次使用不同的训练和测试集组合。因此,您将得到kk个模型和kk个相应测试集的准确性分数。您不仅对平均准确率感兴趣,还对标准偏差感兴趣。标准偏差告诉您准确性分数是否一致,或者某些测试集在模型中特别好或特别差。

由于 k 折交叉验证多次从头开始训练模型,最好将训练循环包装在函数中:

def model_train(X_train, y_train, X_test, y_test):
    # create new model
    model = nn.Sequential(
        nn.Linear(8, 12),
        nn.ReLU(),
        nn.Linear(12, 8),
        nn.ReLU(),
        nn.Linear(8, 1),
        nn.Sigmoid()
    )

    # loss function and optimizer
    loss_fn = nn.BCELoss()  # binary cross entropy
    optimizer = optim.Adam(model.parameters(), lr=0.0001)

    n_epochs = 25    # number of epochs to run
    batch_size = 10  # size of each batch
    batches_per_epoch = len(X_train) // batch_size

    for epoch in range(n_epochs):
        with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0, disable=True) as bar:
            bar.set_description(f"Epoch {epoch}")
            for i in bar:
                # take a batch
                start = i * batch_size
                X_batch = X_train[start:start+batch_size]
                y_batch = y_train[start:start+batch_size]
                # forward pass
                y_pred = model(X_batch)
                loss = loss_fn(y_pred, y_batch)
                # backward pass
                optimizer.zero_grad()
                loss.backward()
                # update weights
                optimizer.step()
                # print progress
                acc = (y_pred.round() == y_batch).float().mean()
                bar.set_postfix(
                    loss=float(loss),
                    acc=float(acc)
                )
    # evaluate accuracy at end of training
    y_pred = model(X_test)
    acc = (y_pred.round() == y_test).float().mean()
    return float(acc)

上面的代码故意不打印任何内容(使用tqdm中的disable=True)以保持屏幕整洁。

同样,在 scikit-learn 中,您有一个用于 k 折交叉验证的函数。您可以利用它来生成模型准确性的稳健估计:

from sklearn.model_selection import StratifiedKFold

# define 5-fold cross validation test harness
kfold = StratifiedKFold(n_splits=5, shuffle=True)
cv_scores = []
for train, test in kfold.split(X, y):
    # create model, train, and get accuracy
    acc = model_train(X[train], y[train], X[test], y[test])
    print("Accuracy: %.2f" % acc)
    cv_scores.append(acc)
# evaluate the model
print("%.2f%% (+/- %.2f%%)" % (np.mean(cv_scores)*100, np.std(cv_scores)*100))

运行此命令将输出:

Accuracy: 0.64
Accuracy: 0.67
Accuracy: 0.68
Accuracy: 0.63
Accuracy: 0.59
64.05% (+/- 3.30%)

在 scikit-learn 中,有多个 k 折交叉验证函数,这里使用的是分层 k 折。它假设y是类标签,并考虑它们的值,以便在拆分中提供平衡的类表示。

上面的代码使用了k=5k=5或 5 个拆分。这意味着将数据集分为五个相等的部分,选择其中一个作为测试集,将其余部分组合为训练集。有五种方法可以做到这一点,因此上述的 for 循环将进行五次迭代。在每次迭代中,您调用model_train()函数并得到准确率分数。然后将其保存到列表中,这将用于计算最终的均值和标准偏差。

kfold对象将返回给您索引。因此,您无需提前运行训练-测试分割,而是在调用model_train()函数时使用提供的索引动态提取训练集和测试集。

上面的结果显示,该模型的表现适中,平均准确率为 64%。由于标准偏差为 3%,这意味着大部分时间,您预期模型的准确率在 61%到 67%之间。您可以尝试更改上述模型,例如添加或删除一层,并观察均值和标准偏差的变化。您也可以尝试增加训练中使用的时期数并观察结果。

k 折交叉验证的均值和标准偏差是您应该用来评估模型设计的基准。

将所有内容综合起来,以下是完整的 k 折交叉验证代码:

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import tqdm
from sklearn.model_selection import StratifiedKFold

data = np.loadtxt("pima-indians-diabetes.csv", delimiter=",")
X = data[:, 0:8]
y = data[:, 8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

def model_train(X_train, y_train, X_test, y_test):
    # create new model
    model = nn.Sequential(
        nn.Linear(8, 12),
        nn.ReLU(),
        nn.Linear(12, 8),
        nn.ReLU(),
        nn.Linear(8, 1),
        nn.Sigmoid()
    )

    # loss function and optimizer
    loss_fn = nn.BCELoss()  # binary cross entropy
    optimizer = optim.Adam(model.parameters(), lr=0.0001)

    n_epochs = 25    # number of epochs to run
    batch_size = 10  # size of each batch
    batches_per_epoch = len(X_train) // batch_size

    for epoch in range(n_epochs):
        with tqdm.trange(batches_per_epoch, unit="batch", mininterval=0, disable=True) as bar:
            bar.set_description(f"Epoch {epoch}")
            for i in bar:
                # take a batch
                start = i * batch_size
                X_batch = X_train[start:start+batch_size]
                y_batch = y_train[start:start+batch_size]
                # forward pass
                y_pred = model(X_batch)
                loss = loss_fn(y_pred, y_batch)
                # backward pass
                optimizer.zero_grad()
                loss.backward()
                # update weights
                optimizer.step()
                # print progress
                acc = (y_pred.round() == y_batch).float().mean()
                bar.set_postfix(
                    loss=float(loss),
                    acc=float(acc)
                )
    # evaluate accuracy at end of training
    y_pred = model(X_test)
    acc = (y_pred.round() == y_test).float().mean()
    return float(acc)

# define 5-fold cross validation test harness
kfold = StratifiedKFold(n_splits=5, shuffle=True)
cv_scores = []
for train, test in kfold.split(X, y):
    # create model, train, and get accuracy
    acc = model_train(X[train], y[train], X[test], y[test])
    print("Accuracy: %.2f" % acc)
    cv_scores.append(acc)
# evaluate the model
print("%.2f%% (+/- %.2f%%)" % (np.mean(cv_scores)*100, np.std(cv_scores)*100))

摘要

在本文中,您了解到在深度学习模型在未见数据上估计性能时,有一个稳健的方法的重要性,并学习了如何实现。您看到:

  • 如何使用 scikit-learn 将数据分割成训练集和测试集

  • 如何在 scikit-learn 的帮助下进行 k 折交叉验证

  • 如何修改 PyTorch 模型中的训练循环,以包括测试集验证和交叉验证

如何为 PyTorch 模型进行超参数网格搜索

原文:machinelearningmastery.com/how-to-grid-search-hyperparameters-for-pytorch-models/

神经网络的“权重”在 PyTorch 代码中被称为“参数”,并且在训练过程中由优化器进行微调。相反,超参数是神经网络的参数,设计固定并且不通过训练进行调整。例如隐藏层数量和激活函数的选择。超参数优化是深度学习的重要部分。原因在于神经网络配置非常困难,并且需要设置很多参数。此外,单个模型的训练可能非常缓慢。

在这篇文章中,您将发现如何使用 scikit-learn Python 机器学习库的网格搜索功能来调整 PyTorch 深度学习模型的超参数。阅读完本文后,您将了解到:

  • 如何将 PyTorch 模型包装以在 scikit-learn 中使用,以及如何使用网格搜索

  • 如何网格搜索常见的神经网络参数,如学习率、退出率、时期和神经元数量

  • 如何在自己的项目中定义自己的超参数调整实验

**用我的书 Deep Learning with PyTorch 开始你的项目 。它提供 自学教程 可工作的代码 **。

让我们开始吧!

如何为 PyTorch 模型进行超参数网格搜索

照片由 brandon siu 提供。部分权利保留。

概述

在本文中,您将看到如何使用 scikit-learn 的网格搜索功能,提供一系列示例,您可以将其复制粘贴到自己的项目中作为起点。以下是我们将要涵盖的主题列表:

  • 如何在 scikit-learn 中使用 PyTorch 模型

  • 如何在 scikit-learn 中使用网格搜索

  • 如何调整批量大小和训练时期

  • 如何调整优化算法

  • 如何调整学习率和动量

  • 如何调整网络权重初始化

  • 如何调整激活函数

  • 如何调整退出正则化

  • 如何调整隐藏层中神经元的数量

如何在 scikit-learn 中使用 PyTorch 模型

如果使用 skorch 包装,PyTorch 模型可以在 scikit-learn 中使用。这是为了利用 Python 的鸭子类型特性,使 PyTorch 模型提供类似于 scikit-learn 模型的 API,以便可以与 scikit-learn 中的所有内容一起使用。在 skorch 中,有 NeuralNetClassifier 用于分类神经网络和 NeuralNetRegressor 用于回归神经网络。您可能需要运行以下命令来安装模块。

pip install skorch

要使用这些包装器,你必须将你的 PyTorch 模型定义为使用 nn.Module 的一个类,然后在构造 NeuralNetClassifier 类时将类的名称传递给 module 参数。例如:

class MyClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        ...

    def forward(self, x):
        ...
        return x

# create the skorch wrapper
model = NeuralNetClassifier(
    module=MyClassifier
)

NeuralNetClassifier 类的构造函数可以接受默认参数,这些参数会传递给 model.fit()(这是在 scikit-learn 模型中调用训练循环的方式),例如训练轮数和批次大小。例如:

model = NeuralNetClassifier(
    module=MyClassifier,
    max_epochs=150,
    batch_size=10
)

NeuralNetClassifier 类的构造函数还可以接受新的参数,这些参数可以传递给你的模型类的构造函数,但你必须在参数前加上 module__(两个下划线)。这些新参数可能在构造函数中有默认值,但当包装器实例化模型时,它们会被覆盖。例如:

import torch.nn as nn
from skorch import NeuralNetClassifier

class SonarClassifier(nn.Module):
    def __init__(self, n_layers=3):
        super().__init__()
        self.layers = []
        self.acts = []
        for i in range(n_layers):
            self.layers.append(nn.Linear(60, 60))
            self.acts.append(nn.ReLU())
            self.add_module(f"layer{i}", self.layers[-1])
            self.add_module(f"act{i}", self.acts[-1])
        self.output = nn.Linear(60, 1)

    def forward(self, x):
        for layer, act in zip(self.layers, self.acts):
            x = act(layer(x))
        x = self.output(x)
        return x

model = NeuralNetClassifier(
    module=SonarClassifier,
    max_epochs=150,
    batch_size=10,
    module__n_layers=2
)

你可以通过初始化模型并打印它来验证结果:

print(model.initialize())

在这个示例中,你应该能看到:

<class 'skorch.classifier.NeuralNetClassifier'>initialized: Linear(in_features=60, out_features=60, bias=True)
    (act0): ReLU()
    (layer1): Linear(in_features=60, out_features=60, bias=True)
    (act1): ReLU()
    (output): Linear(in_features=60, out_features=1, bias=True)
  ),
)

想要开始使用 PyTorch 进行深度学习吗?

现在就参加我的免费电子邮件速成课程(附带示例代码)。

点击注册并免费获得课程的 PDF Ebook 版本。

如何在 scikit-learn 中使用网格搜索

网格搜索是一种模型超参数优化技术。它通过穷举所有超参数的组合,找到给出最佳分数的组合。在 scikit-learn 中,这种技术由 GridSearchCV 类提供。在构造这个类时,你必须在 param_grid 参数中提供一个超参数字典。这是模型参数名称与要尝试的值数组的映射。

默认情况下,准确率是优化的评分指标,但你可以在 GridSearchCV 构造函数的 score 参数中指定其他评分指标。GridSearchCV 过程将为每个参数组合构建和评估一个模型。交叉验证用于评估每个单独的模型,默认使用的是 3 折交叉验证,虽然你可以通过将 cv 参数指定给 GridSearchCV 构造函数来覆盖这一点。

下面是一个定义简单网格搜索的示例:

param_grid = {
    'epochs': [10,20,30]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, Y)

通过将 GridSearchCV 构造函数中的 n_jobs 参数设置为 1-1,该过程将使用你机器上的所有核心。否则,网格搜索过程将仅在单线程中运行,这在多核 CPU 上较慢。

完成后,你可以在 grid.fit() 返回的结果对象中访问网格搜索的结果。best_score_ 成员提供了在优化过程中观察到的最佳分数,而 best_params_ 描述了获得最佳结果的参数组合。你可以在 scikit-learn API 文档中了解更多关于 GridSearchCV 类的信息。

快速启动你的项目,可以参考我的书籍 Deep Learning with PyTorch。它提供了自学教程可运行的代码

问题描述

现在你已经知道如何将 PyTorch 模型与 scikit-learn 配合使用以及如何在 scikit-learn 中使用网格搜索,让我们看一些示例。

所有示例将在一个小型标准机器学习数据集上演示,名为Pima Indians 糖尿病发作分类数据集。这是一个包含所有数值属性的小数据集,易于处理。

在本帖中的示例中,你将汇总最佳参数。这不是网格搜索的最佳方式,因为参数可能相互作用,但它适用于演示目的。

如何调整批量大小和轮数

在第一个简单的示例中,你将查看调整批量大小和训练网络时使用的轮数。

在迭代梯度下降中,批量大小是指在权重更新之前展示给网络的样本数。它也是网络训练中的一种优化,定义了每次读取多少样本并保持在内存中。

轮数是指整个训练数据集在训练过程中被展示给网络的次数。一些网络对批量大小比较敏感,比如 LSTM 递归神经网络和卷积神经网络。

在这里,你将评估从 10 到 100 的不同小批量大小,每次递增 20。

完整的代码列表如下:

import random
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

# PyTorch classifier
class PimaClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = nn.Linear(8, 12)
        self.act = nn.ReLU()
        self.output = nn.Linear(12, 1)
        self.prob = nn.Sigmoid()

    def forward(self, x):
        x = self.act(self.layer(x))
        x = self.prob(self.output(x))
        return x

# create model with skorch
model = NeuralNetClassifier(
    PimaClassifier,
    criterion=nn.BCELoss,
    optimizer=optim.Adam,
    verbose=False
)

# define the grid search parameters
param_grid = {
    'batch_size': [10, 20, 40, 60, 80, 100],
    'max_epochs': [10, 50, 100]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)

# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

运行此示例将产生以下输出:

Best: 0.714844 using {'batch_size': 10, 'max_epochs': 100}
0.665365 (0.020505) with: {'batch_size': 10, 'max_epochs': 10}
0.588542 (0.168055) with: {'batch_size': 10, 'max_epochs': 50}
0.714844 (0.032369) with: {'batch_size': 10, 'max_epochs': 100}
0.671875 (0.022326) with: {'batch_size': 20, 'max_epochs': 10}
0.696615 (0.008027) with: {'batch_size': 20, 'max_epochs': 50}
0.714844 (0.019918) with: {'batch_size': 20, 'max_epochs': 100}
0.666667 (0.009744) with: {'batch_size': 40, 'max_epochs': 10}
0.687500 (0.033603) with: {'batch_size': 40, 'max_epochs': 50}
0.707031 (0.024910) with: {'batch_size': 40, 'max_epochs': 100}
0.667969 (0.014616) with: {'batch_size': 60, 'max_epochs': 10}
0.694010 (0.036966) with: {'batch_size': 60, 'max_epochs': 50}
0.694010 (0.042473) with: {'batch_size': 60, 'max_epochs': 100}
0.670573 (0.023939) with: {'batch_size': 80, 'max_epochs': 10}
0.674479 (0.020752) with: {'batch_size': 80, 'max_epochs': 50}
0.703125 (0.026107) with: {'batch_size': 80, 'max_epochs': 100}
0.680990 (0.014382) with: {'batch_size': 100, 'max_epochs': 10}
0.670573 (0.013279) with: {'batch_size': 100, 'max_epochs': 50}
0.687500 (0.017758) with: {'batch_size': 100, 'max_epochs': 100}

你可以看到,批量大小为 10 和 100 轮数取得了约 71%准确率的最佳结果(但你还应该考虑准确率的标准差)。

如何调整训练优化算法

所有深度学习库应提供多种优化算法。PyTorch 也不例外。

在此示例中,你将调整用于训练网络的优化算法,每种算法都使用默认参数。

这是一个奇特的示例,因为通常你会先选择一种方法,然后专注于调整其在问题上的参数(见下一个示例)。

在这里,你将评估 PyTorch 中可用的优化算法套件

完整的代码列表如下:

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

# PyTorch classifier
class PimaClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = nn.Linear(8, 12)
        self.act = nn.ReLU()
        self.output = nn.Linear(12, 1)
        self.prob = nn.Sigmoid()

    def forward(self, x):
        x = self.act(self.layer(x))
        x = self.prob(self.output(x))
        return x

# create model with skorch
model = NeuralNetClassifier(
    PimaClassifier,
    criterion=nn.BCELoss,
    max_epochs=100,
    batch_size=10,
    verbose=False
)

# define the grid search parameters
param_grid = {
    'optimizer': [optim.SGD, optim.RMSprop, optim.Adagrad, optim.Adadelta,
                  optim.Adam, optim.Adamax, optim.NAdam],
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)

# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

运行此示例将产生以下输出:

Best: 0.721354 using {'optimizer': <class 'torch.optim.adamax.Adamax'>}
0.674479 (0.036828) with: {'optimizer': <class 'torch.optim.sgd.SGD'>}
0.700521 (0.043303) with: {'optimizer': <class 'torch.optim.rmsprop.RMSprop'>}
0.682292 (0.027126) with: {'optimizer': <class 'torch.optim.adagrad.Adagrad'>}
0.572917 (0.051560) with: {'optimizer': <class 'torch.optim.adadelta.Adadelta'>}
0.714844 (0.030758) with: {'optimizer': <class 'torch.optim.adam.Adam'>}
0.721354 (0.019225) with: {'optimizer': <class 'torch.optim.adamax.Adamax'>}
0.709635 (0.024360) with: {'optimizer': <class 'torch.optim.nadam.NAdam'>}

结果表明,Adamax 优化算法表现最佳,准确率约为 72%。

值得一提的是,GridSearchCV会经常重新创建你的模型,因此每次试验都是独立的。之所以能够做到这一点,是因为NeuralNetClassifier封装器知道你 PyTorch 模型的类名,并在请求时为你实例化一个。

如何调整学习率和动量

通常,预先选择一种优化算法来训练网络并调整其参数是很常见的。

迄今为止,最常见的优化算法是传统的随机梯度下降(SGD),因为它被广泛理解。在这个示例中,你将优化 SGD 学习率和动量参数。

学习率控制每个批次结束时更新权重的幅度,动量控制前一次更新对当前权重更新的影响程度。

你将尝试一系列小的标准学习率和动量值,从 0.2 到 0.8,步长为 0.2,以及 0.9(因为它在实际中可能是一个流行的值)。在 PyTorch 中,设置学习率和动量的方法如下:

optimizer = optim.SGD(lr=0.001, momentum=0.9)

在 skorch 包装器中,你可以使用前缀optimizer__将参数路由到优化器。

通常,将优化中的纪元数也包含在内是一个好主意,因为每批次的学习量(学习率)、每纪元的更新次数(批量大小)和纪元数之间存在依赖关系。

完整的代码清单如下:

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

# PyTorch classifier
class PimaClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = nn.Linear(8, 12)
        self.act = nn.ReLU()
        self.output = nn.Linear(12, 1)
        self.prob = nn.Sigmoid()

    def forward(self, x):
        x = self.act(self.layer(x))
        x = self.prob(self.output(x))
        return x

# create model with skorch
model = NeuralNetClassifier(
    PimaClassifier,
    criterion=nn.BCELoss,
    optimizer=optim.SGD,
    max_epochs=100,
    batch_size=10,
    verbose=False
)

# define the grid search parameters
param_grid = {
    'optimizer__lr': [0.001, 0.01, 0.1, 0.2, 0.3],
    'optimizer__momentum': [0.0, 0.2, 0.4, 0.6, 0.8, 0.9],
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)

# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

运行此示例将产生以下输出。

Best: 0.682292 using {'optimizer__lr': 0.001, 'optimizer__momentum': 0.9}
0.648438 (0.016877) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.0}
0.671875 (0.017758) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.2}
0.674479 (0.022402) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.4}
0.677083 (0.011201) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.6}
0.679688 (0.027621) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.8}
0.682292 (0.026557) with: {'optimizer__lr': 0.001, 'optimizer__momentum': 0.9}
0.671875 (0.019918) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.0}
0.648438 (0.024910) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.2}
0.546875 (0.143454) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.4}
0.567708 (0.153668) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.6}
0.552083 (0.141790) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.8}
0.451823 (0.144561) with: {'optimizer__lr': 0.01, 'optimizer__momentum': 0.9}
0.348958 (0.001841) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.0}
0.450521 (0.142719) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.2}
0.450521 (0.142719) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.4}
0.450521 (0.142719) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.6}
0.348958 (0.001841) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.8}
0.348958 (0.001841) with: {'optimizer__lr': 0.1, 'optimizer__momentum': 0.9}
0.444010 (0.136265) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.0}
0.450521 (0.142719) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.2}
0.348958 (0.001841) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.4}
0.552083 (0.141790) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.6}
0.549479 (0.142719) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.8}
0.651042 (0.001841) with: {'optimizer__lr': 0.2, 'optimizer__momentum': 0.9}
0.552083 (0.141790) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.0}
0.348958 (0.001841) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.2}
0.450521 (0.142719) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.4}
0.552083 (0.141790) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.6}
0.450521 (0.142719) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.8}
0.450521 (0.142719) with: {'optimizer__lr': 0.3, 'optimizer__momentum': 0.9}

你可以看到,使用 SGD 时,最佳结果是学习率为 0.001 和动量为 0.9,准确率约为 68%。

如何调整网络权重初始化

神经网络权重初始化曾经很简单:使用小的随机值。

现在有一套不同的技术可供选择。你可以从torch.nn.init文档中获得一个[备选列表]。

在这个例子中,你将通过评估所有可用技术来调整网络权重初始化的选择。

你将在每一层上使用相同的权重初始化方法。理想情况下,根据每层使用的激活函数,使用不同的权重初始化方案可能更好。在下面的示例中,你将在隐藏层使用整流函数。由于预测是二元的,因此在输出层使用 sigmoid。权重初始化在 PyTorch 模型中是隐式的。因此,你需要在层创建后但在使用前编写自己的逻辑来初始化权重。让我们按如下方式修改 PyTorch:

# PyTorch classifier
class PimaClassifier(nn.Module):
    def __init__(self, weight_init=torch.nn.init.xavier_uniform_):
        super().__init__()
        self.layer = nn.Linear(8, 12)
        self.act = nn.ReLU()
        self.output = nn.Linear(12, 1)
        self.prob = nn.Sigmoid()
        # manually init weights
        weight_init(self.layer.weight)
        weight_init(self.output.weight)

    def forward(self, x):
        x = self.act(self.layer(x))
        x = self.prob(self.output(x))
        return x

PimaClassifier类添加了一个参数weight_init,它期望来自torch.nn.init的一个初始化器。在GridSearchCV中,你需要使用module__前缀来使NeuralNetClassifier将参数路由到模型类构造函数。

完整的代码清单如下:

import numpy as np
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

# PyTorch classifier
class PimaClassifier(nn.Module):
    def __init__(self, weight_init=init.xavier_uniform_):
        super().__init__()
        self.layer = nn.Linear(8, 12)
        self.act = nn.ReLU()
        self.output = nn.Linear(12, 1)
        self.prob = nn.Sigmoid()
        # manually init weights
        weight_init(self.layer.weight)
        weight_init(self.output.weight)

    def forward(self, x):
        x = self.act(self.layer(x))
        x = self.prob(self.output(x))
        return x

# create model with skorch
model = NeuralNetClassifier(
    PimaClassifier,
    criterion=nn.BCELoss,
    optimizer=optim.Adamax,
    max_epochs=100,
    batch_size=10,
    verbose=False
)

# define the grid search parameters
param_grid = {
    'module__weight_init': [init.uniform_, init.normal_, init.zeros_,
                           init.xavier_normal_, init.xavier_uniform_,
                           init.kaiming_normal_, init.kaiming_uniform_]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)

# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

运行此示例将产生以下输出。

Best: 0.697917 using {'module__weight_init': <function kaiming_uniform_ at 0x112020c10>}
0.348958 (0.001841) with: {'module__weight_init': <function uniform_ at 0x1120204c0>}
0.602865 (0.061708) with: {'module__weight_init': <function normal_ at 0x112020550>}
0.652344 (0.003189) with: {'module__weight_init': <function zeros_ at 0x112020820>}
0.691406 (0.030758) with: {'module__weight_init': <function xavier_normal_ at 0x112020af0>}
0.592448 (0.171589) with: {'module__weight_init': <function xavier_uniform_ at 0x112020a60>}
0.563802 (0.152971) with: {'module__weight_init': <function kaiming_normal_ at 0x112020ca0>}
0.697917 (0.013279) with: {'module__weight_init': <function kaiming_uniform_ at 0x112020c10>}

最佳结果是通过 He-uniform 权重初始化方案实现的,性能达到约 70%。

如何调整神经元激活函数

激活函数控制单个神经元的非线性及其触发时机。

一般来说,整流线性单元(ReLU)激活函数是最受欢迎的。然而,过去使用过的是 sigmoid 和 tanh 函数,这些函数可能在不同的问题上仍然更为适用。

在这个示例中,你将评估 PyTorch 中一些可用的激活函数。你只会在隐藏层中使用这些函数,因为在输出层中需要一个 sigmoid 激活函数用于二分类问题。类似于之前的示例,这个是模型类构造函数的一个参数,你将使用 module__ 前缀来设置 GridSearchCV 参数网格。

一般来说,将数据准备到不同传递函数的范围是一个好主意,但在这个案例中你不会这样做。

完整的代码清单如下所示:

import numpy as np
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

# PyTorch classifier
class PimaClassifier(nn.Module):
    def __init__(self, activation=nn.ReLU):
        super().__init__()
        self.layer = nn.Linear(8, 12)
        self.act = activation()
        self.output = nn.Linear(12, 1)
        self.prob = nn.Sigmoid()
        # manually init weights
        init.kaiming_uniform_(self.layer.weight)
        init.kaiming_uniform_(self.output.weight)

    def forward(self, x):
        x = self.act(self.layer(x))
        x = self.prob(self.output(x))
        return x

# create model with skorch
model = NeuralNetClassifier(
    PimaClassifier,
    criterion=nn.BCELoss,
    optimizer=optim.Adamax,
    max_epochs=100,
    batch_size=10,
    verbose=False
)

# define the grid search parameters
param_grid = {
    'module__activation': [nn.Identity, nn.ReLU, nn.ELU, nn.ReLU6,
                           nn.GELU, nn.Softplus, nn.Softsign, nn.Tanh,
                           nn.Sigmoid, nn.Hardsigmoid]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)

# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

运行此示例将产生以下输出。

Best: 0.699219 using {'module__activation': <class 'torch.nn.modules.activation.ReLU'>}
0.687500 (0.025315) with: {'module__activation': <class 'torch.nn.modules.linear.Identity'>}
0.699219 (0.011049) with: {'module__activation': <class 'torch.nn.modules.activation.ReLU'>}
0.674479 (0.035849) with: {'module__activation': <class 'torch.nn.modules.activation.ELU'>}
0.621094 (0.063549) with: {'module__activation': <class 'torch.nn.modules.activation.ReLU6'>}
0.674479 (0.017566) with: {'module__activation': <class 'torch.nn.modules.activation.GELU'>}
0.558594 (0.149189) with: {'module__activation': <class 'torch.nn.modules.activation.Softplus'>}
0.675781 (0.014616) with: {'module__activation': <class 'torch.nn.modules.activation.Softsign'>}
0.619792 (0.018688) with: {'module__activation': <class 'torch.nn.modules.activation.Tanh'>}
0.643229 (0.019225) with: {'module__activation': <class 'torch.nn.modules.activation.Sigmoid'>}
0.636719 (0.022326) with: {'module__activation': <class 'torch.nn.modules.activation.Hardsigmoid'>}

它显示 ReLU 激活函数在准确率约为 70% 时取得了最佳结果。

如何调整 dropout 正则化

在这个示例中,你将调整 dropout 率以进行正则化 以限制过拟合并提高模型的泛化能力。

为了获得最佳结果,dropout 最好与权重约束(例如最大范数约束)结合使用,后者在前向传播函数中实现。

这涉及到拟合 dropout 百分比和权重约束。我们将尝试 0.0 到 0.9 之间的 dropout 百分比(1.0 不合适)以及 0 到 5 之间的 MaxNorm 权重约束值。

完整的代码清单如下所示。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

# PyTorch classifier
class PimaClassifier(nn.Module):
    def __init__(self, dropout_rate=0.5, weight_constraint=1.0):
        super().__init__()
        self.layer = nn.Linear(8, 12)
        self.act = nn.ReLU()
        self.dropout = nn.Dropout(dropout_rate)
        self.output = nn.Linear(12, 1)
        self.prob = nn.Sigmoid()
        self.weight_constraint = weight_constraint
        # manually init weights
        init.kaiming_uniform_(self.layer.weight)
        init.kaiming_uniform_(self.output.weight)

    def forward(self, x):
        # maxnorm weight before actual forward pass
        with torch.no_grad():
            norm = self.layer.weight.norm(2, dim=0, keepdim=True).clamp(min=self.weight_constraint / 2)
            desired = torch.clamp(norm, max=self.weight_constraint)
            self.layer.weight *= (desired / norm)
        # actual forward pass
        x = self.act(self.layer(x))
        x = self.dropout(x)
        x = self.prob(self.output(x))
        return x

# create model with skorch
model = NeuralNetClassifier(
    PimaClassifier,
    criterion=nn.BCELoss,
    optimizer=optim.Adamax,
    max_epochs=100,
    batch_size=10,
    verbose=False
)

# define the grid search parameters
param_grid = {
    'module__weight_constraint': [1.0, 2.0, 3.0, 4.0, 5.0],
    'module__dropout_rate': [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)

# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

运行此示例将产生以下输出。

Best: 0.701823 using {'module__dropout_rate': 0.1, 'module__weight_constraint': 2.0}
0.669271 (0.015073) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 1.0}
0.692708 (0.035132) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 2.0}
0.589844 (0.170180) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 3.0}
0.561198 (0.151131) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 4.0}
0.688802 (0.021710) with: {'module__dropout_rate': 0.0, 'module__weight_constraint': 5.0}
0.697917 (0.009744) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 1.0}
0.701823 (0.016367) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 2.0}
0.694010 (0.010253) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 3.0}
0.686198 (0.025976) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 4.0}
0.679688 (0.026107) with: {'module__dropout_rate': 0.1, 'module__weight_constraint': 5.0}
0.701823 (0.029635) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 1.0}
0.682292 (0.014731) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 2.0}
0.701823 (0.009744) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 3.0}
0.701823 (0.026557) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 4.0}
0.687500 (0.015947) with: {'module__dropout_rate': 0.2, 'module__weight_constraint': 5.0}
0.686198 (0.006639) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 1.0}
0.656250 (0.006379) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 2.0}
0.565104 (0.155608) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 3.0}
0.700521 (0.028940) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 4.0}
0.669271 (0.012890) with: {'module__dropout_rate': 0.3, 'module__weight_constraint': 5.0}
0.661458 (0.018688) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 1.0}
0.669271 (0.017566) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 2.0}
0.652344 (0.006379) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 3.0}
0.680990 (0.037783) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 4.0}
0.692708 (0.042112) with: {'module__dropout_rate': 0.4, 'module__weight_constraint': 5.0}
0.666667 (0.006639) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 1.0}
0.652344 (0.011500) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 2.0}
0.662760 (0.007366) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 3.0}
0.558594 (0.146610) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 4.0}
0.552083 (0.141826) with: {'module__dropout_rate': 0.5, 'module__weight_constraint': 5.0}
0.548177 (0.141826) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 1.0}
0.653646 (0.013279) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 2.0}
0.661458 (0.008027) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 3.0}
0.553385 (0.142719) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 4.0}
0.669271 (0.035132) with: {'module__dropout_rate': 0.6, 'module__weight_constraint': 5.0}
0.662760 (0.015733) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 1.0}
0.636719 (0.024910) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 2.0}
0.550781 (0.146818) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 3.0}
0.537760 (0.140094) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 4.0}
0.542969 (0.138144) with: {'module__dropout_rate': 0.7, 'module__weight_constraint': 5.0}
0.565104 (0.148654) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 1.0}
0.657552 (0.008027) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 2.0}
0.428385 (0.111418) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 3.0}
0.549479 (0.142719) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 4.0}
0.648438 (0.005524) with: {'module__dropout_rate': 0.8, 'module__weight_constraint': 5.0}
0.540365 (0.136861) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 1.0}
0.605469 (0.053083) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 2.0}
0.553385 (0.139948) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 3.0}
0.549479 (0.142719) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 4.0}
0.595052 (0.075566) with: {'module__dropout_rate': 0.9, 'module__weight_constraint': 5.0}

你可以看到 10% 的 dropout 率和 2.0 的权重约束得到了最佳的准确率,约为 70%。

如何调整隐藏层中的神经元数量

层中神经元的数量是一个重要的调整参数。通常,层中神经元的数量控制网络的表示能力,至少在拓扑结构的那个点上是如此。

一般来说,足够大的单层网络可以近似任何其他神经网络,这归因于 通用逼近定理

在这个示例中,你将调整单个隐藏层中的神经元数量。你将尝试从 1 到 30 的值,步长为 5。

更大的网络需要更多的训练,并且批量大小和训练轮数至少应该与神经元的数量一起进行优化。

完整的代码清单如下所示。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
from skorch import NeuralNetClassifier
from sklearn.model_selection import GridSearchCV

# load the dataset, split into input (X) and output (y) variables
dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',')
X = dataset[:,0:8]
y = dataset[:,8]
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

class PimaClassifier(nn.Module):
    def __init__(self, n_neurons=12):
        super().__init__()
        self.layer = nn.Linear(8, n_neurons)
        self.act = nn.ReLU()
        self.dropout = nn.Dropout(0.1)
        self.output = nn.Linear(n_neurons, 1)
        self.prob = nn.Sigmoid()
        self.weight_constraint = 2.0
        # manually init weights
        init.kaiming_uniform_(self.layer.weight)
        init.kaiming_uniform_(self.output.weight)

    def forward(self, x):
        # maxnorm weight before actual forward pass
        with torch.no_grad():
            norm = self.layer.weight.norm(2, dim=0, keepdim=True).clamp(min=self.weight_constraint / 2)
            desired = torch.clamp(norm, max=self.weight_constraint)
            self.layer.weight *= (desired / norm)
        # actual forward pass
        x = self.act(self.layer(x))
        x = self.dropout(x)
        x = self.prob(self.output(x))
        return x

# create model with skorch
model = NeuralNetClassifier(
    PimaClassifier,
    criterion=nn.BCELoss,
    optimizer=optim.Adamax,
    max_epochs=100,
    batch_size=10,
    verbose=False
)

# define the grid search parameters
param_grid = {
    'module__n_neurons': [1, 5, 10, 15, 20, 25, 30]
}
grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3)
grid_result = grid.fit(X, y)

# summarize results
print("Best: %f using %s" % (grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
    print("%f (%f) with: %r" % (mean, stdev, param))

运行此示例将产生以下输出。

Best: 0.708333 using {'module__n_neurons': 30}
0.654948 (0.003683) with: {'module__n_neurons': 1}
0.666667 (0.023073) with: {'module__n_neurons': 5}
0.694010 (0.014382) with: {'module__n_neurons': 10}
0.682292 (0.014382) with: {'module__n_neurons': 15}
0.707031 (0.028705) with: {'module__n_neurons': 20}
0.703125 (0.030758) with: {'module__n_neurons': 25}
0.708333 (0.015733) with: {'module__n_neurons': 30}

你可以看到,最佳结果是在隐藏层有 30 个神经元的网络中获得的,准确率约为 71%。

超参数优化的提示

本节列出了一些在调整神经网络的超参数时需要考虑的实用提示。

  • kk 折交叉验证。你可以看到本文示例中的结果存在一些变化。默认使用了 3 折交叉验证,但也许使用 k=5k=5k=10k=10 会更稳定。仔细选择交叉验证配置以确保结果稳定。

  • 审查整个网格。不要只关注最佳结果,审查整个结果网格并寻找支持配置决策的趋势。当然,会有更多的组合,评估时间更长。

  • 并行化。如果可以的话,请使用所有核心,神经网络训练速度较慢,我们经常希望尝试许多不同的参数。考虑在云平台如 AWS 上运行它。

  • 使用数据集的样本。由于网络训练速度较慢,请尝试在训练数据集的较小样本上进行训练,只是为了了解参数的一般方向,而不是最优配置。

  • 从粗网格开始。从粗粒度的网格开始,并在能够缩小范围后逐渐缩放到更细粒度的网格。

  • 不要转移结果。结果通常是特定于问题的。尽量避免在每个新问题上使用喜爱的配置。你发现的一个问题上的最优结果不太可能转移到下一个项目上。相反,要寻找像层的数量或参数之间的关系这样更广泛的趋势。

  • 可复现性是一个问题。虽然我们在 NumPy 中设置了随机数生成器的种子,但结果并不是 100%可复现的。在网格搜索包装的 PyTorch 模型中,可复现性比本文介绍的更多。

进一步阅读

此部分提供了更多关于这个主题的资源,如果你想深入了解的话。

摘要

在本文中,你了解到如何使用 PyTorch 和 scikit-learn 在 Python 中调整深度学习网络的超参数。

具体来说,您学到了:

  • 如何将 PyTorch 模型包装以在 scikit-learn 中使用以及如何使用网格搜索。

  • 如何为 PyTorch 模型网格搜索一套不同的标准神经网络参数。

  • 如何设计您自己的超参数优化实验。