2019 年 3 月

第 34 卷,第 3 期

此文章由机器翻译

[测试运行]

使用 PyTorch 实现神经回归

通过James McCaffrey

James McCaffrey回归问题的目标是预测单个数值。例如,你可能想要预测的基于其占地面积、 年龄、 邮政编码等一套住房价格。在本文中,我将介绍如何创建使用 PyTorch 代码库的神经回归模型。了解本文所述观点的最佳方法是看一下中的演示程序图 1。  

神经回归使用 PyTorch 演示运行
图 1 神经回归使用 PyTorch 演示运行

演示程序创建基于波士顿住房数据集,其目标是预测一个 506 城镇接近波士顿中的中间值房屋价格的预测模型。数据来自早期二十世纪七十年代。每个数据项具有 13 个预测变量,如镇的聊天室中心内的房子每平均数的犯罪索引,依此类推。目标是预测单个数值在于只有一个输出值。

该演示将 404 定型项和 102 测试项加载到内存中,,然后创建 13-(10-10) 1 神经网络。神经网络有两个隐藏的处理层,其中每个有 10 个节点。输入和输出节点数由数据,但隐藏层数量和每个中的节点数是自由参数,必须通过反复试验来确定。

演示训练神经网络,这意味着这些权重的值,并使用具有已知正确的输入和输出值的定型数据计算偏置,它们定义神经网络的行为。定型后,此演示程序计算上测试数据 (75.49%,77 共 102 正确) 的模型的准确性。测试准确性是将预期模型要前所未见的新数据上执行操作的程度的粗略度量。

最后,演示程序会由第一个测试城镇预测。13 原始输入值为 (0.09266,34.0,...8.67).当对神经回归模型进行训练时,已使用规范化的数据 (缩放,使所有值都都介于 0.0 和 1.0 之间),因此演示进行预测时必须使用规范化的数据,这是 (0.00097,0.34,...0.191501).该模型预测的中间值房屋价格是 $24,870.07,非常接近的美元 26,400 实际中间价。

本文假定您具有 C 系列语言的中级或更好的编程技能和机器学习基本的了解。本文将提供完整的演示代码。源代码和演示程序使用的两个数据文件也是在随附下载中提供的。为了尽可能地让主要思想清晰明确,已删除所有常见错误检查。

安装 PyTorch

安装 PyTorch 涉及两个主要步骤。首次安装 Python 和多个所需的辅助包,例如 NumPy 和 SciPy,然后作为一个加载项 Python 包安装 PyTorch。

虽然可以安装 Python 和单独运行 PyTorch 所需的包,但最好是安装 Python 分发。对于我的演示程序,我安装了 Anaconda3 5.2.0 分发 (其中包含 Python 3.6.5) 和 PyTorch 1.0.0。如果您是初次接触 Python,请注意安装和管理外接程序的包依赖项很重要。

通过 Anaconda 分发安装 Python 之后, 以 PyTorch 程序包都可以使用 pip 实用工具函数和.whl ("wheel") 文件安装。PyTorch 提供仅 CPU 版本和 GPU 版本中。我使用仅 CPU 版本。

了解数据

波士顿住房数据集来自于 1978 研究以无线方式污染编写一篇研究论文。可以在 Internet 上的多个位置中找到的数据集的不同版本。第一个数据项是:

0.00632, 18.00, 2.310, 0, 0.5380, 6.5750, 65.20
4.0900, 1, 296.0, 15.30, 396.90, 4.98, 24.00

每个数据项具有 14 值并表示一个 506 城镇波士顿附近。前 13 个数字是预测因子变量的值,最后一个值为 (除以 1000) 中心内的中间值房屋价格。简单地说,13 的预测自变量是: 在城镇、 大很多百分比、 行业,Charles River,污染、 平均数字聊天室房子、 房屋年龄信息、 到波士顿,高速公路、 的可访问性的距离每对相邻的分区的百分比的犯罪率税速率,瞳孔教师的比例,黑色居民的比例和低状态居民的百分比。  

因为有 14 的变量,不能直观显示数据集,但可以从关系图中获取数据的大致了解图 2。此图显示中间值房屋价格为城镇中测试数据集的 102 项行业的分区的百分比的函数。

部分波士顿区域内部数据集
图 2 部分波士顿区域内部数据集

在使用神经网络,必须编码非数值数据和应规范化数字数据,以便大型值,例如瞳孔教师的比例的 20.0,不会严重影响较小的值,例如 0.538 污染读取。Charles River 变量是分类值存储为 0 (不相邻城市) 或 1 (相邻)。这些值已重新编码为-1,+ 1。其他 12 预测自变量是数值。对于每个变量,计算最小值和最大值,并且然后针对每个值 x,规范化为 (x-最小值) / (最大值-最小值)。在执行最小最大规范化后的所有值都将介于 0.0 和 1.0 之间。

通过除以 1000,因此范围是从 5.0 到 50.0,与最大约 25.0 处的值进行了已规范中的原始数据的中间值的内部值。我通过将价格除以 10,以便所有中间房屋价格是 0.5 和 5.0,其中大多数是大约 2.5 之间应用其他规范化。

演示程序

图 3 展示了完整的演示程序(为节省空间,进行了少量小幅改动)。为了节省空间,我缩进了两个空格(而不是通常的四个空格)。并请注意,Python 使用 \ 字符的行继续符。我使用记事本来编辑我的程序,但很多我的同事喜欢 Visual Studio 或 VS Code 中,其中有两种完美地支持 Python。

图 3 外壳演示程序波士顿

# boston_dnn.py
# Boston Area House Price dataset regression
# Anaconda3 5.2.0 (Python 3.6.5), PyTorch 1.0.0
import numpy as np
import torch as T  # non-standard alias
# ------------------------------------------------------------
def accuracy(model, data_x, data_y, pct_close):
  n_items = len(data_y)
  X = T.Tensor(data_x)  # 2-d Tensor
  Y = T.Tensor(data_y)  # actual as 1-d Tensor
  oupt = model(X)       # all predicted as 2-d Tensor
  pred = oupt.view(n_items)  # all predicted as 1-d
  n_correct = T.sum((T.abs(pred - Y) < T.abs(pct_close * Y)))
  result = (n_correct.item() * 100.0 / n_items)  # scalar
  return result 
# ------------------------------------------------------------
class Net(T.nn.Module):
  def __init__(self):
    super(Net, self).__init__()
    self.hid1 = T.nn.Linear(13, 10)  # 13-(10-10)-1
    self.hid2 = T.nn.Linear(10, 10)
    self.oupt = T.nn.Linear(10, 1)
    T.nn.init.xavier_uniform_(self.hid1.weight)  # glorot
    T.nn.init.zeros_(self.hid1.bias)
    T.nn.init.xavier_uniform_(self.hid2.weight)
    T.nn.init.zeros_(self.hid2.bias)
    T.nn.init.xavier_uniform_(self.oupt.weight)
    T.nn.init.zeros_(self.oupt.bias)
  def forward(self, x):
    z = T.tanh(self.hid1(x))
    z = T.tanh(self.hid2(z))
    z = self.oupt(z)  # no activation, aka Identity()
    return z
# ------------------------------------------------------------
def main():
  # 0. Get started
  print("\nBoston regression using PyTorch 1.0 \n")
  T.manual_seed(1);  np.random.seed(1)
  # 1. Load data
  print("Loading Boston data into memory ")
  train_file = ".\\Data\\boston_train.txt"
  test_file = ".\\Data\\boston_test.txt"
  train_x = np.loadtxt(train_file, delimiter="\t",
    usecols=range(0,13), dtype=np.float32)
  train_y = np.loadtxt(train_file, delimiter="\t",
    usecols=[13], dtype=np.float32)
  test_x = np.loadtxt(test_file, delimiter="\t",
    usecols=range(0,13), dtype=np.float32)
  test_y = np.loadtxt(test_file, delimiter="\t",
    usecols=[13], dtype=np.float32)
  # 2. Create model
  print("Creating 13-(10-10)-1 DNN regression model \n")
  net = Net()  # all work done above
  # 3. Train model
  net = net.train()  # set training mode
  bat_size = 10
  loss_func = T.nn.MSELoss()  # mean squared error
  optimizer = T.optim.SGD(net.parameters(), lr=0.01)
  n_items = len(train_x)
  batches_per_epoch = n_items // bat_size
  max_batches = 1000 * batches_per_epoch
  print("Starting training")
  for b in range(max_batches):
    curr_bat = np.random.choice(n_items, bat_size,
      replace=False)
    X = T.Tensor(train_x[curr_bat])
    Y = T.Tensor(train_y[curr_bat]).view(bat_size,1)
    optimizer.zero_grad()
    oupt = net(X)
    loss_obj = loss_func(oupt, Y)
    loss_obj.backward()
    optimizer.step()
    if b % (max_batches // 10) == 0:
      print("batch = %6d" % b, end="")
      print("  batch loss = %7.4f" % loss_obj.item(), end="")
      net = net.eval()
      acc = accuracy(net, train_x, train_y, 0.15)
      net = net.train()
      print("  accuracy = %0.2f%%" % acc)      
  print("Training complete \n")
  # 4. Evaluate model
  net = net.eval()  # set eval mode
  acc = accuracy(net, test_x, test_y, 0.15)
  print("Accuracy on test data = %0.2f%%" % acc)
  # 5. Save model - TODO
  # 6. Use model
  raw_inpt = np.array([[0.09266, 34, 6.09, 0, 0.433, 6.495, 18.4,
    5.4917, 7, 329, 16.1, 383.61, 8.67]], dtype=np.float32)
  norm_inpt = np.array([[0.000970, 0.340000, 0.198148, -1,
    0.098765, 0.562177, 0.159629, 0.396666, 0.260870, 0.270992,
    0.372340, 0.966488, 0.191501]], dtype=np.float32)
  X = T.Tensor(norm_inpt)
  y = net(X)
  print("For a town with raw input values: ")
  for (idx,val) in enumerate(raw_inpt[0]):
    if idx % 5 == 0: print("")
    print("%11.6f " % val, end="")
  print("\n\nPredicted median house price = $%0.2f" %
    (y.item()*10000))
if __name__=="__main__":
  main()

演示将导入整个 PyTorch 程序包并将其分配的别名初始化状态传递。一种替代方法是导入的模块和所需的函数。

此演示程序定义了一个名为准确性的帮助器函数。使用回归模型时,没有固有的预测准确性的定义。必须定义如何关闭预测的值必须为目标值才能计为正确的预测。演示程序计算预测的中间值房屋价格为 15%,则返回 true 值的范围内是否正确。

演示程序的所有控制逻辑都包含在一个主函数。程序开始执行通过设置全局 NumPy 和 PyTorch 随机种子,因此结果将是可重现。

将数据加载到内存

该演示将加载使用 NumPy loadtxt 函数的内存中的数据:

train_x = np.loadtxt(train_file, delimiter="\t",
  usecols=range(0,13), dtype=np.float32)
train_y = np.loadtxt(train_file, delimiter="\t",
  usecols=[13], dtype=np.float32)
test_x = np.loadtxt(test_file, delimiter="\t",
  usecols=range(0,13), dtype=np.float32)
test_y = np.loadtxt(test_file, delimiter="\t",
  usecols=[13], dtype=np.float32)

该代码假定将数据放在名为 Data 的子目录中。演示数据是通过将其拆分为训练和测试集预处理过的。数据整理不是从概念上讲困难,但几乎始终是非常耗时且令人讨厌。很多我的同事们喜欢使用 pandas (Python 数据分析) 包来处理数据。

定义神经网络

此演示程序定义了名为继承自 nn 的 Net 程序定义类中的 13-(10-10) 1 神经网络。模块的模块。您可以将 Python __init__ 函数作为类构造函数。请注意,没有显式定义输入的层因为输入的值直接送入第一个隐藏层。

网络有 (13 * 10) + (10 * 10) + (10 * 1) = 240 权重。每个权重初始化为使用 Xavier 一致性算法较小的随机值。网络具有 10 + 10 + 1 = 21 个偏差。每个偏置值初始化为零。

Net 类转发函数定义层如何计算输出。此演示程序使用两个隐藏的层和输出层没有激活 tanh (双曲正切) 激活:

def forward(self, x):
  z = T.tanh(self.hid1(x))
  z = T.tanh(self.hid2(z))
  z = self.oupt(z)
  return z

隐藏的层激活的主要替代方案是线性整流 (ReLU) 激活,但有很多其他功能。

PyTorch 在相对较低的抽象级别工作,因为有许多可以使用的另一种设计模式。例如,而不是定义具有 __init__ 和转发函数,以及使用 net 然后实例化的类 Net = Net(),可以使用顺序的函数,如下所示:

net = T.nn.Sequential(
  T.nn.Linear(13,10),
  T.nn.Tanh(),
  T.nn.Linear(10,10),
  T.nn.Tanh(),
  T.nn.Linear(10,1))

顺序方法要简单得多,但请注意,不需直接控制的权重和偏置初始化算法。一旦您熟悉的库,与使用 PyTorch 时获取更大的灵活性是一个优点。

训练模型

训练模型开始通过以下七个语句:

net = net.train()  # Set training mode
bat_size = 10
loss_func = T.nn.MSELoss()  # Mean squared error
optimizer = T.optim.SGD(net.parameters(), lr=0.01)
n_items = len(train_x)
batches_per_epoch = n_items // bat_size
max_batches = 1000 * batches_per_epoch

PyTorch 有两种模式: 训练和评估版。默认模式是训练、 但在我看来最好是显式设置模式。(通常称为微批处理) 的批处理大小为超参数。对于回归问题,意味着平方的误差是最常见的损失函数。随机梯度下降 (SGD) 算法是最基本的技术,并在许多情况下 Adam 算法提供更好的结果。

该演示程序进行批处理定型项使用一种简单方法。对于演示中,有大约 400 定型项,因此如果批大小为 10,平均访问每个训练项一次 (这通常称为一个时期中机器学习术语) 将需要 400 / 10 = 40 个进行批处理。因此,若要训练 1,000 时期的等效项,演示程序需要 1000 * 40 = 40,000 批处理。

核心培训语句是:

for b in range(max_batches):
  curr_bat = np.random.choice(n_items, bat_size,
    replace=False)
  X = T.Tensor(train_x[curr_bat])
  Y = T.Tensor(train_y[curr_bat]).view(bat_size,1)
  optimizer.zero_grad()
  oupt = net(X)
  loss_obj = loss_func(oupt, Y)
  loss_obj.backward()  # Compute gradients
  optimizer.step()     # Update weights and biases

选择函数从 404 适用的培训项选择 10 个随机索引。项从 NumPy 数组转换为 PyTorch tensors。您可以将 tensor 可以有效地处理 gpu (即使此演示程序不利用 GPU) 的多维数组的形式。奇怪的是命名的视图函数重新定形为二维 tensor 的一维目标值。将 NumPy 数组转换为 PyTorch tensors 和处理数组和 tensor 形状使用 PyTorch 时是一项重大挑战。

后每个 4,000 批处理演示程序显示的值的平均平方错误丢失当前批的 10 个定型项,并在整个 404 项训练数据集上使用当前权重和偏置在模型的预测准确性:

if b % (max_batches // 10) == 0:
  print("batch = %6d" % b, end="")
  print("  batch loss = %7.4f" % loss_obj.item(), end="")
  net = net.eval()
  acc = accuracy(net, train_x, train_y, 0.15)
  net = net.train()
  print("  accuracy = %0.2f%%" % acc)

"/ /"运算符是在 Python 中的整数除法。之前调用程序定义准确性函数时,该演示为 eval 模式设置网络。从技术上讲,没有必要这样做因为训练和评估模式下只能如果给出不同结果的网络使用 dropout 或层批处理规范化。

评估和使用经过训练的模型

完成定型后,演示程序计算结果对测试数据集的模型的预测准确性:

net = net.eval()  # set eval mode
acc = accuracy(net, test_x, test_y, 0.15)
print("Accuracy on test data = %0.2f%%" % acc)

Eval 函数返回对其应用它; 该模型的引用可能已不在赋值语句的情况下调用了它。

在大多数情况下,为模型定型后你想要保存以供将来使用该模型。保存已训练的 PyTorch 模型均这篇文章的讨论范围内的一个位,但可以 PyTorch 文档中找到几个示例。

使用它来进行预测训练回归模型的全部意义。该演示程序进行预测使用 102 测试项中的第一个数据项:

raw_inpt = np.array([[0.09266, 34, 6.09, 0, 0.433, 6.495, 18.4,
  5.4917, 7, 329, 16.1, 383.61, 8.67]], dtype=np.float32)
norm_inpt = np.array([[0.000970, 0.340000, 0.198148, -1,
  0.098765, 0.562177, 0.159629, 0.396666, 0.260870, 0.270992,
  0.372340, 0.966488, 0.191501]], dtype=np.float32)

当有新数据时,必须记住要与已规范化训练数据相同的方式规范化预测因子值。对于最小-最大值规范化,这意味着你需要保存的最小值和最大值为已规范化每个变量。

最后,演示程序会通过发出并显示该预测:

...
  X = T.Tensor(norm_inpt)
  y = net(X)
  print("Predicted = $%0.2f" % (y.item()*10000))
if __name__=="__main__":
  main()

作为单个值 tensor 返回预测的值。Item 函数用于访问此值,以便可以显示。

总结

PyTorch 库是比起其他方案 TensorFlow、 Keras 和 CNTK,尤其是对于示例代码是不太成熟。但在我的同事之间使用的 PyTorch 的增长速度非常快。我认为这一趋势以继续和高质量的示例将会变得越来越多地提供给你。


Dr.James McCaffrey 供职于华盛顿地区雷蒙德市沃什湾的 Microsoft Research。他一直从事多个关键 Microsoft 产品,包括 Internet Explorer 和必应。Dr.可通过 jamccaff@microsoft.com 与 McCaffrey 取得联系。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Chris Lee 和 Ricky Loynd