PyTorch是一个功能强大的Python库,用于构建深度学习模型。它提供了定义和训练神经网络以及使用它进行推理所需的一切。你不需要编写太多代码就能完成所有这些工作。在这篇文章中,你将了解如何使用Python和PyTorch创建你的第一个深度学习神经网络模型。完成本文后,你将知道:
- 如何加载CSV数据集并准备用于PyTorch
- 如何在PyTorch中定义多层感知器模型
- 如何在验证数据集上训练和评估PyTorch模型
通过我的《用PyTorch进行深度学习》一书来启动你的项目。它提供了包含可用代码的自学教程。
让我们开始吧。

手把手教你用PyTorch开发第一个神经网络
照片由 drown_in_city 拍摄。保留部分权利。
概述
所需的代码并不多。我们会慢慢讲解,以便你将来知道如何创建自己的模型。你将在本文中学到的步骤如下:
- 加载数据
- 定义PyTorch模型
- 定义损失函数和优化器
- 运行训练循环
- 评估模型
- 进行预测
加载数据
第一步是定义你打算在本文中使用的函数和类。你将使用NumPy库加载数据集,并使用PyTorch库进行深度学习模型的构建。
所需的导入如下所示:
1 2 3 4 |
import numpy as np import torch import torch.nn as nn import torch.optim as optim |
现在你可以加载数据集了。
在本文中,你将使用皮马印第安人糖尿病发病数据集。自机器学习领域早期以来,这一直是一个标准的机器学习数据集。它描述了皮马印第安人的患者病历数据以及他们是否在五年内患上糖尿病。
这是一个二元分类问题(糖尿病发病为1,否则为0)。描述每个患者的所有输入变量都经过了转换并且是数值型的。这使得它很容易直接与期望数值输入和输出的神经网络一起使用,是我们用PyTorch构建第一个神经网络的理想选择。
你也可以在这里下载它:这里。
下载数据集并将其放在你的本地工作目录中,与你的Python文件在同一位置。将其保存为文件名 pima-indians-diabetes.csv
。查看文件内部;你应该能看到如下所示的数据行:
1 2 3 4 5 6 |
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()
函数将文件加载为数字矩阵。有八个输入变量和一个输出变量(最后一列)。你将学习一个模型,将输入变量的行($X$)映射到一个输出变量($y$),这通常概括为 $y = f(X)$。这些变量总结如下:
输入变量 ($X$)
- 怀孕次数
- 口服葡萄糖耐量试验中2小时血浆葡萄糖浓度
- 舒张压(毫米汞柱)
- 肱三头肌皮褶厚度(毫米)
- 2小时血清胰岛素(μIU/ml)
- 身体质量指数(体重kg/(身高m)2)
- 糖尿病家族史函数
- 年龄(岁)
输出变量 ($y$)
- 类别标签(0 或 1)
一旦CSV文件加载到内存中,你就可以将数据列拆分为输入和输出变量。
数据将存储在一个二维数组中,其中第一个维度是行,第二个维度是列,例如(行,列)。你可以使用标准的NumPy切片操作符“:
”选择列的子集,将数组拆分为两个数组。你可以通过切片 0:8
选择从索引0到索引7的前八列。然后,你可以通过索引8选择输出列(第9个变量)。
1 2 3 4 5 6 |
... # 加载数据集,拆分为输入(X)和输出(y)变量 dataset = np.loadtxt('pima-indians-diabetes.csv', delimiter=',') X = dataset[:,0:8] y = dataset[:,8] |
但这些数据应该首先转换为PyTorch张量。原因之一是PyTorch通常使用32位浮点数进行操作,而NumPy默认使用64位浮点数。在大多数操作中,混合使用是不允许的。转换为PyTorch张量可以避免可能导致问题的隐式转换。你还可以借此机会修正形状以适应PyTorch的期望,例如,更倾向于使用 $n\times 1$ 矩阵而不是 $n$ 维向量。
要进行转换,可以从NumPy数组创建张量:
1 2 |
X = torch.tensor(X, dtype=torch.float32) y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1) |
现在你已经准备好定义你的神经网络模型了。
想开始使用PyTorch进行深度学习吗?
立即参加我的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
定义模型
实际上,在PyTorch中有两种定义模型的方法。目标是使其像一个函数一样,接收输入并返回输出。
模型可以定义为一系列层。你可以创建一个 Sequential
模型,并列出其中的层。要正确做到这一点,首先需要确保第一层具有正确的输入特征数。在本例中,你可以为八个输入变量指定输入维度 8
,作为一个向量。
层的其他参数或模型需要多少层并不是一个简单的问题。你可以使用启发式方法来帮助你设计模型,或者你可以参考其他人处理类似问题时的设计。通常,最佳的神经网络结构是通过反复试验的过程找到的。总的来说,你需要一个足够大的网络来捕捉问题的结构,但又足够小以使其快速运行。在本例中,让我们使用一个具有三层的全连接网络结构。
全连接层或密集层在PyTorch中使用 Linear
类定义。它简单地表示一个类似于矩阵乘法的操作。你可以将输入数量指定为第一个参数,输出数量指定为第二个参数。输出数量有时被称为层中的神经元数量或节点数量。
你还需要在层之后有一个激活函数。如果不提供,你只需将矩阵乘法的输出传递到下一步,或者有时称之为使用线性激活,这也是该层名称的由来。
在本例中,你将在前两层使用修正线性单元激活函数,简称ReLU,在输出层使用sigmoid函数。
在输出层使用sigmoid可以确保输出在0和1之间,这很容易映射为类别1的概率,或者通过0.5的阈值将其强制分类为任一类别。过去,你可能会在所有层都使用sigmoid和tanh激活函数,但事实证明,sigmoid激活可能导致深度神经网络中的梯度消失问题,而ReLU激活在速度和准确性方面都表现更好。
你可以通过添加每一层来将它们组合在一起,使得:
- 模型期望输入包含8个变量的数据行(第一层的第一个参数设置为
8
) - 第一个隐藏层有12个神经元,后面跟着一个ReLU激活函数
- 第二个隐藏层有8个神经元,后面跟着另一个ReLU激活函数
- 输出层有一个神经元,后面跟着一个sigmoid激活函数
1 2 3 4 5 6 7 8 9 |
... model = nn.Sequential( nn.Linear(8, 12), nn.ReLU(), nn.Linear(12, 8), nn.ReLU(), nn.Linear(8, 1), nn.Sigmoid() |
你可以通过打印模型来检查它,如下所示:
1 |
print(model) |
你会看到:
1 2 3 4 5 6 7 8 |
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
来创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... 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) |
在这种情况下,打印出的模型将是:
1 2 3 4 5 6 7 8 |
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
的情况下,如何产生输出张量。
从上面的输出中你可以看到,模型记住了你是如何调用每一层的。
训练准备
定义好的模型已经可以进行训练,但你需要指定训练的目标是什么。在本例中,数据包含输入特征 $X$ 和输出标签 $y$。你希望神经网络模型产生的输出尽可能接近 $y$。训练网络意味着找到最佳的权重集,以便在你的数据集中将输入映射到输出。损失函数是衡量预测与 $y$ 之间距离的指标。在本例中,你应该使用二元交叉熵,因为它是一个二元分类问题。
一旦确定了损失函数,你还需要一个优化器。优化器是你用来逐步调整模型权重以产生更好输出的算法。有许多优化器可供选择,在本例中,我们使用Adam。这个流行的梯度下降版本可以自动调整自身,并在各种问题中给出良好的结果。
1 2 |
loss_fn = nn.BCELoss() # 二元交叉熵 optimizer = optim.Adam(model.parameters(), lr=0.001) |
优化器通常有一些配置参数。最值得注意的是学习率 lr
。但所有优化器都需要知道要优化什么。因此,你传入 model.parameters()
,这是一个来自你创建的模型的所有参数的生成器。
训练模型
你已经定义了模型、损失度量和优化器。现在可以通过在一些数据上执行模型来进行训练了。
训练神经网络模型通常涉及轮次(epoch)和批次(batch)。它们是关于如何将数据传递给模型的术语:
- 轮次 (Epoch):将整个训练数据集传递给模型一次
- 批次 (Batch):传递给模型的一个或多个样本,梯度下降算法将基于此执行一次迭代
简单来说,整个数据集被分成多个批次,你使用一个训练循环逐个将这些批次传递给模型。一旦你用完了所有的批次,你就完成了一个轮次。然后你可以用相同的数据集重新开始,开始第二个轮次,继续优化模型。这个过程重复进行,直到你对模型的输出感到满意为止。
批次的大小受系统内存的限制。此外,所需的计算量与批次大小成线性比例。在许多轮次中总的批次数就是你运行梯度下降来优化模型的次数。这是一个权衡:你希望有更多的梯度下降迭代次数以便产生更好的模型,但同时又不希望训练时间太长。轮次数和批次大小可以通过反复试验来选择。
训练模型的目标是确保它学习到足够好的从输入数据到输出分类的映射。这不会是完美的,错误是不可避免的。通常,你会看到在后面的轮次中错误量减少,但最终会趋于平稳。这被称为模型收敛。
构建训练循环最简单的方法是使用两个嵌套的for循环,一个用于轮次,一个用于批次:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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'完成第 {epoch} 轮, 最新损失 {loss}') |
当这段代码运行时,它将打印以下内容:
1 2 3 4 5 6 7 |
完成第 0 轮, 最新损失 0.6271069645881653 完成第 1 轮, 最新损失 0.6056771874427795 完成第 2 轮, 最新损失 0.5916517972946167 完成第 3 轮, 最新损失 0.5822567939758301 完成第 4 轮, 最新损失 0.5682642459869385 完成第 5 轮, 最新损失 0.5640913248062134 ... |
评估模型
你已经在整个数据集上训练了我们的神经网络,现在可以在相同的数据集上评估网络的性能。这只能让你了解你对数据集的建模效果如何(例如,训练准确率),但无法得知该算法在处理新数据时可能表现如何。这样做是为了简单起见,但理想情况下,你应该将数据分为训练集和测试集,用于模型的训练和评估。
你可以像在训练中调用模型一样,在你的训练数据集上评估你的模型。这将为每个输入生成预测,但你仍然需要计算一个评估分数。这个分数可以与你的损失函数相同,也可以是不同的东西。因为你正在进行二元分类,你可以使用准确率作为你的评估分数,方法是将输出(一个0到1范围内的浮点数)转换为整数(0或1),并与我们已知的标签进行比较。
具体操作如下:
1 2 3 4 5 6 |
# 计算准确率 (no_grad 是可选的) with torch.no_grad(): y_pred = model(X) accuracy = (y_pred.round() == y).float().mean() print(f"准确率 {accuracy}") |
round()
函数将浮点数四舍五入到最接近的整数。==
操作符进行比较并返回一个布尔张量,该张量可以转换为浮点数1.0和0.0。mean()
函数将为你提供1的数量(即预测与标签匹配)除以样本总数。no_grad()
上下文是可选的但建议使用,这样可以使 y_pred
不必记住它是如何得出这个数字的,因为你不会对它进行微分。
将所有内容放在一起,完整的代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import numpy as np import torch import torch.nn as nn import torch.optim as optim # 加载数据集,拆分为输入(X)和输出(y)变量 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) # 定义模型 model = nn.Sequential( nn.Linear(8, 12), nn.ReLU(), nn.Linear(12, 8), nn.ReLU(), nn.Linear(8, 1), nn.Sigmoid() ) print(model) # 训练模型 loss_fn = nn.BCELoss() # 二元交叉熵 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'完成第 {epoch} 轮, 最新损失 {loss}') # 计算准确率 (no_grad 是可选的) with torch.no_grad(): y_pred = model(X) accuracy = (y_pred.round() == y).float().mean() print(f"准确率 {accuracy}") |
你可以将所有代码复制到你的Python文件中,并将其保存在与你的数据文件 “pima-indians-diabetes.csv
” 相同的目录中,命名为 “pytorch_network.py
”。然后,你可以从命令行将该Python文件作为脚本运行。
运行这个例子,你应该会看到训练循环在每个轮次中随着损失值的变化而进行,最后打印出最终的准确率。理想情况下,你希望损失降至零,准确率达到1.0(即100%)。但这对于除了最简单的机器学习问题之外的任何问题都是不可能的。相反,你的模型中总会有一些错误。目标是选择一个模型配置和训练配置,以在给定数据集上实现尽可能低的损失和尽可能高的准确率。
神经网络是随机算法,这意味着相同的算法在相同的数据上,每次运行代码都可能训练出性能不同的模型。这是一个特性,而不是一个bug。模型性能的这种差异意味着,要对你的模型表现如何有一个合理的估计,你可能需要多次拟合它并计算准确率分数的平均值。例如,下面是重新运行该示例五次的准确率分数:
1 2 3 4 5 |
准确率: 0.7604166865348816 准确率: 0.7838541865348816 准确率: 0.7669270634651184 准确率: 0.7721354365348816 准确率: 0.7669270634651184 |
你可以看到所有的准确率分数都在77%左右。
进行预测
你可以调整上述示例,并用它在训练数据集上生成预测,假装这是一个你以前从未见过的新数据集。进行预测就像调用模型函数一样简单。你在输出层上使用了sigmoid激活函数,因此预测结果将是0到1之间的概率。你可以通过四舍五入将它们轻松地转换为这个分类任务的明确二元预测。例如:
1 2 3 4 5 6 |
... # 使用模型进行概率预测 predictions = model(X) # 对预测进行四舍五入 rounded = predictions.round() |
或者,你可以将概率直接转换为0或1来预测明确的类别;例如:
1 2 3 |
... # 使用模型进行类别预测 predictions = (model(X) > 0.5).int() |
下面的完整示例为数据集中的每个样本进行预测,然后打印出数据集中前五个样本的输入数据、预测类别和期望类别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import numpy as np import torch import torch.nn as nn import torch.optim as optim # 加载数据集,拆分为输入(X)和输出(y)变量 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): 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) # 训练模型 loss_fn = nn.BCELoss() # 二元交叉熵 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() # 计算准确率 y_pred = model(X) accuracy = (y_pred.round() == y).float().mean() print(f"准确率 {accuracy}") # 使用模型进行类别预测 predictions = (model(X) > 0.5).int() for i in range(5): print('%s => %d (期望 %d)' % (X[i].tolist(), predictions[i], y[i])) |
这段代码使用了另一种构建模型的方式,但功能上应与之前相同。模型训练后,对数据集中的所有样本进行预测,并打印前五个样本的输入行和预测类别值,并与期望类别值进行比较。你可以看到大多数行都被正确预测了。事实上,根据你在上一节中对模型性能的估计,你可以期望大约77%的行被正确预测。
1 2 3 4 5 |
[6.0, 148.0, 72.0, 35.0, 0.0, 33.599998474121094, 0.6269999742507935, 50.0] => 1 (期望 1) [1.0, 85.0, 66.0, 29.0, 0.0, 26.600000381469727, 0.35100001096725464, 31.0] => 0 (期望 0) [8.0, 183.0, 64.0, 0.0, 0.0, 23.299999237060547, 0.671999990940094, 32.0] => 1 (期望 1) [1.0, 89.0, 66.0, 23.0, 94.0, 28.100000381469727, 0.16699999570846558, 21.0] => 0 (期望 0) [0.0, 137.0, 40.0, 35.0, 168.0, 43.099998474121094, 2.2880001068115234, 33.0] => 1 (期望 1) |
进一步阅读
要了解更多关于深度学习和PyTorch的知识,请查看以下资源:
书籍
API
总结
在这篇文章中,你了解了如何使用PyTorch创建你的第一个神经网络模型。具体来说,你逐步学习了使用PyTorch创建神经网络或深度学习模型的关键步骤,包括:
- 如何加载数据
- 如何在PyTorch中定义神经网络
- 如何用数据训练模型
- 如何评估模型
- 如何用模型进行预测
精彩的文章!我喜欢所有那些不留任何困惑空间的小解释。谢谢你!!!
谢谢Sneha的支持和反馈!
很棒的文章!
一个基本问题:在分批训练时,损失是为整个批次计算的吗?梯度步长是相对于整个批次的损失而不是任何单个数据点计算的吗?如果是这样,我还需要聚合在这一步中计算出的各种损失吗?
谢谢!
是的,是整个批次。每个批次都对模型进行一次更新,但更新是基于批次中所有单个数据点的某个平均指标。因此,大批次能给你更好的平均值,但训练应该更多地受到运行的总批次数的影响。
好文章!你是如何计算第一个隐藏层的输入大小的?解释中说——“第一个隐藏层有12个神经元”。这个“12”是怎么计算出来的?
你好 Ani...这个数字是一个可以调整和优化的超参数。
井号标签内的拼写更正:“在这篇#文章#(原文为pose)中,你将了解如何使用Python和PyTorch创建你的第一个深度学习神经网络模型。完成本文后,你将知道:”
你好 Milan...我们感谢你的支持和反馈!
在“定义模型”下的第一个代码片段中缺少了“)”..非常好的文章,特别是对于初学者
谢谢你的反馈,Arun!
将训练数据集划分为批次时,为什么我们不随机划分并在所有轮次中都使用相同的划分呢?这不会导致泛化问题吗?还是PyTorch训练会隐式处理这个问题?
你好 Abel...好问题!在PyTorch(或类似框架)中训练模型时,我们通常使用**数据加载器**将数据划分为批次,这些加载器会在每个轮次开始时对数据进行洗牌。下面是它的工作原理以及为什么这很重要:
1. **洗牌以避免泛化问题**:在每个轮次洗牌数据集有助于确保模型不会从固定的批次划分中学习到某种模式,这确实可能导致过拟合或泛化问题。通过在每个轮次重新随机化批次,模型在每个批次中都会接触到不同范围的数据样本,从而实现更具泛化性的学习过程。
2. **PyTorch的DataLoader和洗牌**:PyTorch的
DataLoader
有一个shuffle=True
参数,设置后,它将在每个轮次开始时洗牌数据。这种随机化的批次处理是有益的,因为它防止模型每次都以相同的顺序看到数据,从而帮助它学习到更鲁棒的模式。3. **为什么我们不在不同轮次间保持固定的批次**:如果我们在所有轮次中都使用相同的固定批次划分,模型可能会学习到特定于每个批次的模式而无法泛化。这就像反复向模型展示相同的数据播放列表,而不是洗牌播放列表以确保每次都接触到变化。
### PyTorch中的示例代码
python
from torch.utils.data import DataLoader, TensorDataset
# 假设 `train_data` 和 `train_labels` 是你的数据集和标签
train_dataset = TensorDataset(train_data, train_labels)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) # shuffle=True 用于随机批次
# 在每个轮次中,DataLoader会再次洗牌数据集。
for epoch in range(num_epochs):
for batch in train_loader:
# 此处为训练循环
总而言之,每个轮次洗牌批次对于泛化至关重要,而PyTorch的
DataLoader
可以帮助自动处理这个问题。所以,只要你在你的DataLoader中使用了shuffle=True
,你就不用担心了!