Python-机器学习秘籍第二版-六-

114 阅读57分钟

Python 机器学习秘籍第二版(六)

原文:annas-archive.org/md5/343c5e6c97737f77853e89eacb95df75

译者:飞龙

协议:CC BY-NC-SA 4.0

第十八章:朴素贝叶斯

18.0 引言

贝叶斯定理是理解某些事件概率的首选方法,如在给定一些新信息 P ( A ∣ B ) 和对事件概率的先验信念 P ( A ) 的情况下,事件 P ( B ∣ A ) 的概率。

P ( A ∣ B ) = P(B∣A)P(A) P(B)

贝叶斯方法在过去十年中的流行度急剧上升,越来越多地在学术界、政府和企业中与传统的频率学应用竞争。在机器学习中,贝叶斯定理在分类问题上的一种应用是朴素贝叶斯分类器。朴素贝叶斯分类器将多种实用的机器学习优点结合到一个单一的分类器中。这些优点包括:

  • 一种直观的方法

  • 能够处理少量数据

  • 训练和预测的低计算成本

  • 在各种设置中通常能够产生可靠的结果

具体来说,朴素贝叶斯分类器基于:

P ( y ∣ x 1 , … , x j ) = P(x 1 ,…,x j ∣y)P(y) P(x 1 ,…,x j )

其中:

  • P ( y ∣ x 1 , … , x j ) 被称为后验概率,表示观察值为 x 1 , … , x j 特征时类别 y 的概率。

  • P(x 1 ,…,x j ∣y) 被称为似然,表示在给定类别 y 时,特征 x 1 , … , x j 的观察值的可能性。

  • P(y) 被称为先验概率,表示在观察数据之前,类别 y 的概率信念。

  • P(x 1 ,…,x j ) 被称为边缘概率

在朴素贝叶斯中,我们比较每个可能类别的观测后验概率值。具体来说,因为边际概率在这些比较中是恒定的,我们比较每个类别后验的分子部分。对于每个观测,具有最大后验分子的类别成为预测类别,y^。

有两个关于朴素贝叶斯分类器需要注意的重要事项。首先,对于数据中的每个特征,我们必须假设似然的统计分布,P(x j ∣y)。常见的分布包括正态(高斯)、多项式和伯努利分布。选择的分布通常由特征的性质(连续、二进制等)决定。其次,朴素贝叶斯之所以得名,是因为我们假设每个特征及其结果的似然是独立的。这种“朴素”的假设在实践中往往是错误的,但并不会阻止构建高质量的分类器。

在本章中,我们将介绍使用 scikit-learn 训练三种类型的朴素贝叶斯分类器,使用三种不同的似然分布。此后,我们将学习如何校准朴素贝叶斯模型的预测,使其可解释。

18.1 训练连续特征的分类器

问题

您只有连续特征,并且希望训练朴素贝叶斯分类器。

解决方案

在 scikit-learn 中使用高斯朴素贝叶斯分类器:

# Load libraries
from sklearn import datasets
from sklearn.naive_bayes import GaussianNB

# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target

# Create Gaussian naive Bayes object
classifer = GaussianNB()

# Train model
model = classifer.fit(features, target)

讨论

最常见的朴素贝叶斯分类器类型是高斯朴素贝叶斯。在高斯朴素贝叶斯中,我们假设给定观测的特征值的似然,x,属于类别y,遵循正态分布:

p ( x j ∣ y ) = 1 2πσ y 2 e -(x j -μ y ) 2 2σ y 2

其中σ y 2和μ y分别是特征x j对类别y的方差和均值。由于正态分布的假设,高斯朴素贝叶斯最适合于所有特征均为连续的情况。

在 scikit-learn 中,我们像训练其他模型一样训练高斯朴素贝叶斯,使用fit,然后可以对观测的类别进行预测:

# Create new observation
new_observation = [[ 4,  4,  4,  0.4]]

# Predict class
model.predict(new_observation)
array([1])

朴素贝叶斯分类器的一个有趣方面之一是,它们允许我们对目标类别分配先验信念。我们可以使用GaussianNB priors参数来实现这一点,该参数接受目标向量每个类别的概率列表:

# Create Gaussian naive Bayes object with prior probabilities of each class
clf = GaussianNB(priors=[0.25, 0.25, 0.5])

# Train model
model = classifer.fit(features, target)

如果我们不向priors参数添加任何参数,则根据数据调整先验。

最后,请注意,从高斯朴素贝叶斯获得的原始预测概率(使用predict_proba输出)未经校准。也就是说,它们不应被信任。如果我们想要创建有用的预测概率,我们需要使用等渗回归或相关方法进行校准。

另请参阅

18.2 训练离散和计数特征的分类器

问题

给定离散或计数数据,您需要训练一个朴素贝叶斯分类器。

解决方案

使用多项式朴素贝叶斯分类器:

# Load libraries
import numpy as np
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import CountVectorizer

# Create text
text_data = np.array(['I love Brazil. Brazil!',
                      'Brazil is best',
                      'Germany beats both'])

# Create bag of words
count = CountVectorizer()
bag_of_words = count.fit_transform(text_data)

# Create feature matrix
features = bag_of_words.toarray()

# Create target vector
target = np.array([0,0,1])

# Create multinomial naive Bayes object with prior probabilities of each class
classifer = MultinomialNB(class_prior=[0.25, 0.5])

# Train model
model = classifer.fit(features, target)

讨论

多项式朴素贝叶斯的工作方式与高斯朴素贝叶斯类似,但特征被假定为多项式分布。实际上,这意味着当我们有离散数据时(例如,电影评分从 1 到 5),这种分类器通常被使用。多项式朴素贝叶斯最常见的用途之一是使用词袋或tf-idf方法进行文本分类(参见 Recipes 6.9 和 6.10)。

在我们的解决方案中,我们创建了一个包含三个观察结果的玩具文本数据集,并将文本字符串转换为词袋特征矩阵和相应的目标向量。然后,我们使用MultinomialNB来训练一个模型,同时为两个类别(支持巴西和支持德国)定义了先验概率。

MultinomialNB的工作方式类似于GaussianNB;模型使用fit进行训练,并且可以使用predict进行预测:

# Create new observation
new_observation = [[0, 0, 0, 1, 0, 1, 0]]

# Predict new observation's class
model.predict(new_observation)
array([0])

如果未指定class_prior,则使用数据学习先验概率。但是,如果我们想要使用均匀分布作为先验,可以设置fit_prior=False

最后,MultinomialNB包含一个添加平滑的超参数alpha,应该进行调节。默认值为1.00.0表示不进行平滑。

18.3 训练二元特征的朴素贝叶斯分类器

问题

您有二元特征数据,并需要训练一个朴素贝叶斯分类器。

解决方案

使用伯努利朴素贝叶斯分类器:

# Load libraries
import numpy as np
from sklearn.naive_bayes import BernoulliNB

# Create three binary features
features = np.random.randint(2, size=(100, 3))

# Create a binary target vector
target = np.random.randint(2, size=(100, 1)).ravel()

# Create Bernoulli naive Bayes object with prior probabilities of each class
classifer = BernoulliNB(class_prior=[0.25, 0.5])

# Train model
model = classifer.fit(features, target)

讨论

伯努利朴素贝叶斯分类器假设所有特征都是二元的,即它们只能取两个值(例如,已经进行了独热编码的名义分类特征)。与其多项式兄弟一样,伯努利朴素贝叶斯在文本分类中经常被使用,当我们的特征矩阵仅是文档中单词的存在或不存在时。此外,像MultinomialNB一样,BernoulliNB也有一个添加平滑的超参数alpha,我们可以使用模型选择技术来调节。最后,如果我们想使用先验概率,可以使用class_prior参数并将其设置为包含每个类的先验概率的列表。如果我们想指定均匀先验,可以设置fit_prior=False

model_uniform_prior = BernoulliNB(class_prior=None, fit_prior=False)

18.4 校准预测概率

问题

您希望校准朴素贝叶斯分类器的预测概率,以便能够解释它们。

解决方案

使用 CalibratedClassifierCV

# Load libraries
from sklearn import datasets
from sklearn.naive_bayes import GaussianNB
from sklearn.calibration import CalibratedClassifierCV

# Load data
iris = datasets.load_iris()
features = iris.data
target = iris.target

# Create Gaussian naive Bayes object
classifer = GaussianNB()

# Create calibrated cross-validation with sigmoid calibration
classifer_sigmoid = CalibratedClassifierCV(classifer, cv=2, method='sigmoid')

# Calibrate probabilities
classifer_sigmoid.fit(features, target)

# Create new observation
new_observation = [[ 2.6,  2.6,  2.6,  0.4]]

# View calibrated probabilities
classifer_sigmoid.predict_proba(new_observation)
array([[0.31859969, 0.63663466, 0.04476565]])

讨论

类概率是机器学习模型中常见且有用的一部分。在 scikit-learn 中,大多数学习算法允许我们使用 predict_proba 来查看类成员的预测概率。例如,如果我们只想在模型预测某个类的概率超过 90%时预测该类,这将非常有用。然而,一些模型,包括朴素贝叶斯分类器,输出的概率不是基于现实世界的。也就是说,predict_proba 可能会预测一个观测属于某一类的概率是 0.70,而实际上可能是 0.10 或 0.99。具体来说,在朴素贝叶斯中,虽然对不同目标类的预测概率排序是有效的,但原始预测概率往往会取极端值,接近 0 或 1。

要获得有意义的预测概率,我们需要进行所谓的校准。在 scikit-learn 中,我们可以使用 CalibratedClassifierCV 类通过 k 折交叉验证创建良好校准的预测概率。在 CalibratedClassifierCV 中,训练集用于训练模型,测试集用于校准预测概率。返回的预测概率是 k 折交叉验证的平均值。

使用我们的解决方案,我们可以看到原始和良好校准的预测概率之间的差异。在我们的解决方案中,我们创建了一个高斯朴素贝叶斯分类器。如果我们训练该分类器,然后预测新观测的类概率,我们可以看到非常极端的概率估计:

# Train a Gaussian naive Bayes then predict class probabilities
classifer.fit(features, target).predict_proba(new_observation)
array([[2.31548432e-04, 9.99768128e-01, 3.23532277e-07]])

然而,如果在我们校准预测的概率之后(我们在我们的解决方案中完成了这一步),我们得到非常不同的结果:

# View calibrated probabilities
array([[0.31859969, 0.63663466, 0.04476565]])
array([[ 0.31859969,  0.63663466,  0.04476565]])

CalibratedClassifierCV 提供两种校准方法——Platt 的 sigmoid 模型和等温回归——由 method 参数定义。虽然我们没有空间详细讨论,但由于等温回归是非参数的,当样本量非常小时(例如 100 个观测),它往往会过拟合。在我们的解决方案中,我们使用了包含 150 个观测的鸢尾花数据集,因此使用了 Platt 的 sigmoid 模型。

第十九章:聚类

19.0 介绍

在本书的大部分内容中,我们已经研究了监督机器学习——我们既可以访问特征又可以访问目标的情况。不幸的是,这并不总是事实。经常情况下,我们遇到的情况是我们只知道特征。例如,想象一下我们有一家杂货店的销售记录,并且我们想要按照购物者是否是折扣俱乐部会员来分割销售记录。使用监督学习是不可能的,因为我们没有一个目标来训练和评估我们的模型。然而,还有另一种选择:无监督学习。如果杂货店的折扣俱乐部会员和非会员的行为实际上是不同的,那么两个会员之间的平均行为差异将小于会员和非会员购物者之间的平均行为差异。换句话说,会有两个观察结果的簇。

聚类算法的目标是识别那些潜在的观察结果分组,如果做得好,即使没有目标向量,我们也能够预测观察结果的类别。有许多聚类算法,它们有各种各样的方法来识别数据中的簇。在本章中,我们将介绍一些使用 scikit-learn 的聚类算法以及如何在实践中使用它们。

19.1 使用 K 均值进行聚类

问题

你想要将观察结果分成k组。

解决方案

使用k 均值聚类

# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans

# Load data
iris = datasets.load_iris()
features = iris.data

# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)

# Create k-means object
cluster = KMeans(n_clusters=3, random_state=0, n_init="auto")

# Train model
model = cluster.fit(features_std)

讨论

k 均值聚类是最常见的聚类技术之一。在 k 均值聚类中,算法试图将观察结果分成k组,每组的方差大致相等。组数k由用户作为超参数指定。具体来说,在 k 均值聚类中:

  1. 在随机位置创建k聚类的“中心”点。

  2. 对于每个观察结果:

    1. 计算每个观察结果与k中心点的距离。

    2. 观察结果被分配到最近中心点的簇中。

  3. 中心点被移动到各自簇的平均值(即,中心)。

  4. 步骤 2 和 3 重复,直到没有观察结果在簇成员资格上发生变化。

在这一点上,算法被认为已经收敛并停止。

关于 k 均值聚类有三点需要注意。首先,k 均值聚类假设簇是凸形的(例如,圆形,球形)。其次,所有特征都是等比例缩放的。在我们的解决方案中,我们标准化了特征以满足这个假设。第三,各组是平衡的(即,观察结果的数量大致相同)。如果我们怀疑无法满足这些假设,我们可以尝试其他聚类方法。

在 scikit-learn 中,k-means 聚类是在 KMeans 类中实现的。最重要的参数是 n_clusters,它设置聚类数 k。在某些情况下,数据的性质将决定 k 的值(例如,学校学生的数据将有一个班级对应一个聚类),但通常我们不知道聚类数。在这些情况下,我们希望基于某些准则选择 k。例如,轮廓系数(参见第 11.9 节)可以衡量聚类内部的相似性与聚类间的相似性。此外,由于 k-means 聚类计算开销较大,我们可能希望利用计算机的所有核心。我们可以通过设置 n_jobs=-1 来实现这一点。

在我们的解决方案中,我们有点作弊,使用了已知包含三个类别的鸢尾花数据。因此,我们设置 k = 3。我们可以使用 labels_ 查看每个观测数据的预测类别:

# View predicted class
model.labels_
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 2, 2, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2,
       1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 1, 2, 1, 2,
       2, 1, 2, 1, 1, 2, 2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 1, 1, 2, 2, 2, 2,
       2, 1, 1, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 1], dtype=int32)

如果我们将其与观测数据的真实类别进行比较,可以看到,尽管类标签有所不同(即 012),k-means 的表现还是相当不错的:

# View true class
iris.target
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])

然而,正如你所想象的那样,如果我们选择了错误的聚类数,k-means 的性能将显著甚至可能严重下降。

最后,与其他 scikit-learn 模型一样,我们可以使用训练好的聚类模型来预测新观测数据的值:

# Create new observation
new_observation = [[0.8, 0.8, 0.8, 0.8]]

# Predict observation's cluster
model.predict(new_observation)
array([2], dtype=int32)

预测观测数据属于距离其最近的聚类中心点。我们甚至可以使用 cluster_centers_ 查看这些中心点:

# View cluster centers
model.cluster_centers_
array([[-1.01457897,  0.85326268, -1.30498732, -1.25489349],
       [-0.01139555, -0.87600831,  0.37707573,  0.31115341],
       [ 1.16743407,  0.14530299,  1.00302557,  1.0300019 ]])

参见

19.2 加速 K-Means 聚类

问题

您希望将观测数据分组成 k 组,但 k-means 太耗时。

解决方案

使用 mini-batch k-means:

# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import MiniBatchKMeans

# Load data
iris = datasets.load_iris()
features = iris.data

# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)

# Create k-mean object
cluster = MiniBatchKMeans(n_clusters=3, random_state=0, batch_size=100,
       n_init="auto")

# Train model
model = cluster.fit(features_std)

讨论

Mini-batch k-means 类似于讨论中的 k-means 算法(见第 19.1 节)。不详细讨论的话,两者的区别在于,mini-batch k-means 中计算成本最高的步骤仅在随机抽样的观测数据上进行,而不是全部观测数据。这种方法可以显著减少算法找到收敛(即拟合数据)所需的时间,只有少量的质量损失。

MiniBatchKMeans 类似于 KMeans,但有一个显著的区别:batch_size 参数。batch_size 控制每个批次中随机选择的观测数据数量。批次大小越大,训练过程的计算成本越高。

19.3 使用均值漂移进行聚类

问题

您希望在不假设聚类数或形状的情况下对观测数据进行分组。

解决方案

使用均值漂移聚类:

# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import MeanShift

# Load data
iris = datasets.load_iris()
features = iris.data

# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)

# Create mean shift object
cluster = MeanShift(n_jobs=-1)

# Train model
model = cluster.fit(features_std)

讨论

我们之前讨论过 k-means 聚类的一个缺点是在训练之前需要设置聚类数 k,并且该方法对聚类形状作出了假设。一种无此限制的聚类算法是均值漂移。

均值漂移是一个简单的概念,但有些难以解释。因此,通过类比可能是最好的方法。想象一个非常雾蒙蒙的足球场(即,二维特征空间),上面站着 100 个人(即,我们的观察结果)。因为有雾,一个人只能看到很短的距离。每分钟,每个人都会四处张望,并朝着能看到最多人的方向迈出一步。随着时间的推移,人们开始团结在一起,重复向更大的人群迈步。最终的结果是围绕场地的人群聚类。人们被分配到他们最终停留的聚类中。

scikit-learn 的实际均值漂移实现,MeanShift,更为复杂,但遵循相同的基本逻辑。MeanShift有两个重要的参数我们应该注意。首先,bandwidth设置了观察使用的区域(即核心)的半径,以确定向何处移动。在我们的类比中,bandwidth 代表一个人透过雾能看到的距离。我们可以手动设置此参数,但默认情况下会自动估算一个合理的带宽(计算成本显著增加)。其次,在均值漂移中有时没有其他观察在观察的核心中。也就是说,在我们的足球场上,一个人看不到其他人。默认情况下,MeanShift将所有这些“孤儿”观察分配给最近观察的核心。但是,如果我们希望排除这些孤儿,我们可以设置cluster_all=False,其中孤儿观察被赋予标签-1

参见

19.4 使用 DBSCAN 进行聚类

问题

您希望将观察结果分组为高密度的聚类。

解决方案

使用 DBSCAN 聚类:

# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import DBSCAN

# Load data
iris = datasets.load_iris()
features = iris.data

# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)

# Create DBSCAN object
cluster = DBSCAN(n_jobs=-1)

# Train model
model = cluster.fit(features_std)

讨论

DBSCAN的动机在于,聚类将是许多观察结果密集堆积的区域,并且不对聚类形状做出假设。具体来说,在 DBSCAN 中:

  1. 选择一个随机观察结果,x[i]

  2. 如果*x[i]*有足够数量的近邻观察,我们认为它是聚类的一部分。

  3. 步骤 2 递归地重复对*x[i]*的所有邻居,邻居的邻居等的处理。这些是聚类的核心观察结果。

  4. 一旦步骤 3 耗尽附近的观察,就会选择一个新的随机点(即,在步骤 1 重新开始)。

一旦完成这一步骤,我们就得到了多个聚类的核心观察结果集。最终,任何靠近聚类但不是核心样本的观察被认为是聚类的一部分,而不靠近聚类的观察则被标记为离群值。

DBSCAN有三个主要的参数需要设置:

eps

一个观察到另一个观察的最大距离,以便将其视为邻居。

min_samples

小于eps距离的观察数目最少的观察,被认为是核心观察结果。

metric

eps使用的距离度量——例如,minkowskieuclidean(注意,如果使用 Minkowski 距离,参数p可以用来设置 Minkowski 度量的幂)。

如果我们查看我们的训练数据中的集群,我们可以看到已经识别出两个集群,01,而异常值观测被标记为-1

# Show cluster membership
model.labels_
array([ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, -1, -1,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, -1, -1,
        0,  0,  0,  0,  0,  0,  0, -1,  0,  0,  0,  0,  0,  0,  0,  0,  1,
        1,  1,  1,  1,  1, -1, -1,  1, -1, -1,  1, -1,  1,  1,  1,  1,  1,
       -1,  1,  1,  1, -1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,
       -1,  1, -1,  1,  1,  1,  1,  1, -1,  1,  1,  1,  1, -1,  1, -1,  1,
        1,  1,  1, -1, -1, -1, -1, -1,  1,  1,  1,  1, -1,  1,  1, -1, -1,
       -1,  1,  1, -1,  1,  1, -1,  1,  1,  1, -1, -1, -1,  1,  1,  1, -1,
       -1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, -1,  1])

另请参阅

19.5 使用分层合并进行聚类

问题

您想使用集群的层次结构对观测进行分组。

解决方案

使用聚类:

# Load libraries
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import AgglomerativeClustering

# Load data
iris = datasets.load_iris()
features = iris.data

# Standardize features
scaler = StandardScaler()
features_std = scaler.fit_transform(features)

# Create agglomerative clustering object
cluster = AgglomerativeClustering(n_clusters=3)

# Train model
model = cluster.fit(features_std)

讨论

凝聚式聚类是一种强大、灵活的分层聚类算法。在凝聚式聚类中,所有观测都开始作为自己的集群。接下来,满足一些条件的集群被合并。这个过程重复进行,直到达到某个结束点为止。在 scikit-learn 中,AgglomerativeClustering使用linkage参数来确定最小化合并策略:

  • 合并集群的方差(ward

  • 来自成对集群的观察之间的平均距离(average

  • 来自成对集群的观察之间的最大距离(complete

还有两个有用的参数需要知道。首先,affinity参数确定用于linkage的距离度量(minkowskieuclidean等)。其次,n_clusters设置聚类算法将尝试找到的聚类数。也就是说,集群被连续合并,直到只剩下n_clusters

与我们讨论过的其他聚类算法一样,我们可以使用labels_来查看每个观察被分配到的集群:

# Show cluster membership
model.labels_
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1,
       1, 1, 1, 1, 1, 1, 0, 0, 0, 2, 0, 2, 0, 2, 0, 2, 2, 0, 2, 0, 2, 0,
       2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 2, 0, 0, 2,
       2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

第二十章:PyTorch 中的张量

20.0 简介

就像 NumPy 是机器学习堆栈中数据操作的基础工具一样,PyTorch 是深度学习堆栈中处理张量的基础工具。在深入学习之前,我们应该熟悉 PyTorch 张量,并创建许多与 NumPy 中执行的操作类似的操作(见 第一章)。

虽然 PyTorch 只是多个深度学习库之一,但在学术界和工业界都非常流行。PyTorch 张量与 NumPy 数组非常相似。然而,它们还允许我们在 GPU(专门用于深度学习的硬件)上执行张量操作。在本章中,我们将熟悉 PyTorch 张量的基础知识和许多常见的低级操作。

20.1 创建张量

问题

您需要创建一个张量。

解决方案

使用 PyTorch 创建张量:

# Load library
import torch

# Create a vector as a row
tensor_row = torch.tensor([1, 2, 3])

# Create a vector as a column
tensor_column = torch.tensor(
    [
        [1],
        [2],
        [3]
    ]
)

讨论

PyTorch 中的主要数据结构是张量,在许多方面,张量与多维 NumPy 数组(见 第一章)完全相同。就像向量和数组一样,这些张量可以水平(即行)或垂直(即列)表示。

参见

20.2 从 NumPy 创建张量

问题

您需要从 NumPy 数组创建 PyTorch 张量。

解决方案

使用 PyTorch 的 from_numpy 函数:

# Import libraries
import numpy as np
import torch

# Create a NumPy array
vector_row = np.array([1, 2, 3])

# Create a tensor from a NumPy array
tensor_row = torch.from_numpy(vector_row)

讨论

正如我们所看到的,PyTorch 在语法上与 NumPy 非常相似。此外,它还允许我们轻松地将 NumPy 数组转换为可以在 GPU 和其他加速硬件上使用的 PyTorch 张量。在撰写本文时,PyTorch 文档中频繁提到 NumPy,并且 PyTorch 本身甚至提供了一种使 PyTorch 张量和 NumPy 数组可以共享内存以减少开销的方式。

参见

20.3 创建稀疏张量

问题

给定数据,其中非零值非常少,您希望以张量的方式高效表示它。

解决方案

使用 PyTorch 的 to_sparse 函数:

# Import libraries
import torch

# Create a tensor
tensor = torch.tensor(
[
[0, 0],
[0, 1],
[3, 0]
]
)

# Create a sparse tensor from a regular tensor
sparse_tensor = tensor.to_sparse()

讨论

稀疏张量是表示由大多数 0 组成的数据的内存高效方法。在 第一章 中,我们使用 scipy 创建了一个压缩稀疏行(CSR)矩阵,它不再是 NumPy 数组。

torch.Tensor 类允许我们使用同一个对象创建常规矩阵和稀疏矩阵。如果我们检查刚刚创建的两个张量的类型,我们可以看到它们实际上都属于同一类:

print(type(tensor))
print(type(sparse_tensor))
<class 'torch.Tensor'>
<class 'torch.Tensor'>

参见

20.4 在张量中选择元素

问题

我们需要选择张量的特定元素。

解决方案

使用类似于 NumPy 的索引和切片返回元素:

# Load library
import torch

# Create vector tensor
vector = torch.tensor([1, 2, 3, 4, 5, 6])

# Create matrix tensor
matrix = torch.tensor(
    [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]
    ]
)

# Select third element of vector
vector[2]
tensor(3)
# Select second row, second column
matrix[1,1]
tensor(5)

讨论

像 NumPy 数组和 Python 中的大多数内容一样,PyTorch 张量也是从零开始索引的。索引和切片也都受支持。一个关键区别是,对 PyTorch 张量进行索引以返回单个元素仍然会返回一个张量,而不是对象本身的值(该值将是整数或浮点数)。切片语法也与 NumPy 相同,并将以张量对象的形式返回:

# Select all elements of a vector
vector[:]
array([1, 2, 3, 4, 5, 6])
# Select everything up to and including the third element
vector[:3]
tensor([1, 2, 3])
# Select everything after the third element
vector[3:]
tensor([4, 5, 6])
# Select the last element
vector[-1]
tensor(6)
# Select the first two rows and all columns of a matrix
matrix[:2,:]
tensor([[1, 2, 3],
       [4, 5, 6]])
# Select all rows and the second column
matrix[:,1:2]
tensor([[2],
       [5],
       [8]])

一个关键区别是,PyTorch 张量在切片时不支持负步长。因此,尝试使用切片反转张量会产生错误:

# Reverse the vector
vector[::-1]
ValueError: step must be greater than zero

相反,如果我们希望反转张量,我们可以使用 flip 方法:

vector.flip(dims=(-1,))
tensor([6, 5, 4, 3, 2, 1])

参见

20.5 描述张量

问题

您想描述张量的形状、数据类型和格式以及它所使用的硬件。

解决方案

检查张量的 shapedtypelayoutdevice 属性:

# Load library
import torch

# Create a tensor
tensor = torch.tensor([[1,2,3], [1,2,3]])

# Get the shape of the tensor
tensor.shape
torch.Size([2, 3])
# Get the data type of items in the tensor
tensor.dtype
torch.int64
# Get the layout of the tensor
tensor.layout
torch.strided
# Get the device being used by the tensor
tensor.device
device(type='cpu')

讨论

PyTorch 张量提供了许多有用的属性,用于收集关于给定张量的信息,包括:

形状

返回张量的维度

Dtype

返回张量中对象的数据类型

布局

返回内存布局(最常见的是用于稠密张量的 strided

设备

返回张量存储的硬件(CPU/GPU)

再次,张量与数组的主要区别在于 设备 这样的属性,因为张量为我们提供了像 GPU 这样的硬件加速选项。

20.6 对元素应用操作

问题

您想对张量中的所有元素应用操作。

解决方案

利用 PyTorch 进行 广播

# Load library
import torch

# Create a tensor
tensor = torch.tensor([1, 2, 3])

# Broadcast an arithmetic operation to all elements in a tensor
tensor * 100
tensor([100, 200, 300])

讨论

PyTorch 中的基本操作将利用广播并行化,使用像 GPU 这样的加速硬件。这对于 Python 中支持的数学运算符(+、-、×、/)和 PyTorch 内置函数是真实的。与 NumPy 不同,PyTorch 不包括用于在张量上应用函数的 vectorize 方法。然而,PyTorch 配备了所有必要的数学工具,以分发和加速深度学习工作流程中所需的常规操作。

参见

20.7 查找最大值和最小值

问题

您需要在张量中找到最大值或最小值。

解决方案

使用 PyTorch 的 maxmin 方法:

# Load library
import torch

# Create a tensor
torch.tensor([1,2,3])

# Find the largest value
tensor.max()
tensor(3)
# Find the smallest value
tensor.min()
tensor(1)

讨论

张量的 maxmin 方法帮助我们找到该张量中的最大值或最小值。这些方法在多维张量上同样适用:

# Create a multidimensional tensor
tensor = torch.tensor([[1,2,3],[1,2,5]])

# Find the largest value
tensor.max()
tensor(5)

20.8 改变张量的形状

问题

您希望改变张量的形状(行数和列数)而不改变元素值。

解决方案

使用 PyTorch 的 reshape 方法:

# Load library
import torch

# Create 4x3 tensor
tensor = torch.tensor([[1, 2, 3],
                       [4, 5, 6],
                       [7, 8, 9],
                       [10, 11, 12]])

# Reshape tensor into 2x6 tensor
tensor.reshape(2, 6)
tensor([[ 1,  2,  3,  4,  5,  6],
        [ 7,  8,  9, 10, 11, 12]])

讨论

在深度学习领域中,操作张量的形状可能很常见,因为神经网络中的神经元通常需要具有非常特定形状的张量。由于给定神经网络中的神经元之间所需的张量形状可能会发生变化,因此了解深度学习中输入和输出的低级细节是很有好处的。

20.9 转置张量

问题

您需要转置一个张量。

解决方案

使用 mT 方法:

# Load library
import torch

# Create a two-dimensional tensor
tensor = torch.tensor([[[1,2,3]]])

# Transpose it
tensor.mT
tensor([[1],
        [2],
        [3]])

讨论

使用 PyTorch 进行转置与 NumPy 略有不同。用于 NumPy 数组的 T 方法仅支持二维张量,在 PyTorch 中对于其他形状的张量时,该方法在写作时已被弃用。用于转置批处理张量的 mT 方法更受欢迎,因为它适用于超过两个维度的张量。

除了使用 permute 方法之外,还可以使用 PyTorch 中的另一种方式来转置任意形状的张量:

tensor.permute(*torch.arange(tensor.ndim - 1, -1, -1))
tensor([[1],
        [2],
        [3]])

这种方法也适用于一维张量(其中转置张量的值与原始张量相同)。

20.10 张量展平

问题

您需要将张量转换为一维。

解决方案

使用 flatten 方法:

# Load library
import torch

# Create tensor
tensor = torch.tensor([[1, 2, 3],
                       [4, 5, 6],
                       [7, 8, 9]])

# Flatten tensor
tensor.flatten()
tensor([1, 2, 3, 4, 5, 6, 7, 8, 9])

讨论

张量展平是将多维张量降维为一维的一种有用技术。

20.11 计算点积

问题

您需要计算两个张量的点积。

解决方案

使用 dot 方法:

# Load library
import torch

# Create one tensor
tensor_1 = torch.tensor([1, 2, 3])

# Create another tensor
tensor_2 = torch.tensor([4, 5, 6])

# Calculate the dot product of the two tensors
tensor_1.dot(tensor_2)
tensor(32)

讨论

计算两个张量的点积是深度学习空间以及信息检索空间中常用的操作。您可能还记得本书中我们使用两个向量的点积执行基于余弦相似度的搜索。在 PyTorch 上使用 GPU(而不是在 CPU 上使用 NumPy 或 scikit-learn)执行此操作可以在信息检索问题上获得显著的性能优势。

参见

20.12 乘法张量

问题

您需要将两个张量相乘。

解决方案

使用基本的 Python 算术运算符:

# Load library
import torch

# Create one tensor
tensor_1 = torch.tensor([1, 2, 3])

# Create another tensor
tensor_2 = torch.tensor([4, 5, 6])

# Multiply the two tensors
tensor_1 * tensor_2
tensor([ 4, 10, 18])

讨论

PyTorch 支持基本算术运算符,如 ×、+、- 和 /。虽然在深度学习中,张量乘法可能是最常用的操作之一,但了解张量也可以进行加法、减法和除法是很有用的。

将一个张量加到另一个张量中:

tensor_1+tensor_2
tensor([5, 7, 9])

从一个张量中减去另一个张量:

tensor_1-tensor_2
tensor([-3, -3, -3])

将一个张量除以另一个张量:

tensor_1/tensor_2
tensor([0.2500, 0.4000, 0.5000])

第二十一章:神经网络

21.0 引言

基本神经网络的核心是单元(也称为节点神经元)。一个单元接收一个或多个输入,将每个输入乘以一个参数(也称为权重),将加权输入的值与一些偏置值(通常为 0)求和,然后将值馈送到激活函数中。然后,该输出被发送到神经网络中更深层的其他神经元(如果存在)。

神经网络可以被视为一系列连接的层,形成一个网络,将观察的特征值连接在一端,目标值(例如,观察的类)连接在另一端。前馈神经网络—也称为多层感知器—是任何实际设置中使用的最简单的人工神经网络。名称“前馈”来自于这样一个事实:观察的特征值被“前向”传递到网络中,每一层逐渐地转换特征值,目标是输出与目标值相同(或接近)。

具体而言,前馈神经网络包含三种类型的层。在神经网络的开始处是输入层,每个单元包含单个特征的观察值。例如,如果一个观察有 100 个特征,输入层有 100 个单元。在神经网络的末端是输出层,它将中间层(称为隐藏层)的输出转换为对任务有用的值。例如,如果我们的目标是二元分类,可以使用一个输出层,其中一个单元使用 sigmoid 函数将自己的输出缩放到 0 到 1 之间,表示预测的类概率。

在输入层和输出层之间是所谓的隐藏层。这些隐藏层逐步转换从输入层获取的特征值,以使其在被输出层处理后类似于目标类。具有许多隐藏层(例如,10、100、1,000)的神经网络被认为是“深”网络。训练深度神经网络的过程称为深度学习

神经网络通常是用高斯或正态均匀分布中的小随机值初始化所有参数。一旦观察到(或更频繁地说是一组称为批量的观察),通过网络,输出的值与观察到的真实值使用损失函数进行比较。这称为前向传播。接下来,算法通过网络“向后”传播,识别每个参数在预测值和真实值之间误差中的贡献,这个过程称为反向传播。在每个参数处,优化算法确定每个权重应该调整多少以改善输出。

神经网络通过重复进行前向传播和反向传播的过程来学习,每个观察结果都会多次(每次所有观察结果都通过网络称为epoch,训练通常包含多个 epoch),通过使用梯度下降过程来逐步优化参数值,从而优化给定输出的参数值。

在本章中,我们将使用上一章节中使用的同一 Python 库 PyTorch 来构建、训练和评估各种神经网络。PyTorch 是深度学习领域内流行的工具,因为其良好编写的 API 和直观表示低级张量操作的能力。PyTorch 的一个关键特性被称为autograd,它在前向传播和反向传播后自动计算和存储用于优化网络参数的梯度。

使用 PyTorch 代码创建的神经网络可以使用 CPU(例如,您的笔记本电脑)和 GPU(例如,专门的深度学习计算机)进行训练。在现实世界中使用真实数据时,通常需要使用 GPU 来训练神经网络,因为对于大数据和复杂网络,使用 GPU 比使用 CPU 快数个数量级。然而,本书中的所有神经网络都足够小和简单,可以仅使用 CPU 在几分钟内训练。只需注意,当我们有更大的网络和更多的训练数据时,使用 CPU 训练比使用 GPU 训练显著慢。

21.1 使用 PyTorch 的 Autograd

问题

您希望在前向传播和反向传播后使用 PyTorch 的自动微分功能来计算和存储梯度。

解决方案

使用requires_grad选项设置为True创建张量:

# Import libraries
import torch

# Create a torch tensor that requires gradients
t = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

# Perform a tensor operation simulating "forward propagation"
tensor_sum = t.sum()

# Perform back propagation
tensor_sum.backward()

# View the gradients
t.grad
tensor([1., 1., 1.])

讨论

自动微分是 PyTorch 的核心特性之一,也是其作为深度学习库受欢迎的重要因素之一。能够轻松计算、存储和可视化梯度使得 PyTorch 对于从头构建神经网络的研究人员和爱好者来说非常直观。

PyTorch 使用有向无环图(DAG)来记录在数据上执行的所有数据和计算操作。这非常有用,但也意味着我们在尝试应用需要梯度的 PyTorch 数据的操作时需要小心。在使用自动微分时,我们不能轻松地将张量转换为 NumPy 数组,也不能将其转换回来,而不会“破坏图”,这是用来描述不支持自动微分的操作的术语:

import torch

tensor = torch.tensor([1.0,2.0,3.0], requires_grad=True)
tensor.numpy()
RuntimeError: Can't call numpy() on Tensor that requires grad. Use
    tensor.detach().numpy() instead.

要将此张量转换为 NumPy 数组,我们需要在其上调用detach()方法,这将中断计算图,从而无法自动计算梯度。虽然这确实有用,但值得注意的是,分离张量将阻止 PyTorch 自动计算梯度。

参见

21.2 为神经网络预处理数据

问题

你想为神经网络预处理数据。

解决方案

使用 scikit-learn 的StandardScaler标准化每个特征:

# Load libraries
from sklearn import preprocessing
import numpy as np

# Create feature
features = np.array([[-100.1, 3240.1],
                     [-200.2, -234.1],
                     [5000.5, 150.1],
                     [6000.6, -125.1],
                     [9000.9, -673.1]])

# Create scaler
scaler = preprocessing.StandardScaler()

# Convert to a tensor
features_standardized_tensor = torch.from_numpy(features)

# Show features
features_standardized_tensor
tensor([[-100.1000, 3240.1000],
        [-200.2000, -234.1000],
        [5000.5000,  150.1000],
        [6000.6000, -125.1000],
        [9000.9000, -673.1000]], dtype=torch.float64)

讨论

尽管这个配方与配方 4.2 非常相似,但由于对神经网络的重要性,值得重复。通常情况下,神经网络的参数被初始化(即创建)为小的随机数。当特征值远远大于参数值时,神经网络的表现通常不佳。此外,由于观察的特征值在通过各个单元时被合并,因此重要的是所有特征具有相同的尺度。

出于这些原因,最佳实践是(虽然不总是必要;例如,当所有特征都是二进制时)标准化每个特征,使得特征值具有均值为 0 和标准差为 1。使用 scikit-learn 的StandardScaler可以轻松实现这一点。

然而,如果你需要在创建了requires_grad=True的张量之后执行此操作,则需要在 PyTorch 中原生地执行,以避免破坏图形。虽然通常会在开始训练网络之前标准化特征,但了解如何在 PyTorch 中完成相同的事情也是值得的:

# Load library
import torch

# Create features
torch_features = torch.tensor([[-100.1, 3240.1],
                               [-200.2, -234.1],
                               [5000.5, 150.1],
                               [6000.6, -125.1],
                               [9000.9, -673.1]], requires_grad=True)

# Compute the mean and standard deviation
mean = torch_features.mean(0, keepdim=True)
standard_deviation = torch_features.std(0, unbiased=False, keepdim=True)

# Standardize the features using the mean and standard deviation
torch_features_standardized = torch_features - mean
torch_features_standardized /= standard_deviation

# Show standardized features
torch_features_standardized
tensor([[-1.1254,  1.9643],
        [-1.1533, -0.5007],
        [ 0.2953, -0.2281],
        [ 0.5739, -0.4234],
        [ 1.4096, -0.8122]], grad_fn=<DivBackward0>)

21.3 设计一个神经网络

问题

你想设计一个神经网络。

解决方案

使用 PyTorch 的nn.Module类定义一个简单的神经网络架构:

# Import libraries
import torch
import torch.nn as nn

# Define a neural network
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.fc1 = nn.Linear(10, 16)
        self.fc2 = nn.Linear(16, 16)
        self.fc3 = nn.Linear(16, 1)

    def forward(self, x):
        x = nn.functional.relu(self.fc1(x))
        x = nn.functional.relu(self.fc2(x))
        x = nn.functional.sigmoid(self.fc3(x))
        return x

# Initialize the neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
loss_criterion = nn.BCELoss()
optimizer = torch.optim.RMSprop(network.parameters())

# Show the network
network
SimpleNeuralNet(
  (fc1): Linear(in_features=10, out_features=16, bias=True)
  (fc2): Linear(in_features=16, out_features=16, bias=True)
  (fc3): Linear(in_features=16, out_features=1, bias=True)
)

讨论

神经网络由多层单元组成。然而,关于层类型及其如何组合形成网络架构有很多不同的选择。虽然有一些常用的架构模式(我们将在本章中介绍),但选择正确的架构大多是一门艺术,并且是大量研究的主题。

要在 PyTorch 中构建一个前馈神经网络,我们需要就网络架构和训练过程做出许多选择。请记住,每个隐藏层中的每个单元:

  1. 接收若干个输入。

  2. 通过参数值加权每个输入。

  3. 将所有加权输入与一些偏差(通常为 0)相加。

  4. 最常见的是应用一些函数(称为激活函数)。

  5. 将输出发送到下一层的单元。

首先,对于隐藏层和输出层中的每一层,我们必须定义包括在该层中的单元数和激活函数。总体来说,一个层中有更多的单元,我们的网络就能够学习更复杂的模式。然而,更多的单元可能会使我们的网络过度拟合训练数据,从而损害测试数据的性能。

对于隐藏层,一个流行的激活函数是修正线性单元(ReLU):

f ( z ) = max ( 0 , z )

其中z是加权输入和偏差的总和。正如我们所见,如果z大于 0,则激活函数返回z;否则,函数返回 0。这个简单的激活函数具有许多理想的特性(其讨论超出了本书的范围),这使其成为神经网络中的热门选择。然而,我们应该注意,存在许多十几种激活函数。

第二步,我们需要定义网络中要使用的隐藏层的数量。更多的层允许网络学习更复杂的关系,但需要计算成本。

第三步,我们必须定义输出层激活函数(如果有的话)的结构。输出函数的性质通常由网络的目标确定。以下是一些常见的输出层模式:

二元分类

一个带有 sigmoid 激活函数的单元

多类别分类

k个单元(其中k是目标类别的数量)和 softmax 激活函数

回归

一个没有激活函数的单元

第四步,我们需要定义一个损失函数(衡量预测值与真实值匹配程度的函数);同样,这通常由问题类型决定:

二元分类

二元交叉熵

多类别分类

分类交叉熵

回归

均方误差

第五步,我们需要定义一个优化器,直观上可以将其视为我们在损失函数上“漫步”以找到产生最低误差的参数值的策略。常见的优化器选择包括随机梯度下降、带动量的随机梯度下降、均方根传播以及自适应矩估计(有关这些优化器的更多信息,请参见“参考文献”)。

第六步,我们可以选择一个或多个指标来评估性能,如准确性。

在我们的例子中,我们使用torch.nn.Module命名空间来组成一个简单的顺序神经网络,可以进行二元分类。在 PyTorch 中,标准的方法是创建一个子类,继承torch.nn.Module类,在__init__方法中实例化网络架构,并在类的forward方法中定义我们希望在每次前向传递中执行的数学操作。在 PyTorch 中定义网络的方法有很多种,虽然在本例中我们使用了函数式方法作为我们的激活函数(如nn.functional.relu),我们也可以将这些激活函数定义为层。如果我们希望将网络中的所有东西组成一层,我们可以使用Sequential类:

# Import libraries
import torch

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16, 1),
            torch.nn.Sigmoid()
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Instantiate and view the network
SimpleNeuralNet()
SimpleNeuralNet(
  (sequential): Sequential(
    (0): Linear(in_features=10, out_features=16, bias=True)
    (1): ReLU()
    (2): Linear(in_features=16, out_features=16, bias=True)
    (3): ReLU()
    (4): Linear(in_features=16, out_features=1, bias=True)
    (5): Sigmoid()
  )
)

在这两种情况下,网络本身都是一个两层神经网络(当计算层数时,不包括输入层,因为它没有任何要学习的参数),使用 PyTorch 的顺序模型进行定义。每一层都是“密集的”(也称为“全连接的”),意味着前一层中的所有单元都连接到下一层中的所有单元。

在第一个隐藏层中,我们设置 out_features=16,意味着该层包含 16 个单元。这些单元在我们类的 forward 方法中使用 ReLU 激活函数定义为 x = nn.functional.relu(self.fc1(x))。我们网络的第一层大小为 (10, 16),这告诉第一层期望从输入数据中每个观测值有 10 个特征值。这个网络设计用于二元分类,因此输出层只包含一个单元,使用 sigmoid 激活函数将输出约束在 0 到 1 之间(表示观测为类别 1 的概率)。

另请参阅

21.4 训练二元分类器

问题

您希望训练一个二元分类器神经网络。

解决方案

使用 PyTorch 构建一个前馈神经网络并对其进行训练:

# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
    n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16, 1),
            torch.nn.Sigmoid()
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Initialize neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Compile the model using torch 2.0's optimizer
network = torch.compile(network)

# Train neural network
epochs = 3
for epoch in range(epochs):
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
    print("Epoch:", epoch+1, "\tLoss:", loss.item())

# Evaluate neural network
with torch.no_grad():
    output = network(x_test)
    test_loss = criterion(output, y_test)
    test_accuracy = (output.round() == y_test).float().mean()
    print("Test Loss:", test_loss.item(), "\tTest Accuracy:",
        test_accuracy.item())
Epoch: 1 	Loss: 0.19006995856761932
Epoch: 2 	Loss: 0.14092367887496948
Epoch: 3 	Loss: 0.03935524448752403
Test Loss: 0.06877756118774414 	Test Accuracy: 0.9700000286102295

讨论

在 Recipe 21.3 中,我们讨论了如何使用 PyTorch 的顺序模型构建神经网络。在这个配方中,我们使用了来自 scikit-learn 的 make_classification 函数生成的具有 10 个特征和 1,000 个观测值的假分类数据集来训练该神经网络。

我们使用的神经网络与 Recipe 21.3 中的相同(详见该配方进行详细解释)。不同之处在于,我们只是创建了神经网络,而没有对其进行训练。

最后,我们使用 with torch.no_grad() 来评估网络。这表示我们不应计算在代码这一部分中进行的任何张量操作的梯度。由于我们只在模型训练过程中使用梯度,因此我们不希望为在其外部发生的操作(如预测或评估)存储新梯度。

epochs 变量定义了在训练数据时使用的 epochs 数量。batch_size 设置了在更新参数之前要通过网络传播的观测值数量。

然后,我们迭代多个 epochs,通过网络进行前向传递使用 forward 方法,然后反向传递以更新梯度。结果是一个经过训练的模型。

21.5 训练多类分类器

问题

您希望训练一个多类分类器神经网络。

解决方案

使用 PyTorch 构建一个具有 softmax 激活函数输出层的前馈神经网络:

# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

N_CLASSES=3
EPOCHS=3

# Create training and test sets
features, target = make_classification(n_classes=N_CLASSES, n_informative=9,
    n_redundant=0, n_features=10, n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.nn.functional.one_hot(torch.from_numpy(target_train).long(),
    num_classes=N_CLASSES).float()
x_test = torch.from_numpy(features_test).float()
y_test = torch.nn.functional.one_hot(torch.from_numpy(target_test).long(),
    num_classes=N_CLASSES).float()

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,3),
            torch.nn.Softmax()
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Initialize neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
criterion = nn.CrossEntropyLoss()
optimizer = RMSprop(network.parameters())

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Compile the model using torch 2.0's optimizer
network = torch.compile(network)

# Train neural network
for epoch in range(EPOCHS):
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
    print("Epoch:", epoch+1, "\tLoss:", loss.item())

# Evaluate neural network
with torch.no_grad():
    output = network(x_test)
    test_loss = criterion(output, y_test)
    test_accuracy = (output.round() == y_test).float().mean()
    print("Test Loss:", test_loss.item(), "\tTest Accuracy:",
        test_accuracy.item())
Epoch: 1 	Loss: 0.8022041916847229
Epoch: 2 	Loss: 0.775616466999054
Epoch: 3 	Loss: 0.7751263380050659
Test Loss: 0.8105319142341614 	Test Accuracy: 0.8199999928474426

讨论

在这个解决方案中,我们创建了一个类似于上一个示例中的二元分类器的神经网络,但是有一些显著的改变。在我们生成的分类数据中,我们设置了N_CLASSES=3。为了处理多类分类问题,我们还使用了nn.CrossEntropyLoss(),该函数期望目标是独热编码的。为了实现这一点,我们使用了torch.nn.functional.one_hot函数,最终得到一个独热编码的数组,其中1.的位置表示给定观察的类别:

# View target matrix
y_train
tensor([[1., 0., 0.],
        [0., 1., 0.],
        [1., 0., 0.],
        ...,
        [0., 1., 0.],
        [1., 0., 0.],
        [0., 0., 1.]])

因为这是一个多类分类问题,我们使用了大小为 3 的输出层(每个类别一个)并包含 softmax 激活函数。Softmax 激活函数将返回一个数组,其中的 3 个值相加为 1。这 3 个值表示一个观察结果属于每个类别的概率。

如本文提到的,我们使用了适合多类分类的损失函数,即分类交叉熵损失函数:nn.CrossEntropyLoss()

21.6 训练回归器

问题

您希望为回归训练一个神经网络。

解决方案

使用 PyTorch 构建一个只有一个输出单元且没有激活函数的前馈神经网络:

# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split

EPOCHS=5

# Create training and test sets
features, target = make_regression(n_features=10, n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1,1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1,1)

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,1),
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Initialize neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
criterion = nn.MSELoss()
optimizer = RMSprop(network.parameters())

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Compile the model using torch 2.0's optimizer
network = torch.compile(network)

# Train neural network
for epoch in range(EPOCHS):
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
    print("Epoch:", epoch+1, "\tLoss:", loss.item())

# Evaluate neural network
with torch.no_grad():
    output = network(x_test)
    test_loss = float(criterion(output, y_test))
    print("Test MSE:", test_loss)
Epoch: 1 	Loss: 10764.02734375
Epoch: 2 	Loss: 1356.510009765625
Epoch: 3 	Loss: 504.9664306640625
Epoch: 4 	Loss: 199.11314392089844
Epoch: 5 	Loss: 191.20834350585938
Test MSE: 162.24497985839844

讨论

完全可以创建一个神经网络来预测连续值,而不是类概率。在我们的二元分类器的情况下(Recipe 21.4),我们使用了一个具有单个单元和 sigmoid 激活函数的输出层,以生成观察是类 1 的概率。重要的是,sigmoid 激活函数将输出值限制在 0 到 1 之间。如果我们去除这种约束,即没有激活函数,我们允许输出为连续值。

此外,因为我们正在训练回归模型,我们应该使用适当的损失函数和评估指标,在我们的情况下是均方误差:

MSE = 1 n ∑ i=1 n (y ^ i -y i ) 2

其中n是观察数量;yi是我们试图预测的目标y的真实值,对于观察i;y ^i是模型对yi的预测值。

最后,由于我们使用了使用 scikit-learn 的make_regression生成的模拟数据,我们不需要对特征进行标准化。然而,需要注意的是,在几乎所有实际情况下,标准化是必要的。

21.7 进行预测

问题

您希望使用神经网络进行预测。

解决方案

使用 PyTorch 构建一个前馈神经网络,然后使用forward进行预测:

# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
    n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16, 1),
            torch.nn.Sigmoid()
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Initialize neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Compile the model using torch 2.0's optimizer
network = torch.compile(network)

# Train neural network
epochs = 3
for epoch in range(epochs):
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
    print("Epoch:", epoch+1, "\tLoss:", loss.item())

# Evaluate neural network
with torch.no_grad():
    predicted_class = network.forward(x_train).round()

predicted_class[0]
Epoch: 1 	Loss: 0.19006995856761932
Epoch: 2 	Loss: 0.14092367887496948
Epoch: 3 	Loss: 0.03935524448752403
tensor([1.])

讨论

在 PyTorch 中进行预测非常容易。一旦我们训练了神经网络,我们可以使用 forward 方法(已作为训练过程的一部分使用),该方法接受一组特征作为输入,并通过网络进行前向传递。在我们的解决方案中,神经网络被设置为二元分类,因此预测的输出是属于类 1 的概率。预测值接近 1 的观察结果高度可能属于类 1,而预测值接近 0 的观察结果高度可能属于类 0。因此,我们使用 round 方法将这些值转换为二元分类器中的 1 和 0。

21.8 可视化训练历史

问题

您希望找到神经网络损失和/或准确率得分的“甜蜜点”。

解决方案

使用 Matplotlib 可视化每个 epoch 中测试集和训练集的损失:

# Load libraries
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

import numpy as np
import matplotlib.pyplot as plt

# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
    n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16, 1),
            torch.nn.Sigmoid()
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Initialize neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Compile the model using torch 2.0's optimizer
network = torch.compile(network)

# Train neural network
epochs = 8
train_losses = []
test_losses = []
for epoch in range(epochs):
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

    with torch.no_grad():
        train_output = network(x_train)
        train_loss = criterion(output, target)
        train_losses.append(train_loss.item())

        test_output = network(x_test)
        test_loss = criterion(test_output, y_test)
        test_losses.append(test_loss.item())

# Visualize loss history
epochs = range(0, epochs)
plt.plot(epochs, train_losses, "r--")
plt.plot(epochs, test_losses, "b-")
plt.legend(["Training Loss", "Test Loss"])
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.show();

mpc2 21in01

讨论

当我们的神经网络是新的时,它的性能会比较差。随着神经网络在训练数据上的学习,模型在训练集和测试集上的错误通常会减少。然而,在某个点上,神经网络可能会开始“记忆”训练数据并过拟合。当这种情况发生时,训练错误可能会减少,而测试错误则开始增加。因此,在许多情况下,存在一个“甜蜜点”,在这个点上测试错误(我们主要关心的错误)达到最低点。这种效果可以在解决方案中看到,我们可视化了每个 epoch 的训练和测试损失。请注意,测试错误在第 6 个 epoch 左右达到最低点,此后训练损失趋于平稳,而测试损失开始增加。从这一点开始,模型开始过拟合。

21.9 使用权重正则化来减少过拟合

问题

您希望通过正则化网络的权重来减少过拟合。

解决方案

尝试对网络参数进行惩罚,也称为 weight regularization

# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
    n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16, 1),
            torch.nn.Sigmoid()
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Initialize neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(network.parameters(), lr=1e-4, weight_decay=1e-5)

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Compile the model using torch 2.0's optimizer
network = torch.compile(network)

# Train neural network
epochs = 100
for epoch in range(epochs):
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

# Evaluate neural network
with torch.no_grad():
    output = network(x_test)
    test_loss = criterion(output, y_test)
    test_accuracy = (output.round() == y_test).float().mean()
    print("Test Loss:", test_loss.item(), "\tTest Accuracy:",
        test_accuracy.item())
Test Loss: 0.4030887186527252 	Test Accuracy: 0.9599999785423279

讨论

抑制过拟合神经网络的一种策略是通过对神经网络的参数(即权重)施加惩罚,使它们趋向于较小的值,从而创建一个不容易过拟合的简单模型。这种方法称为权重正则化或权重衰减。具体而言,在权重正则化中,将惩罚项添加到损失函数中,如 L2 范数。

在 PyTorch 中,我们可以通过在优化器中包含 weight_decay=1e-5 来添加权重正则化,在这里正则化发生。在这个例子中,1e-5 决定了我们对较高参数值施加的惩罚程度。大于 0 的数值表示在 PyTorch 中使用 L2 正则化。

21.10 使用早停策略来减少过拟合

问题

您希望通过在训练和测试得分发散时停止训练来减少过拟合。

解决方案

使用 PyTorch Lightning 实现一种名为 early stopping 的策略:

# Import libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
import lightning as pl
from lightning.pytorch.callbacks.early_stopping import EarlyStopping
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
    n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16, 1),
            torch.nn.Sigmoid()
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

class LightningNetwork(pl.LightningModule):
    def __init__(self, network):
        super().__init__()
        self.network = network
        self.criterion = nn.BCELoss()
        self.metric = nn.functional.binary_cross_entropy

    def training_step(self, batch, batch_idx):
        # training_step defines the train loop.
        data, target = batch
        output = self.network(data)
        loss = self.criterion(output, target)
        self.log("val_loss", loss)
        return loss

    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=1e-3)

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Initialize neural network
network = LightningNetwork(SimpleNeuralNet())

# Train network
trainer = pl.Trainer(callbacks=[EarlyStopping(monitor="val_loss", mode="min",
    patience=3)], max_epochs=1000)
trainer.fit(model=network, train_dataloaders=train_loader)
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs

  | Name      | Type            | Params
----------------------------------------------
0 | network   | SimpleNeuralNet | 465
1 | criterion | BCELoss         | 0
----------------------------------------------
465       Trainable params
0         Non-trainable params
465       Total params
0.002     Total estimated model params size (MB)
/usr/local/lib/python3.10/site-packages/lightning/pytorch/trainer/
    connectors/data_connector.py:224: PossibleUserWarning:
    The dataloader, train_dataloader, does not have many workers which
    may be a bottleneck. Consider increasing the value of the `num_workers`
    argument (try 7 which is the number of cpus on this machine)
    in the `DataLoader` init to improve performance.
  rank_zero_warn(
/usr/local/lib/python3.10/site-packages/lightning/pytorch/trainer/
    trainer.py:1609: PossibleUserWarning: The number of training batches (9)
    is smaller than the logging interval Trainer(log_every_n_steps=50).
    Set a lower value for log_every_n_steps if you want to see logs
    for the training epoch.
  rank_zero_warn(
Epoch 23: 100%|███████████████| 9/9 [00:00<00:00, 59.29it/s, loss=0.147, v_num=5]

讨论

正如我们在 Recipe 21.8 中讨论的,通常在最初的几个训练 epoch 中,训练和测试错误都会减少,但是在某个时候,网络将开始“记忆”训练数据,导致训练错误继续减少,而测试错误开始增加。因此,对抗过拟合最常见且非常有效的方法之一是监控训练过程,并在测试错误开始增加时停止训练。这种策略称为早期停止

在 PyTorch 中,我们可以将早期停止作为回调函数来实现。回调函数是在训练过程的特定阶段应用的函数,例如在每个 epoch 结束时。然而,PyTorch 本身并没有为您定义一个早期停止的类,因此在这里我们使用流行的库lightning(即 PyTorch Lightning)来使用现成的早期停止功能。PyTorch Lightning 是一个为 PyTorch 提供大量有用功能的高级库。在我们的解决方案中,我们包括了 PyTorch Lightning 的EarlyStopping(monitor="val_loss", mode="min", patience=3),以定义我们希望在每个 epoch 监控测试(验证)损失,并且如果经过三个 epoch(默认值)后测试损失没有改善,则中断训练。

如果我们没有包含EarlyStopping回调,模型将在完整的 1,000 个最大 epoch 中训练而不会自行停止:

# Train network
trainer = pl.Trainer(max_epochs=1000)
trainer.fit(model=network, train_dataloaders=train_loader)
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs

  | Name      | Type            | Params
----------------------------------------------
0 | network   | SimpleNeuralNet | 465
1 | criterion | BCELoss         | 0
----------------------------------------------
465       Trainable params
0         Non-trainable params
465       Total params
0.002     Total estimated model params size (MB)
Epoch 999: 100%|████████████| 9/9 [00:01<00:00,  7.95it/s, loss=0.00188, v_num=6]
`Trainer.fit` stopped: `max_epochs=1000` reached.
Epoch 999: 100%|████████████| 9/9 [00:01<00:00,  7.80it/s, loss=0.00188, v_num=6]

21.11 使用 Dropout 减少过拟合

问题

您希望减少过拟合。

解决方案

使用 dropout 在您的网络架构中引入噪声:

# Load libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
    n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16, 1),
            torch.nn.Dropout(0.1), # Drop 10% of neurons
            torch.nn.Sigmoid(),
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Initialize neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Compile the model using torch 2.0's optimizer
network = torch.compile(network)

# Train neural network
epochs = 3
for epoch in range(epochs):
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
    print("Epoch:", epoch+1, "\tLoss:", loss.item())

# Evaluate neural network
with torch.no_grad():
    output = network(x_test)
    test_loss = criterion(output, y_test)
    test_accuracy = (output.round() == y_test).float().mean()
    print("Test Loss:", test_loss.item(), "\tTest Accuracy:",
        test_accuracy.item())
Epoch: 1 	Loss: 0.18791493773460388
Epoch: 2 	Loss: 0.17331615090370178
Epoch: 3 	Loss: 0.1384529024362564
Test Loss: 0.12702330946922302 	Test Accuracy: 0.9100000262260437

讨论

Dropout 是一种相对常见的正则化较小神经网络的方法。在 dropout 中,每次为训练创建一批观察时,一个或多个层中的单位比例被乘以零(即被删除)。在此设置中,每个批次都在相同的网络上训练(例如相同的参数),但是每个批次都面对稍微不同版本的该网络的架构

Dropout 被认为是有效的,因为通过在每个批次中不断随机删除单位,它强制单位学习能够在各种网络架构下执行的参数值。也就是说,它们学会了对其他隐藏单元中的干扰(即噪声)具有鲁棒性,从而防止网络简单地记住训练数据。

可以将 dropout 添加到隐藏层和输入层中。当输入层被删除时,其特征值在该批次中不会被引入网络中。

在 PyTorch 中,我们可以通过在网络架构中添加一个nn.Dropout层来实现 dropout。每个nn.Dropout层将在每个批次中删除前一层中用户定义的超参数单位。

21.12 保存模型训练进度

问题

鉴于神经网络训练时间较长,您希望在训练过程中保存进度以防中断。

解决方案

使用 torch.save 函数在每个 epoch 后保存模型:

# Load libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
    n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16, 1),
            torch.nn.Dropout(0.1), # Drop 10% of neurons
            torch.nn.Sigmoid(),
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Initialize neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Compile the model using torch 2.0's optimizer
network = torch.compile(network)

# Train neural network
epochs = 5
for epoch in range(epochs):
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        # Save the model at the end of every epoch
        torch.save(
            {
                'epoch': epoch,
                'model_state_dict': network.state_dict(),
                'optimizer_state_dict': optimizer.state_dict(),
                'loss': loss,
            },
            "model.pt"
        )
    print("Epoch:", epoch+1, "\tLoss:", loss.item())
Epoch: 1 	Loss: 0.18791493773460388
Epoch: 2 	Loss: 0.17331615090370178
Epoch: 3 	Loss: 0.1384529024362564
Epoch: 4 	Loss: 0.1435958743095398
Epoch: 5 	Loss: 0.17967987060546875

讨论

在现实世界中,神经网络通常需要训练几个小时甚至几天。在此期间,可能发生很多问题:计算机断电、服务器崩溃,或者不体贴的研究生关掉你的笔记本电脑。

我们可以使用 torch.save 函数来缓解这个问题,通过在每个 epoch 后保存模型。具体来说,在每个 epoch 后,我们将模型保存到位置 model.pt,这是 torch.save 函数的第二个参数。如果我们只包含一个文件名(例如 model.pt),那么该文件将在每个 epoch 都被最新的模型覆盖。

正如你可以想象的,我们可以引入额外的逻辑,在每几个 epochs 保存模型,仅在损失减少时保存模型等。我们甚至可以将这种方法与 PyTorch Lightning 的提前停止方法结合起来,以确保无论训练在哪个 epoch 结束,我们都能保存模型。

21.13 调整神经网络

问题

您想自动选择神经网络的最佳超参数。

解决方案

使用 PyTorch 的 ray 调优库:

# Load libraries
from functools import partial
import numpy as np
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.optim import RMSprop
from torch.utils.data import random_split, DataLoader, TensorDataset
from ray import tune
from ray.tune import CLIReporter
from ray.tune.schedulers import ASHAScheduler
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
    n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)

# Define a neural network using `Sequential`
class SimpleNeuralNet(nn.Module):
    def __init__(self, layer_size_1=10, layer_size_2=10):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, layer_size_1),
            torch.nn.ReLU(),
            torch.nn.Linear(layer_size_1, layer_size_2),
            torch.nn.ReLU(),
            torch.nn.Linear(layer_size_2, 1),
            torch.nn.Sigmoid()
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

config = {
    "layer_size_1": tune.sample_from(lambda _: 2 ** np.random.randint(2, 9)),
    "layer_size_2": tune.sample_from(lambda _: 2 ** np.random.randint(2, 9)),
    "lr": tune.loguniform(1e-4, 1e-1),
}

scheduler = ASHAScheduler(
    metric="loss",
    mode="min",
    max_t=1000,
    grace_period=1,
    reduction_factor=2
)

reporter = CLIReporter(
    parameter_columns=["layer_size_1", "layer_size_2", "lr"],
    metric_columns=["loss"]
)

# # Train neural network
def train_model(config, epochs=3):
    network = SimpleNeuralNet(config["layer_size_1"], config["layer_size_2"])

    criterion = nn.BCELoss()
    optimizer = optim.SGD(network.parameters(), lr=config["lr"], momentum=0.9)

    train_data = TensorDataset(x_train, y_train)
    train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

    # Compile the model using torch 2.0's optimizer
    network = torch.compile(network)

    for epoch in range(epochs):
        for batch_idx, (data, target) in enumerate(train_loader):
            optimizer.zero_grad()
            output = network(data)
            loss = criterion(output, target)
            loss.backward()
            optimizer.step()
            tune.report(loss=(loss.item()))

result = tune.run(
    train_model,
    resources_per_trial={"cpu": 2},
    config=config,
    num_samples=1,
    scheduler=scheduler,
    progress_reporter=reporter
)

best_trial = result.get_best_trial("loss", "min", "last")
print("Best trial config: {}".format(best_trial.config))
print("Best trial final validation loss: {}".format(
    best_trial.last_result["loss"]))

best_trained_model = SimpleNeuralNet(best_trial.config["layer_size_1"],
    best_trial.config["layer_size_2"])
== Status ==
Current time: 2023-03-05 23:31:33 (running for 00:00:00.07)
Memory usage on this node: 1.7/15.6 GiB
Using AsyncHyperBand: num_stopped=0
Bracket: Iter 512.000: None | Iter 256.000: None | Iter 128.000: None |
    Iter 64.000: None | Iter 32.000: None | Iter 16.000: None |
    Iter 8.000: None | Iter 4.000: None | Iter 2.000: None |
    Iter 1.000: None
Resources requested: 2.0/7 CPUs, 0/0 GPUs, 0.0/8.95 GiB heap,
    0.0/4.48 GiB objects
Result logdir: /root/ray_results/train_model_2023-03-05_23-31-33
Number of trials: 1/1 (1 RUNNING)
...

讨论

在第 12.1 和 12.2 节中,我们介绍了使用 scikit-learn 的模型选择技术来识别 scikit-learn 模型的最佳超参数。尽管一般来说,scikit-learn 的方法也可以应用于神经网络,但 ray 调优库提供了一个复杂的 API,允许您在 CPU 和 GPU 上调度实验。

模型的超参数很重要,应该仔细选择。然而,运行实验来选择超参数可能会成本高昂且耗时。因此,神经网络的自动超参数调整并非万能药,但在特定情况下是一个有用的工具。

在我们的解决方案中,我们对不同的参数进行了搜索,包括层大小和优化器的学习率。best_trial.config 显示了在我们的 ray 调优配置中导致最低损失和最佳实验结果的参数。

21.14 可视化神经网络

问题

您想快速可视化神经网络的架构。

解决方案

使用 torch_vizmake_dot 函数:

# Load libraries
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
from torch.optim import RMSprop
from torchviz import make_dot
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# Create training and test sets
features, target = make_classification(n_classes=2, n_features=10,
    n_samples=1000)
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.1, random_state=1)

# Set random seed
torch.manual_seed(0)
np.random.seed(0)

# Convert data to PyTorch tensors
x_train = torch.from_numpy(features_train).float()
y_train = torch.from_numpy(target_train).float().view(-1, 1)
x_test = torch.from_numpy(features_test).float()
y_test = torch.from_numpy(target_test).float().view(-1, 1)

# Define a neural network using Sequential
class SimpleNeuralNet(nn.Module):
    def __init__(self):
        super(SimpleNeuralNet, self).__init__()
        self.sequential = torch.nn.Sequential(
            torch.nn.Linear(10, 16),
            torch.nn.ReLU(),
            torch.nn.Linear(16,16),
            torch.nn.ReLU(),
            torch.nn.Linear(16, 1),
            torch.nn.Sigmoid()
        )

    def forward(self, x):
        x = self.sequential(x)
        return x

# Initialize neural network
network = SimpleNeuralNet()

# Define loss function, optimizer
criterion = nn.BCELoss()
optimizer = RMSprop(network.parameters())

# Define data loader
train_data = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_data, batch_size=100, shuffle=True)

# Compile the model using torch 2.0's optimizer
network = torch.compile(network)

# Train neural network
epochs = 3
for epoch in range(epochs):
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

make_dot(output.detach(), params=dict(
    list(
        network.named_parameters()
        )
      )
    ).render(
        "simple_neural_network",
        format="png"
)
'simple_neural_network.png'

如果我们打开保存到我们机器上的图像,我们可以看到以下内容:

mpc2 21in02

讨论

torchviz 库提供了简单的实用函数,可以快速可视化我们的神经网络并将其输出为图像。