目录
二元分类问题的目标是预测离散值,其中只有两种可能性。例如,您可能希望根据年龄、居住州、年收入和政治倾向(保守派、温和派、自由派)来预测一个人的性别(男性或女性)。
Visual Studio Magazine以前的文章已经解释了使用PyTorch进行二进制分类。但是,具有深度神经技术的机器学习已经迅速发展。本文根据过去两年的经验更新了二元分类技术和最佳做法。
了解本文内容的一个好方法是查看图1 中演示程序的屏幕截图。该演示首先加载一个包含200项的训练数据文件和一组包含40项的测试数据。每个制表符分隔的行代表一个人。这些字段是性别(男性= 0,女性= 1)、年龄、居住州、年收入和政治类型。目标是从年龄,州,收入和政治倾向预测性别。
图1:使用PyTorch演示运行的二进制分类
将训练数据加载到内存中后,演示将创建一个8-(10-10)-1神经网络。这意味着有八个输入节点,两个隐藏的神经层,每个10个节点和一个输出节点。
该演示准备通过设置批大小10、随机梯度下降(SGD)优化(学习率为0.01)以及通过训练数据的最大训练周期500来训练网络。稍后将解释这些值的含义以及如何确定它们。
演示程序通过计算和显示损失值来监控训练。损失值缓慢减小,这表明训练可能成功。损失值的大小无法直接解释;重要的是损失减少了。
经过500个训练周期后,演示程序将训练模型在训练数据上的准确率计算为82.50%(165个正确点中有200个)。测试数据的模型准确率为85.00%(34个正确中的40个)。对于二元分类模型,除了准确性之外,计算其他指标是标准做法:精度、召回率和F1分数。
评估经过训练的网络后,演示会将训练好的模型保存到文件中,以便无需从头开始重新训练网络即可使用。有两种主要方法可以保存PyTorch模型。该演示使用保存状态方法。
保存模型后,演示预测了一个来自俄克拉荷马州的30岁人的性别,他每年赚40,000美元,政治温和。原始预测为0.3193。此值是伪概率,其中小于0.5的值表示类0(男性),大于0.5的值表示类1(女性)。因此,预测是男性。
本文假设你对Python有基本的熟悉,并且对C族语言有中级或更好的经验,但并不假设你对PyTorch或神经网络了解很多。完整的演示程序源代码和数据可以在这里找到。
安装PyTorch
该演示程序是在Windows 10/11机器上使用Anaconda 2020.02 64位发行版(包含Python 3.7.6)和用于CPU的PyTorch版本1.12.1开发的。安装PyTorch就像驾驶汽车一样——一旦你知道怎么做,就相对容易,但如果你以前没有做过,那就很难了。
我在一家大型科技公司工作,我的工作职责之一是为软件工程师和数据科学家提供培训课程。到目前为止,对于刚接触PyTorch的人来说,最大的障碍是安装。
有几十种不同的方法可以在Windows上安装PyTorch。我强烈建议初学者使用的配置是使用Python的Anaconda发行版并使用pip包管理器安装PyTorch。Python的Anaconda发行版包含一个基本的Python引擎以及500多个经过测试可以相互兼容的加载项包。
安装Python发行版后,您可以通过几种不同的方式安装PyTorch。我建议使用pip实用程序,它是作为Anaconda的一部分安装的。简而言之,您将.whl(“wheel”)文件下载到本地计算机,打开命令外壳并发出命令“pip install (whl-file-name)”。
我已经发布了详细的分步说明,用于安装Windows 10/11版 Anaconda Python 以及在 Windows CPU机器上下载和安装PyTorch 1.12.1 for Python 3.7.6 的详细说明。
准备数据
原始演示数据如下所示:
F 24 michigan 29500.00 liberal
M 39 oklahoma 51200.00 moderate
F 63 nebraska 75800.00 conservative
M 36 michigan 44500.00 moderate
. . .
有240行数据。每条线代表一个人。这五个领域是性别(M,F),年龄,居住州(密歇根州,内布拉斯加州,俄克拉荷马州),年收入和政治类型(保守,温和,自由)。数据是人为的。原始数据被拆分为用于训练的200项集和用于测试的40项集。
原始数据必须进行编码和规范化。结果是:
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
. . .
要预测的变量(通常称为类或标签)是性别,其可能的值为男性或女性。对于PyTorch二元分类,您应该使用0-1编码对变量进行编码以进行预测。演示集男性= 0,女性= 1。编码的顺序是任意的。
由于神经网络只能理解数字,因此必须对状态和政治倾向预测因子值(在神经网络术语中通常称为特征)进行编码。州值编码为密歇根州=(1 0 0)、内布拉斯加州=(0 1 0)和俄克拉荷马州=(0 0 1)。编码的顺序是任意的。如果状态变量有四个可能的值,则编码将是(1 0 0 0)、(0 1 0 0)等。政治倾向值是一热编码为保守=(1 0 0),温和=(0 1 0)和自由=(0 0 1)。
演示数据对数字年龄和年收入值进行标准化。年龄值除以100;例如,年龄= 24归一化为年龄= 0.24。收入值除以100,000;例如,收入= $55,000.00归一化为0.5500。由此产生的归一化年龄和收入值均介于0.0和1.0之间。
通过除以常量来规范化数值数据的技术没有标准名称。另外两种归一化技术称为最小——最大归一化和z分数归一化。我建议尽可能使用常量除法技术。有令人信服的(但目前尚未发表的)研究表明,按常数归一化除法通常比最小——最大归一化或z分数归一化提供更好的结果。这个话题相当复杂。有关详细信息,请参阅“为什么我不对神经网络使用Min-Max或Z-Score归一化”。
演示数据没有任何二进制预测变量,例如“已雇用”,可能的值为“是”或“否”。对于二元预测变量,我建议使用负一加一编码而不是0-1编码。理论上,这两种编码方案都适用于二元预测变量,但在实践中,负一加一编码通常会产生更好的模型。有关说明,请参阅“应将神经网络二元预测变量编码为0和1,还是编码为-1和+1?"
该演示通过规范化数值和编码分类值来预处理原始数据。可以动态规范化和编码训练和测试数据,但预处理通常是一种更简单的方法。
总体程序结构
演示程序的整体结构如清单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()
全局设备设置为“cpu”。如果您使用的是具有GPU处理器的计算机,则设备字符串为“cuda”。我和我的大多数同事在本地CPU机器上开发神经网络,然后在必要时(大量的训练数据或巨大的神经网络),将程序推送到GPU机器并在那里训练它。
该演示具有一个程序定义的PeopleDataset类,用于存储训练和测试数据。数据集对象中的数据可以使用内置的DataLoader对象批量提供以进行训练。可以直接使用训练和测试数据,而不是使用数据集,但此类问题场景很少见,您应该使用数据集来解决大多数问题。
二元神经网络分类器在程序定义的Net类中实现。Net类继承自内置的torch.nn.Module类,该类提供大部分神经网络功能。与其使用类来定义PyTorch神经网络,不如直接使用torch.nn.Sequential类创建神经网络。 使用Sequential更简单,但不如使用程序定义的类灵活。定义PyTorch神经网络有两种完全不同的方法这一事实可能会让初学者感到困惑。
在神经网络二元分类问题中,必须实现程序定义的函数来计算已训练模型的分类准确性。演示程序定义了一个接受网络和数据集对象的metrics()函数。
所有演示程序控制逻辑都包含在程序定义的main()函数中。演示程序首先设置NumPy随机数生成器和PyTorch生成器的种子值。设置种子值很有帮助,因此演示运行大多是可重现的。但是,在使用复杂的神经网络(如Transformer网络)时,由于执行线程不同,因此无法始终保证精确的可重复性。
数据集定义
演示数据集定义如清单2 所示。数据集继承自torch.utils.data.Dataset类,您必须实现三种方法:
- __init__(),将数据从文件作为PyTorch张量加载到内存中
- __len__(),它告诉使用数据集的DataLoader对象有多少项,以便DataLoader知道在训练期间何时处理了所有项
- __getitem__(),它返回单个数据项,而不是预期的一批项
清单2:数据集定义
class PeopleDataset(T.utils.data.Dataset):
# like 0 0.27 0 1 0 0.7610 1 0 0
def __init__(self, src_file):
all_data = np.loadtxt(src_file, usecols=range(0,9),
delimiter="\t", comments="#", dtype=np.float32)
self.x_data = T.tensor(all_data[:,1:9],
dtype=T.float32).to(device)
self.y_data = T.tensor(all_data[:,0],
dtype=T.float32).to(device) # float32 required
self.y_data = self.y_data.reshape(-1,1) # 2-D required
def __len__(self):
return len(self.x_data)
def __getitem__(self, idx):
feats = self.x_data[idx,:] # idx row, all 8 cols
sex = self.y_data[idx,:] # idx row, the only col
return feats, sex # as a Tuple
定义PyTorch数据集并非易事。您必须为每个问题/数据方案定义一个自定义数据集。__init__()方法接受一个src_file参数,该参数告诉数据集训练数据文件所在的位置。整个文件使用NumPy loadtxt()函数作为NumPy二维数组读入内存。常用的替代方案包括NumPy genfromtxt()函数和Pandas read_csv()函数。
对loadtxt()的调用指定参数comments=“#”以指示以“#”开头的行是注释,应忽略。“#”字符是注释的默认值,因此可以省略该参数。
数据从NumPy数组转换为PyTorch张量。请注意,self.y_data中要预测的类标签是float32类型,而不是您可能期望的int64类型。这是二元分类所必需的。self.y_data一维向量必须重塑为二维形式。Reshape(-1,1)语法表示“所有批处理行”。在开发过程中,处理PyTorch矢量和矩阵形状可能非常耗时。
self.x_data和self.y_data使用.to(device)方法加载到内存中,在本例中为“cpu”。由于新创建的PyTorch张量对象的默认设备类型为None,因此最好在实例化新张量时显式使用.to(device)。
__len__()函数返回self.x_data张量矩阵中的行数。您也可以使用len(self.y_data),因为self.y_data向量具有相同数量的值。
__getitem__()方法接受索引参数idx。项目[idx]的预测变量值是使用正常数组索引从self.x_data中提取的。类标签也使用正常的索引语法拉取。返回值是一个Python元组对象,其中一组预测变量值位于元组位置[0],单个关联的类标签位于元组位置[1]。
另一种方法是将预测变量和标签作为Python字典对象返回。该代码如下所示:
def __getitem__(self, idx):
feats = self.x_data[idx,:]
sex = self.y_data[idx,:]
sample = { 'predictors' : feats, 'targets' : sex }
return sample # as Dictionary
字典方法允许您按名称而不是索引访问预测变量和标签。但是,字典方法创建“魔术字符串”,因此元组方法更常见。
演示数据集定义假定预测变量值和类标签位于同一源文件中。在某些情况下,预测变量和标签在单独的文件中定义。在这种情况下,您必须将两个文件路径而不仅仅是一个传递给__init__()方法。
数据集必须能够将所有数据存储在内存中。这通常不是问题。但是对于庞大的数据集,您必须创建一个流数据加载器。这是非常困难的。有关示例,请参阅“如何:为PyTorch创建流数据加载器”。
定义网络
神经网络定义如清单3 所示。网络架构为8-(10-10)-1,具有tanh()隐藏节点激活。二元分类器的输入节点数由训练数据决定。始终有一个输出节点。隐藏层的数量和每层中的节点数量是超参数,必须通过反复试验来确定。
__init__()方法设置层,并选择性地指定如何初始化层权重和偏差。该演示使用显式初始化,但更常见的是使用默认权重和偏差初始化。权重和偏差初始化是一个令人惊讶的复杂主题,有关该主题的文档是PyTorch的一个弱点。初始化算法的选择通常会对神经网络的行为产生很大的影响。
使用默认权重和偏差初始化的优点是简单。缺点是默认初始化算法可以并且已经更改了多次。我的建议是对只有一个或两个隐藏层的简单二元分类器使用显式权重和偏差初始化,但对具有三个或更多隐藏层的分类器使用默认初始化。
清单3:神经网络类定义
class Net(T.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.hid1 = T.nn.Linear(8, 10) # 8-(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)
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 = T.sigmoid(self.oupt(z)) # for BCELoss()
return z
演示网络在隐藏节点上使用tanh()激活。在神经网络的早期,sigmoid()隐藏层激活很常见,但现在很少使用。对于深度神经网络,通常使用relu()激活。基本上没有好的经验法则来决定使用哪个隐藏层激活。尝试tanh()和relu()是个好主意,看看哪个与所有其他超参数结合使用似乎效果更好。
对于那些不熟悉PyTorch二元分类的人来说,一个常见的混淆来源是输出层激活函数。输出激活与训练期间使用的损失函数之间存在很强的耦合。演示程序使用sigmoid()输出层激活。此方法假设您在训练期间使用 BCELoss()(“二进制交叉熵损失”)。输出激活和损失函数的基本理论和机制很复杂,但你不需要完全理解它们来创建二元分类器——只需在输出节点上使用sigmoid()激活并在训练期间使用BCELoss()。
8、10、10、1的网络维度是硬编码的。可以将这些值作为参数传递给__init__()函数,但硬编码方法更简单、更容易理解,在我看来,这比灵活性的轻微损失更大。
总结
本文中介绍的演示代码可用作准备训练数据的指南,并用作为大多数二元分类问题定义神经网络的模板。第2部分将解释如何训练网络,计算训练网络的分类准确性,保存网络以供其他程序使用以及使用网络进行预测。
https://visualstudiomagazine.com/Articles/2022/10/05/binary-classification-using-pytorch.aspx