使用新的PyTorch最佳实践进行二进制分类,第2部分:训练、准确性、预测

目录

总体程序结构

准备训练网络

创建网络

训练网络

计算模型的准确性、精度、召回率

保存已训练的模型

使用模型

总结


这是解释如何创建和使用PyTorch二进制分类器的两篇文章中的第二篇。了解本文内容的一个好方法是查看1 中演示程序的屏幕截图。

演示程序预测一个人的性别(男性、女性)。本系列的第一篇文章介绍了如何准备训练和测试数据,以及如何定义神经网络分类器。本文介绍如何训练网络、计算已训练网络的精度、使用网络进行预测以及保存网络以供其他程序使用。

了解本文内容的一个好方法是查看1 中演示程序的屏幕截图。该演示首先加载一个包含200项的训练数据文件和一组包含40项的测试数据。每个制表符分隔的行代表一个人。这些字段是性别(男性= 0,女性= 1)、年龄、居住州、年收入和政治类型。目标是从年龄,州,收入和政治类型预测性别。

1使用PyTorch演示运行的二进制分类

将训练数据加载到内存中后,演示将创建一个8-10-10-1神经网络。这意味着有八个输入节点,两个隐藏的神经层,每个10个节点和一个输出节点。

该演示准备通过设置批大小10、随机梯度下降(SGD)优化(学习率为0.01)以及通过训练数据的最大训练周期500来训练网络。稍后将解释这些值的含义以及如何确定它们。

演示程序通过计算和显示损失值来监控训练。损失值缓慢减小,表明训练可能成功。损失值的大小无法直接解释;重要的是损失减少了。

经过500个训练周期后,演示程序将训练模型在训练数据上的准确率计算为82.50%200个正确点中有165个)。测试数据的模型准确率为85%40个正确数据中有34个)。对于二元分类模型,除了准确性之外,计算其他指标是标准做法:精度、召回率和F1分数。

评估经过训练的网络后,演示会将训练好的模型保存到文件中,以便无需从头开始重新训练网络即可使用。有两种主要方法可以保存PyTorch模型。该演示使用保存状态方法。

保存模型后,演示预测了一个来自俄克拉荷马州的30岁人的性别,他每年赚40,000美元,是政治温和派。原始预测为0.3193。此值是伪概率,其中小于0.5的值表示类0(男性),大于0.5的值表示类1(女性)。因此,预测是男性。

本文假设你对Python有基本的熟悉,并且对C族语言有中级或更好的经验,但并不假设你对PyTorch或神经网络了解很多。完整的演示程序源代码和数据可以在这里找到。

总体程序结构

演示程序的整体结构如清单1 所示。演示程序名为people_gender.py。该程序导入NumPy(数字Python)库并为其分配np的别名。程序导入PyTorch并为其分配T的别名。大多数PyTorch程序不使用T别名,但我和我的同事经常这样做以节省空间。演示程序使用两个空格而不是更常见的四个空格缩进,再次节省空间。

清单1总体程序结构

# people_gender.py
# binary classification
# PyTorch 1.12.1-CPU Anaconda3-2020.02  Python 3.7.6
# Windows 10/11 

import numpy as np
import torch as T
device = T.device('cpu')

class PeopleDataset(T.utils.data.Dataset): . . .

class Net(T.nn.Module): . . .

def metrics(model, ds, thresh=0.5): . . .

def main():
  # 0. get started
  print("People gender using PyTorch ")
  T.manual_seed(1)
  np.random.seed(1)
  
  # 1. create Dataset objects
  # 2. create network
  # 3. train model
  # 4. evaluate model accuracy
  # 5. save model (state_dict approach)
  # 6. make a prediction
  
  print("End People binary classification demo ")

if __name__ == "__main__":
  main()

演示程序将所有控制逻辑放在main()函数中。我的一些同事更喜欢实现程序定义的train()函数来处理执行训练的代码。

演示程序首先设置NumPy随机数生成器和PyTorch生成器的种子值。设置种子值很有帮助,因此演示运行大多是可重现的。但是,在使用复杂的神经网络(如Transformer网络)时,由于执行线程不同,因此无法始终保证精确的可重复性。

准备训练网络

训练神经网络是查找权重和偏差值的过程,以便网络生成与训练数据匹配的输出。大多数演示程序代码都与训练网络相关联。术语网络和模型通常可以互换使用。在某些开发环境中,网络用于在训练神经网络之前引用神经网络,而模型用于在训练网络之后引用网络。

规范化和编码的训练数据如下所示:

 1   0.24   1   0   0   0.2950   0  0  1
 0   0.39   0   0   1   0.5120   0  1  0
 1   0.63   0   1   0   0.7580   1  0  0
 0   0.36   1   0   0   0.4450   0  1  0
. . .

这些字段是性别(0 =男性,1 =女性)、年龄(除以100)、州(密歇根州=100,内布拉克斯加州=010,俄克拉荷马州=001)、收入(除以100,000)和政治倾向(保守派=100,温和派=010,自由派=001)。

main()函数中,训练和测试数据作为Dataset对象加载到内存中,然后将训练数据集传递给DataLoader对象:

 # 1. create Dataset and DataLoader objects
  print("Creating People train and test Datasets ")
  train_file = ".\\Data\\people_train.txt"
  test_file = ".\\Data\\people_test.txt"
  train_ds = PeopleDataset(train_file)  # 200 rows
  test_ds = PeopleDataset(test_file)    # 40 rows
  bat_size = 10
  train_ldr = T.utils.data.DataLoader(train_ds,
    batch_size=bat_size, shuffle=True)

与必须为每个特定二元分类问题定义的数据集对象不同,DataLoader对象可以按原样使用。批大小10是一个超参数。批量大小设置为1的特殊情况有时称为在线训练。

尽管不是必需的,但通常最好设置一个将训练项总数平均划分的批大小,以便所有批的训练数据具有相同的大小。在演示中,批量大小为10200个训练项目,每个批次将有20个项目。当批大小不能均匀划分训练项数时,最后一个批将小于所有其他批。类有一个可选的drop_last参数,默认值为False。如果设置为True,则数据加载程序将忽略较小的最后一批。

将随机参数显式设置为True非常重要。默认值为False。当shuffle设置为True时,训练数据将以随机顺序提供,这是您在训练期间所需的顺序。如果随机播放设置为False,则按顺序提供训练数据。这几乎总是导致训练失败,因为网络权重和偏差的更新会振荡,并且没有取得进展。

创建网络

演示程序创建神经网络,如下所示:

 # 2. create neural network
  print("Creating 8-(10-10)-1 binary NN classifier ")
  net = Net().to(device)
  net.train()

神经网络使用普通的Python语法进行实例化,但附加了.to(device)以明确地将存储放置在“cpu”“cuda”内存中。回想一下,设备是演示中设置为“cpu”的全局范围值。

网络被设置为训练模式,并带有一些误导性的语句net.train()PyTorch神经网络可以处于两种模式之一,train()eval()。网络在训练期间应处于train()模式,在所有其他时间应处于eval()模式。

train()vs. eval()模式经常让刚接触PyTorch的人感到困惑,部分原因是在许多情况下,网络处于什么模式并不重要。简而言之,如果神经网络使用dropout或批量归一化,那么在计算输出值时会得到不同的结果,具体取决于网络是处于train()还是eval()模式。但是,如果网络不使用dropout或批量规范化,则train()eval()模式的结果相同。

由于演示网络不使用dropout或批量规范化,因此无需在train()eval()模式之间切换。但是,在我看来,在训练期间始终将网络显式设置为train()模式并在所有其他时间将eval()模式显式设置为是一种很好的做法。默认情况下,网络处于train()模式。

net.train()这个语句相当具有误导性,因为它表明正在进行某种训练。如果我是实现train()方法的人,我会将其命名为set_train_mode()。此外,train()方法通过引用操作,因此语句net.train()修改了net对象。如果你是函数式编程的粉丝,你可以写net = net.train()

训练网络

训练网络的代码如清单2 所示。训练神经网络涉及两个嵌套循环。外环循环迭代固定数量的周期(可能出现短路退出)。周期是一次完整的训练数据传递。内部循环循环遍历所有训练数据项。

清单2训练网络

# 3. train network
  lrn_rate = 0.01
  loss_func = T.nn.BCELoss()  # binary cross entropy
  optimizer = T.optim.SGD(net.parameters(),
    lr=lrn_rate)
  max_epochs = 500
  ep_log_interval = 100

  print("Loss function: " + str(loss_func))
  print("Optimizer: " + str(optimizer.__class__.__name__))
  print("Learn rate: " + "%0.3f" % lrn_rate)
  print("Batch size: " + str(bat_size))
  print("Max epochs: " + str(max_epochs))

  print("Starting training")
  for epoch in range(0, max_epochs):
    epoch_loss = 0.0            # for one full epoch
    for (batch_idx, batch) in enumerate(train_ldr):
      X = batch[0]             # [bs,8]  inputs
      Y = batch[1]             # [bs,1]  targets
      oupt = net(X)            # [bs,1]  computeds 

      loss_val = loss_func(oupt, Y)   # a tensor
      epoch_loss += loss_val.item()   # accumulate
      optimizer.zero_grad()  # reset all gradients
      loss_val.backward()    # compute new gradients
      optimizer.step()       # update all weights

    if epoch % ep_log_interval == 0:
      print("epoch = %4d   loss = %8.4f" % \
        (epoch, epoch_loss))
  print("Done ")

准备训练的五个语句是:

  lrn_rate = 0.01
  loss_func = T.nn.BCELoss()  # binary cross entropy
  optimizer = T.optim.SGD(net.parameters(),
    lr=lrn_rate)
  max_epochs = 500
  ep_log_interval = 100

要训练的周期数是一个超参数,必须通过反复试验来确定。ep_log_interval指定显示进度消息的频率。

损失函数设置为BCELoss(),它假定输出节点应用了sigmoid()激活。损失函数和输出节点激活之间存在很强的耦合性。在神经网络的早期,经常使用MSELoss()(均方误差),但BCELoss()现在更为常见。

该演示使用随机梯度下降优化(SGD),固定学习率为0.01,用于控制每次更新时权重和偏差的变化程度。PyTorch支持13种不同的优化算法。最常见的两种是SGDAdam(自适应矩估计)。SGD通常适用于简单网络,包括二进制分类器。对于深度神经网络,Adam通常比SGD工作得更好。

PyTorch初学者有时会陷入一个陷阱,试图学习每个优化算法的所有内容。我大多数有经验的同事只使用两三种算法并调整学习率。我的建议是使用SGDAdam,并且仅在这两种算法失败时才尝试其他算法。

监视训练进度很重要,因为训练失败是常态而不是例外。有几种方法可以监视训练进度。演示程序使用最简单的方法,即累积一个周期的总损失,然后每隔一段时间显示累积的损失值(演示中ep_log_interval = 100)。

内部训练循环是完成所有工作的地方:

 for (batch_idx, batch) in enumerate(train_ldr):
    X = batch[0]  # inputs
    Y = batch[1]  # correct class/label/politics
    optimizer.zero_grad()
    oupt = net(X)
    loss_val = loss_func(oupt, Y)  # a tensor
    epoch_loss += loss_val.item()  # accumulate
    loss_val.backward()
    optimizer.step()

enumerate()函数返回当前批处理索引(019)和一批输入值(年龄、州、收入、政治)以及关联的正确目标值(01)。使用enumerate()是可选的,您可以通过编写“for batch in train_ldr”来跳过获取批处理索引。

BCELoss()损失函数返回一个保存单个数值的PyTorch张量。该值是使用 item()方法提取的,因此可以累积为普通的非张量数值。在早期版本的PyTorch中,需要使用item()方法,但较新版本的PyTorch执行隐式类型转换,因此不需要调用item()。在我看来,显式使用item()方法是更好的编码风格。

back()方法计算梯度。每个权重和偏差都有一个相关的梯度。梯度是指示应如何调整相关权重或偏差的数值,以便减少计算输出和目标输出之间的误差/损失。请务必记住在调用back()方法之前调用zero_grad()方法。step()方法使用新计算的梯度来更新网络权重和偏差。

大多数神经二元分类器可以在相对较短的时间内进行训练。在训练需要几个小时或更长时间的情况下,您应该定期保存权重和偏差的值,以便在机器出现故障(断电、网络连接断开等)时,您可以重新加载保存的检查点,避免从头开始。

保存训练检查点不在本文的讨论范围之内。有关保存训练检查点的示例和说明,请参阅此博客文章

计算模型的准确性、精度、召回率

该演示使用程序定义的metrics()函数来计算模型分类准确性、精度、召回率和F1分数。该函数如清单3 所示。计算分类精度在原理上相对简单。准确度只是正确预测的数量除以所做的预测总数。

在许多情况下,普通分类准确性不是一个好的指标。例如,如果数据集有950个类0的数据项和50个类1的数据项,则预测任何输入的类0的模型的准确度将达到95%。精度和召回率提供了对偏斜数据不那么敏感的备用指标。F1分数是精度和召回率的平均值。

在二元分类中,真阳性TP)是正确预测的1类项目的数量。误报FP)是错误预测的1类项目的数量。真阴性TN)是正确预测的0类项目的数量。假阴性FN)是错误预测的0类项目的数量。

使用这些定义,如果N是数据项的总数,则准确性、精度、召回率和F1分数为:

accuracy  = (TP + TN)  / N
precision = TP / (TP + FP)
recall    = TP / (TP + FN)
F1        = 2 / [(1 / precision) + (1 / recall)]

F1分数是精度和召回率的调和平均值,而不是常规平均值/平均值,因为精度和召回率是比率。例如,一个伟大的酒吧赌注询问一辆以每小时30英里的速度从AB的汽车的平均速度,然后以60英里/小时的速度从B返回AA。平均速度为2/1/30 + 1/60= 2/90/1800 = 40.0英里/小时,而不是(30 + 60/2 = 45.0英里/小时。

清单3计算模型分类指标

def metrics(model, ds, thresh=0.5):
  tp = 0; tn = 0; fp = 0; fn = 0
  for i in range(len(ds)):
    inpts = ds[i][0]         # Tuple style
    target = ds[i][1]        # float32  [0.0] or [1.0]
    with T.no_grad():
      p = model(inpts)       # between 0.0 and 1.0

    # should really avoid 'target == 1.0'
    if target > 0.5 and p >= thresh:    # TP
      tp += 1
    elif target > 0.5 and p < thresh:   # FP
      fp += 1
    elif target < 0.5 and p < thresh:   # TN
      tn += 1
    elif target < 0.5 and p >= thresh:  # FN
      fn += 1

  N = tp + fp + tn + fn
  if N != len(ds):
    print("FATAL LOGIC ERROR in metrics()")

  accuracy = (tp + tn) / (N * 1.0)
  precision = (1.0 * tp) / (tp + fp)
  recall = (1.0 * tp) / (tp + fn)
  f1 = 2.0 / ((1.0 / precision) + (1.0 / recall))
  return (accuracy, precision, recall, f1)  # as a Tuple

metrics()函数在torch.no_grad()块中计算预测输出,以便不会更新网络梯度。网络的输出是介于0.01.0之间的值,正确的目标值是0.01.0,因为类型为float32,而不是整数类型。从概念上讲,真阳性的计算公式为:如果目标== 1.0p >= 0.5。但是,比较两个float32 以获得完全相等是一种不好的做法,因此演示代码为:如果目标> 0.5p >= 0.5

metrics()函数不是使用标准的固定0.5值作为确定类0或类1的预测的阈值,而是接受thresh参数。改变阈值的值将更改准确性、精度、召回率和F1分数的结果。如果创建在x轴上具有各种阈值,在y轴上具有真阳性率的图形,则该图形称为ROC(接收器工作特性)曲线。

演示程序在使用以下语句训练后调用metrics()函数:

 # 4. evaluate model
  net.eval()
  metrics_train = metrics(net, train_ds, thresh=0.5)
  print("\nMetrics for train data: ")
  print("accuracy  = %0.4f " % metrics_train[0])
  print("precision = %0.4f " % metrics_train[1])
  print("recall    = %0.4f " % metrics_train[2])
  print("F1        = %0.4f " % metrics_train[3])
  # similarly for test data

请注意,在调用metrics()函数之前,网络已设置为eval()模式。在这个例子中,设置eval()模式不是必需的,但在我看来这样做是很好的风格。

人们很容易过度考虑准确性、精确度、召回率和F1分数。它们都只是模型有效性的衡量标准。只有当其中一个指标与其他指标明显不同时,您才应该关注,例如,精度= 0.93但召回率=0.08

保存已训练的模型

演示程序使用以下语句保存训练的模型:

 # 5. save model
  print("Saving trained model state_dict ")
  net.eval()
  path = ".\\Models\\people_gender_model.pt"
  T.save(net.state_dict(), path)

该代码假定有一个名为Model的目录。有两种主要方法可以保存PyTorch模型。您可以仅保存定义网络的权重和偏差,也可以保存整个网络定义,包括权重和偏差。该演示使用第一种方法。

模型权重和偏差以及其他一些信息保存在state_dict()字典对象中。torch.save()方法接受字典和指示保存位置的文件名。您可以使用任何您想要的文件扩展名,但.pt.pth是两个常见的选择。

要使用其他程序中保存的模型,该程序必须包含网络类定义。然后权重和偏差可以像这样加载:

model = Net()  # requires class definition
model.eval()
fn = ".\\Models\\people_gender_model.pt"
model.load_state_dict(T.load(fn))
# use model to make prediction(s)

保存或加载经过训练的模型时,模型应处于eval()模式而不是train()模式。保存PyTorch模型的另一种方法是使用ONNX(开放神经网络交换)。这允许跨平台使用。

使用模型

训练网络分类器后,演示程序使用该模型对以前未见过的新人进行性别预测: 

# 6. make a prediction 
  print("Setting age = 30  Oklahoma  $40,000  moderate ")
  inpt = np.array([[0.30, 0,0,1, 0.4000, 0,1,0]],
    dtype=np.float32)
  inpt = T.tensor(inpt, dtype=T.float32).to(device)
  net.eval()
  with T.no_grad():
    oupt = net(inpt)   # a Tensor
  pred_prob = oupt.item()  # scalar, [0.0, 1.0]
  print("Computed output: ", end="")
  print("%0.4f" % pred_prob)

输入是一个30岁,住在俄克拉荷马州,年收入40,000美元并且是政治温和派的人。由于网络是在规范化和编码数据上进行训练的,因此必须以相同的方式对输入进行规范化和编码。

请注意方括号的双集。PyTorch网络希望输入采用批处理的形式。额外的括号集创建批大小为1的数据项。像这样的细节可能需要很多时间来调试。

由于神经网络在输出节点上具有sigmoid()激活,因此预测的输出采用伪概率的形式。演示程序最后以友好的格式显示预测的性别:

. . .
  if pred_prob < 0.5:
    print("Prediction = male")
  else:
    print("Prediction = female")
  print("End People binary classification demo ")
if __name__== "__main__":
  main()

总结

本文介绍的二元分类技术在训练期间使用具有sigmoid()激活和BCELoss()的单个输出节点。可以将二元分类问题视为多类分类的特例。在训练期间,您将使用两个具有log_softmax()激活和NLLLoss()的输出节点。但是,本文中介绍的技术更为常见。

有许多二元分类技术。使用神经网络通常会产生良好的结果,但神经网络通常比其他不太复杂的技术需要更多的训练数据。替代的二元分类技术包括:

  • 逻辑回归(仅适用于线性可分数据)
  • 朴素贝叶斯(假设预测变量是独立的)
  • k最近邻(仅适用于严格的数值预测变量)
  • 决策树(往往脆弱且敏感)
  • XGBoost(难以定制的复杂决策树)
  • SVM支持向量机(难以调优,难以定制,不会自然扩展到多类分类)

https://visualstudiomagazine.com/articles/2022/10/14/binary-classification-using-pytorch-2.aspx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值