本节目标
-
如何加载 CSV 数据集并准备将其用于 PyTorch
-
如何在 PyToch 中定义多层感知器模型
-
如何在验证数据集上训练和评估 PyToch 模型
概述
步骤:
-
加载数据
-
定义 PyToch 模型
-
定义损失函数和优化器
-
运行训练循环
-
评估模型
-
作出预测
加载数据
-
定义打算在本文中使用的函数和类。我们将使用 NumPy 库加载数据集,并使用 PyTorch 库加载深度学习模型。
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
-
加载数据集
在本节中,我们将使用Pima Indians 的糖尿病数据集。它描述了皮马印第安人的患者医疗记录数据,以及他们是否在五年内患上了糖尿病。
这是一个二元分类问题(糖尿病发作为 1 或未发作为 0)。描述每个患者的所有输入变量都经过转换和数值化。这使得直接与期望数字输入和输出值的神经网络一起使用变得容易,并且是我们在 PyTorch 中的第一个神经网络的理想选择。
数据集下载地址:https://raw.githubusercontent.com/jbrownlee/Datasets/master/pima-indians-diabetes.data.csv
下载数据集并将其放在本地工作目录中,与 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()。有八个输入变量和一个输出变量(最后一列)。您将学习一个模型来映射输入变量的行(X) 输出变量 (y), 通常概括为y=f(X). 这些变量可以总结如下:
输入变量(X):
-
怀孕次数
-
口服葡萄糖耐量试验中 2 小时的血浆葡萄糖浓度
-
舒张压(毫米汞柱)
-
三头肌皮褶厚度(mm)
-
2 小时血清胰岛素 (μIU/ml)
-
体重指数(体重公斤/(身高米)2)
-
糖尿病谱系函数
-
年龄(岁)
输出变量(Y):
-
类标签(0 或 1
一旦 CSV 文件加载到内存中,我们就可以将数据列拆分为输入和输出变量。
数据将存储在一个二维数组中,其中第一个维度是行,第二个维度是列,例如,(行,列)。您可以使用标准 NumPy 切片运算""符选择列的子集,将数组拆分为两个数组:。您可以通过 slice 选择从索引 0 到索引 7 的前八列0:8。然后您可以通过索引 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×1个矩阵超过n-向量。
为了转换,我们从 NumPy 数组中创建一个张量:
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
定义模型
在 PyTorch 中确实有两种定义模型的方法。目标是让它像一个接受输入并返回输出的函数。
模型可以定义为一系列层。您创建一个Sequential列出层的模型。要做到这一点,首先要确保第一层具有正确数量的输入特征。8在此示例中,您可以将八个输入变量的输入维度指定为一个向量。
层的其他参数或模型需要多少层并不是一个简单的问题。您可以使用启发式方法来帮助您设计模型,或者您可以参考其他人在处理类似问题时的设计。通常,最好的神经网络结构是通过反复试验的过程找到的。通常,您需要一个足够大的网络来捕获问题的结构,但又要足够小以使其快速。在这个例子中,让我们使用一个三层的全连接网络结构。
全连接层或密集层是使用LinearPyTorch 中的类定义的。它只是表示类似于矩阵乘法的操作。您可以将输入数指定为第一个参数,将输出数指定为第二个参数。输出的数量有时称为层中的神经元数量或节点数量。
在该层之后我们还需要一个激活函数。如果未提供,您只需将矩阵乘法的输出带到下一步,或者有时我们使用线性激活来称呼它,因此层的名称。
在此示例中,您将在前两层使用整流线性单元激活函数(称为 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()
)
训练准备
定义的模型已准备好进行训练,但您需要指定训练的目标是什么。在此示例中,数据具有输入特征X和输出标签y. 您希望神经网络模型产生的输出尽可能接近y尽可能。训练网络意味着找到最佳权重集以将输入映射到数据集中的输出。衡量预测距离的指标y是损失函数。在这个例子中,你应该使用二元交叉熵,因为它是一个二元分类问题。
一旦你决定了损失函数,你还需要一个优化器。优化器是您用来逐步调整模型权重以产生更好输出的算法。有许多优化器可供选择,在这个例子中,使用了 Adam。这是梯度下降的流行版本,可以自动调整自身并在广泛的问题中给出良好的结果。
loss_fn = nn.BCELoss() # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.001)
优化器通常有一些配置参数。最值得注意的是,学习率lr。但是所有优化器都需要知道要优化什么。因此我们传递model.parameters(),它是我们创建的模型的所有参数的生成器。
训练模型
您已经定义了模型、损失指标和优化器。通过在某些数据上执行模型,可以为训练做好准备。
训练神经网络模型通常需要多个时期和批次。它们是我们如何将数据传递给模型的习惯用法:
-
Epoch : 将整个训练数据集传递给模型一次
-
Batch : 一个或多个样本传递给模型,从中,梯度下降算法将执行一次迭代
简而言之,整个数据集被分成多个批次,然后您使用训练循环将这些批次一个接一个地传递到模型中。一旦你用完了所有的批次,你就完成了一个时代。然后您可以使用相同的数据集重新开始并开始第二个 epoch,这样您就可以继续完善模型。重复此过程,直到您对模型的输出感到满意为止。
批处理的大小受系统内存的限制。此外,所需的计算量与批处理的大小成线性比例。许多时期的批次总数是我们运行梯度下降以改进模型的次数。这是一种权衡,我们希望对梯度下降进行更多迭代,以便我们可以生成更好的模型,但与此同时,我们不希望训练花费太长时间才能完成。epoch 的数量和批次的大小可以通过反复试验来实验性地选择。
训练模型的目标是使其学习足够好的输入数据到输出分类的映射。它不会是完美的,错误是不可避免的。通常你会看到错误数量减少,就像你在后面的时代一样,但它最终会趋于平稳。这称为模型收敛。
构建训练循环的最简单方法是使用两个嵌套的 for 循环,一个用于 epochs,一个用于批处理:
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
做出预测
可以调整上面的示例并使用它来生成对训练数据集的预测,假装它是您以前从未见过的新数据集。进行预测就像调用模型一样简单,就好像它是一个函数一样。您在输出层上使用了 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)