Python 机器学习秘籍第二版(二)
原文:
annas-archive.org/md5/343c5e6c97737f77853e89eacb95df75译者:飞龙
第四章:处理数值数据
4.0 引言
定量数据是某物的测量——无论是班级规模、月销售额还是学生分数。表示这些数量的自然方式是数值化(例如,29 名学生、销售额为 529,392 美元)。在本章中,我们将介绍多种策略,将原始数值数据转换为专门用于机器学习算法的特征。
4.1 重新调整特征
问题
您需要将数值特征的值重新缩放到两个值之间。
解决方案
使用 scikit-learn 的MinMaxScaler来重新调整特征数组:
# Load libraries
import numpy as np
from sklearn import preprocessing
# Create feature
feature = np.array([[-500.5],
[-100.1],
[0],
[100.1],
[900.9]])
# Create scaler
minmax_scale = preprocessing.MinMaxScaler(feature_range=(0, 1))
# Scale feature
scaled_feature = minmax_scale.fit_transform(feature)
# Show feature
scaled_feature
array([[ 0\. ],
[ 0.28571429],
[ 0.35714286],
[ 0.42857143],
[ 1\. ]])
讨论
重新缩放 是机器学习中常见的预处理任务。本书后面描述的许多算法将假定所有特征在同一尺度上,通常是 0 到 1 或-1 到 1。有许多重新缩放技术,但最简单的之一称为最小-最大缩放。最小-最大缩放使用特征的最小值和最大值将值重新缩放到一个范围内。具体来说,最小-最大缩放计算:
x i ' = x i -min(x) max(x)-min(x)
其中x是特征向量,xi是特征x的单个元素,x i '是重新调整的元素。在我们的例子中,我们可以从输出的数组中看到,特征已成功重新调整为 0 到 1 之间:
array([[ 0\. ],
[ 0.28571429],
[ 0.35714286],
[ 0.42857143],
[ 1\. ]])
scikit-learn 的MinMaxScaler提供了两种重新调整特征的选项。一种选项是使用fit来计算特征的最小值和最大值,然后使用transform来重新调整特征。第二个选项是使用fit_transform来同时执行这两个操作。这两个选项在数学上没有区别,但有时将操作分开会有实际的好处,因为这样可以将相同的转换应用于不同的数据集。
参见
4.2 标准化特征
问题
您希望将一个特征转换为具有均值为 0 和标准差为 1。
解决方案
scikit-learn 的StandardScaler执行这两个转换:
# Load libraries
import numpy as np
from sklearn import preprocessing
# Create feature
x = np.array([[-1000.1],
[-200.2],
[500.5],
[600.6],
[9000.9]])
# Create scaler
scaler = preprocessing.StandardScaler()
# Transform the feature
standardized = scaler.fit_transform(x)
# Show feature
standardized
array([[-0.76058269],
[-0.54177196],
[-0.35009716],
[-0.32271504],
[ 1.97516685]])
讨论
对于问题 4.1 中讨论的最小-最大缩放的常见替代方案是将特征重新缩放为近似标准正态分布。为了实现这一目标,我们使用标准化来转换数据,使其均值x ¯为 0,标准差σ为 1。具体来说,特征中的每个元素都被转换,以便:
x i ' = x i -x ¯ σ
其中 x i ' 是 x i 的标准化形式。转换后的特征表示原始值与特征均值之间的标准偏差数(在统计学中也称为 z-score)。
标准化是机器学习预处理中常见的缩放方法,在我的经验中,它比最小-最大缩放更常用。但这取决于学习算法。例如,主成分分析通常在使用标准化时效果更好,而对于神经网络,则通常建议使用最小-最大缩放(这两种算法稍后在本书中讨论)。作为一个一般规则,我建议除非有特定原因使用其他方法,否则默认使用标准化。
我们可以通过查看解决方案输出的平均值和标准偏差来看到标准化的效果:
# Print mean and standard deviation
print("Mean:", round(standardized.mean()))
print("Standard deviation:", standardized.std())
Mean: 0.0
Standard deviation: 1.0
如果我们的数据存在显著的异常值,它可能通过影响特征的均值和方差而对我们的标准化产生负面影响。在这种情况下,通常可以通过使用中位数和四分位距来重新调整特征,从而提供帮助。在 scikit-learn 中,我们使用 RobustScaler 方法来实现这一点:
# Create scaler
robust_scaler = preprocessing.RobustScaler()
# Transform feature
robust_scaler.fit_transform(x)
array([[ -1.87387612],
[ -0.875 ],
[ 0\. ],
[ 0.125 ],
[ 10.61488511]])
4.3 规范化观测值
问题
您希望将观测值的特征值重新调整为单位范数(总长度为 1)。
解决方案
使用带有 norm 参数的 Normalizer:
# Load libraries
import numpy as np
from sklearn.preprocessing import Normalizer
# Create feature matrix
features = np.array([[0.5, 0.5],
[1.1, 3.4],
[1.5, 20.2],
[1.63, 34.4],
[10.9, 3.3]])
# Create normalizer
normalizer = Normalizer(norm="l2")
# Transform feature matrix
normalizer.transform(features)
array([[ 0.70710678, 0.70710678],
[ 0.30782029, 0.95144452],
[ 0.07405353, 0.99725427],
[ 0.04733062, 0.99887928],
[ 0.95709822, 0.28976368]])
讨论
许多重新调整方法(例如,最小-最大缩放和标准化)作用于特征,但我们也可以跨个体观测值进行重新调整。Normalizer 将单个观测值上的值重新调整为单位范数(它们长度的总和为 1)。当存在许多等效特征时(例如,在文本分类中,每个单词或 n-word 组合都是一个特征时),通常会使用这种重新调整。
Normalizer 提供三种范数选项,其中欧几里德范数(通常称为 L2)是默认参数:
x 2 = x 1 2 + x 2 2 + ⋯ + x n 2
其中 x 是一个单独的观测值,xn 是该观测值在第 n 个特征上的值。
# Transform feature matrix
features_l2_norm = Normalizer(norm="l2").transform(features)
# Show feature matrix
features_l2_norm
array([[ 0.70710678, 0.70710678],
[ 0.30782029, 0.95144452],
[ 0.07405353, 0.99725427],
[ 0.04733062, 0.99887928],
[ 0.95709822, 0.28976368]])
或者,我们可以指定曼哈顿范数(L1):
x 1 = ∑ i=1 n x i .
# Transform feature matrix
features_l1_norm = Normalizer(norm="l1").transform(features)
# Show feature matrix
features_l1_norm
array([[ 0.5 , 0.5 ],
[ 0.24444444, 0.75555556],
[ 0.06912442, 0.93087558],
[ 0.04524008, 0.95475992],
[ 0.76760563, 0.23239437]])
直观上,L2 范数可以被视为鸟在纽约两点之间的距离(即直线距离),而 L1 范数可以被视为在街道上行走的人的距离(向北走一块,向东走一块,向北走一块,向东走一块,等等),这就是为什么它被称为“曼哈顿范数”或“出租车范数”的原因。
在实际应用中,注意到 norm="l1" 将重新调整观测值的值,使其总和为 1,这在某些情况下是一种可取的质量:
# Print sum
print("Sum of the first observation\'s values:",
features_l1_norm[0, 0] + features_l1_norm[0, 1])
Sum of the first observation's values: 1.0
4.4 生成多项式和交互特征
问题
您希望创建多项式和交互特征。
解决方案
即使有些人选择手动创建多项式和交互特征,scikit-learn 提供了一个内置方法:
# Load libraries
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
# Create feature matrix
features = np.array([[2, 3],
[2, 3],
[2, 3]])
# Create PolynomialFeatures object
polynomial_interaction = PolynomialFeatures(degree=2, include_bias=False)
# Create polynomial features
polynomial_interaction.fit_transform(features)
array([[ 2., 3., 4., 6., 9.],
[ 2., 3., 4., 6., 9.],
[ 2., 3., 4., 6., 9.]])
参数degree确定多项式的最大次数。例如,degree=2将创建被提升到二次幂的新特征:
x 1 , x 2 , x 1 2 , x 1 2 , x 2 2
而degree=3将创建被提升到二次和三次幂的新特征:
x 1 , x 2 , x 1 2 , x 2 2 , x 1 3 , x 2 3 , x 1 2 , x 1 3 , x 2 3
此外,默认情况下,PolynomialFeatures包括交互特征:
x 1 x 2
我们可以通过将interaction_only设置为True来限制仅创建交互特征:
interaction = PolynomialFeatures(degree=2,
interaction_only=True, include_bias=False)
interaction.fit_transform(features)
array([[ 2., 3., 6.],
[ 2., 3., 6.],
[ 2., 3., 6.]])
讨论
当我们希望包括特征与目标之间存在非线性关系时,通常会创建多项式特征。例如,我们可能怀疑年龄对患重大医疗状况的概率的影响并非随时间恒定,而是随年龄增加而增加。我们可以通过生成该特征的高阶形式(x2,x3等)来编码这种非恒定效果。
此外,我们经常遇到一种情况,即一个特征的效果取决于另一个特征。一个简单的例子是,如果我们试图预测我们的咖啡是否甜,我们有两个特征:(1)咖啡是否被搅拌,以及(2)是否添加了糖。单独来看,每个特征都不能预测咖啡的甜度,但它们的效果组合起来却可以。也就是说,只有当咖啡既加了糖又被搅拌时,咖啡才会变甜。每个特征对目标(甜度)的影响取决于彼此之间的关系。我们可以通过包含一个交互特征,即两个个体特征的乘积来编码这种关系。
4.5 特征转换
问题
你希望对一个或多个特征进行自定义转换。
解决方案
在 scikit-learn 中,使用FunctionTransformer将一个函数应用到一组特征上:
# Load libraries
import numpy as np
from sklearn.preprocessing import FunctionTransformer
# Create feature matrix
features = np.array([[2, 3],
[2, 3],
[2, 3]])
# Define a simple function
def add_ten(x: int) -> int:
return x + 10
# Create transformer
ten_transformer = FunctionTransformer(add_ten)
# Transform feature matrix
ten_transformer.transform(features)
array([[12, 13],
[12, 13],
[12, 13]])
我们可以使用apply在 pandas 中创建相同的转换:
# Load library
import pandas as pd
# Create DataFrame
df = pd.DataFrame(features, columns=["feature_1", "feature_2"])
# Apply function
df.apply(add_ten)
| feature_1 | feature_2 | |
|---|---|---|
| 0 | 12 | 13 |
| 1 | 12 | 13 |
| 2 | 12 | 13 |
讨论
通常希望对一个或多个特征进行一些自定义转换。例如,我们可能想创建一个特征,其值是另一个特征的自然对数。我们可以通过创建一个函数,然后使用 scikit-learn 的FunctionTransformer或 pandas 的apply将其映射到特征来实现这一点。在解决方案中,我们创建了一个非常简单的函数add_ten,它为每个输入加了 10,但我们完全可以定义一个复杂得多的函数。
4.6 检测异常值
问题
你希望识别极端观察结果。
解决方案
检测异常值很遗憾更像是一种艺术而不是一种科学。然而,一种常见的方法是假设数据呈正态分布,并基于该假设在数据周围“画”一个椭圆,将椭圆内的任何观察结果归类为内围值(标记为1),将椭圆外的任何观察结果归类为异常值(标记为-1):
# Load libraries
import numpy as np
from sklearn.covariance import EllipticEnvelope
from sklearn.datasets import make_blobs
# Create simulated data
features, _ = make_blobs(n_samples = 10,
n_features = 2,
centers = 1,
random_state = 1)
# Replace the first observation's values with extreme values
features[0,0] = 10000
features[0,1] = 10000
# Create detector
outlier_detector = EllipticEnvelope(contamination=.1)
# Fit detector
outlier_detector.fit(features)
# Predict outliers
outlier_detector.predict(features)
array([-1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
在这些数组中,值为-1 表示异常值,而值为 1 表示内围值。这种方法的一个主要局限性是需要指定一个contamination参数,它是异常值观察值的比例,这是我们不知道的值。将contamination视为我们对数据清洁程度的估计。如果我们预计数据中有很少的异常值,我们可以将contamination设置为较小的值。但是,如果我们认为数据可能有异常值,我们可以将其设置为较高的值。
我们可以不将观察结果作为一个整体来看待,而是可以查看单个特征,并使用四分位距(IQR)来识别这些特征中的极端值:
# Create one feature
feature = features[:,0]
# Create a function to return index of outliers
def indicies_of_outliers(x: int) -> np.array(int):
q1, q3 = np.percentile(x, [25, 75])
iqr = q3 - q1
lower_bound = q1 - (iqr * 1.5)
upper_bound = q3 + (iqr * 1.5)
return np.where((x > upper_bound) | (x < lower_bound))
# Run function
indicies_of_outliers(feature)
(array([0]),)
IQR 是一组数据的第一和第三四分位数之间的差异。您可以将 IQR 视为数据的主要集中区域的扩展,而异常值是远离数据主要集中区域的观察结果。异常值通常定义为第一四分位数的 1.5 倍 IQR 小于或第三四分位数的 1.5 倍 IQR 大于的任何值。
讨论
没有单一的最佳技术来检测异常值。相反,我们有一系列技术,各有优缺点。我们最好的策略通常是尝试多种技术(例如,EllipticEnvelope和基于 IQR 的检测)并综合查看结果。
如果可能的话,我们应该查看我们检测到的异常值,并尝试理解它们。例如,如果我们有一个房屋数据集,其中一个特征是房间数量,那么房间数量为 100 的异常值是否真的是一座房子,还是实际上是一个被错误分类的酒店?
参见
4.7 处理异常值
问题
您的数据中存在异常值,您希望识别并减少其对数据分布的影响。
解决方案
通常我们可以采用三种策略来处理异常值。首先,我们可以放弃它们:
# Load library
import pandas as pd
# Create DataFrame
houses = pd.DataFrame()
houses['Price'] = [534433, 392333, 293222, 4322032]
houses['Bathrooms'] = [2, 3.5, 2, 116]
houses['Square_Feet'] = [1500, 2500, 1500, 48000]
# Filter observations
houses[houses['Bathrooms'] < 20]
| 价格 | 浴室 | 平方英尺 | |
|---|---|---|---|
| 0 | 534433 | 2.0 | 1500 |
| 1 | 392333 | 3.5 | 2500 |
| 2 | 293222 | 2.0 | 1500 |
其次,我们可以将它们标记为异常值,并将“异常值”作为特征包含在内:
# Load library
import numpy as np
# Create feature based on boolean condition
houses["Outlier"] = np.where(houses["Bathrooms"] < 20, 0, 1)
# Show data
houses
| 价格 | 浴室 | 平方英尺 | 异常值 | |
|---|---|---|---|---|
| 0 | 534433 | 2.0 | 1500 | 0 |
| 1 | 392333 | 3.5 | 2500 | 0 |
| 2 | 293222 | 2.0 | 1500 | 0 |
| 3 | 4322032 | 116.0 | 48000 | 1 |
最后,我们可以转换特征以减轻异常值的影响:
# Log feature
houses["Log_Of_Square_Feet"] = [np.log(x) for x in houses["Square_Feet"]]
# Show data
houses
| 价格 | 浴室 | 平方英尺 | 异常值 | 平方英尺的对数 | |
|---|---|---|---|---|---|
| 0 | 534433 | 2.0 | 1500 | 0 | 7.313220 |
| 1 | 392333 | 3.5 | 2500 | 0 | 7.824046 |
| 2 | 293222 | 2.0 | 1500 | 0 | 7.313220 |
| 3 | 4322032 | 116.0 | 48000 | 1 | 10.778956 |
讨论
类似于检测异常值,处理它们没有硬性规则。我们处理它们应该基于两个方面。首先,我们应该考虑它们为何成为异常值。如果我们认为它们是数据中的错误,比如来自损坏传感器或错误编码的值,那么我们可能会删除该观测值或将异常值替换为NaN,因为我们不能信任这些值。然而,如果我们认为异常值是真实的极端值(例如,一个有 200 个浴室的豪宅),那么将它们标记为异常值或转换它们的值更为合适。
其次,我们处理异常值的方式应该基于我们在机器学习中的目标。例如,如果我们想根据房屋特征预测房价,我们可能合理地假设拥有超过 100 个浴室的豪宅的价格受到不同动态的驱动,而不是普通家庭住宅。此外,如果我们正在训练一个在线住房贷款网站应用程序的模型,我们可能会假设我们的潜在用户不包括寻求购买豪宅的亿万富翁。
那么如果我们有异常值应该怎么办?考虑它们为何成为异常值,设定数据的最终目标,最重要的是记住,不处理异常值本身也是一种带有影响的决策。
另外一点:如果存在异常值,标准化可能不合适,因为异常值可能会严重影响均值和方差。在这种情况下,应该使用对异常值更具鲁棒性的重新缩放方法,比如RobustScaler。
参见
4.8 特征离散化
问题
您有一个数值特征,并希望将其分割成离散的箱子。
解决方案
根据数据分割方式的不同,我们可以使用两种技术。首先,我们可以根据某个阈值对特征进行二值化:
# Load libraries
import numpy as np
from sklearn.preprocessing import Binarizer
# Create feature
age = np.array([[6],
[12],
[20],
[36],
[65]])
# Create binarizer
binarizer = Binarizer(threshold=18)
# Transform feature
binarizer.fit_transform(age)
array([[0],
[0],
[1],
[1],
[1]])
其次,我们可以根据多个阈值分割数值特征:
# Bin feature
np.digitize(age, bins=[20,30,64])
array([[0],
[0],
[1],
[2],
[3]])
注意,bins 参数的参数表示每个箱的左边缘。例如,20 参数不包括值为 20 的元素,只包括比 20 小的两个值。我们可以通过将参数 right 设置为 True 来切换这种行为:
# Bin feature
np.digitize(age, bins=[20,30,64], right=True)
array([[0],
[0],
[0],
[2],
[3]])
讨论
当我们有理由认为数值特征应该表现得更像分类特征时,离散化可以是一种有效的策略。例如,我们可能认为 19 岁和 20 岁的人的消费习惯几乎没有什么差异,但 20 岁和 21 岁之间存在显著差异(美国的法定饮酒年龄)。在这种情况下,将数据中的个体分为可以饮酒和不能饮酒的人可能是有用的。同样,在其他情况下,将数据离散化为三个或更多的箱子可能是有用的。
在解决方案中,我们看到了两种离散化的方法——scikit-learn 的Binarizer用于两个区间和 NumPy 的digitize用于三个或更多的区间——然而,我们也可以像使用Binarizer那样使用digitize来对功能进行二值化,只需指定一个阈值:
# Bin feature
np.digitize(age, bins=[18])
array([[0],
[0],
[1],
[1],
[1]])
另请参阅
4.9 使用聚类对观测进行分组
问题
您希望将观测聚类,以便将相似的观测分组在一起。
解决方案
如果您知道您有k个组,您可以使用 k 均值聚类来将相似的观测分组,并输出一个新的特征,其中包含每个观测的组成员资格:
# Load libraries
import pandas as pd
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
# Make simulated feature matrix
features, _ = make_blobs(n_samples = 50,
n_features = 2,
centers = 3,
random_state = 1)
# Create DataFrame
dataframe = pd.DataFrame(features, columns=["feature_1", "feature_2"])
# Make k-means clusterer
clusterer = KMeans(3, random_state=0)
# Fit clusterer
clusterer.fit(features)
# Predict values
dataframe["group"] = clusterer.predict(features)
# View first few observations
dataframe.head(5)
| 功能 _1 | 功能 _2 | 组 | |
|---|---|---|---|
| 0 | –9.877554 | –3.336145 | 0 |
| 1 | –7.287210 | –8.353986 | 2 |
| 2 | –6.943061 | –7.023744 | 2 |
| 3 | –7.440167 | –8.791959 | 2 |
| 4 | –6.641388 | –8.075888 | 2 |
讨论
我们稍微超前一点,并且将在本书的后面更深入地讨论聚类算法。但是,我想指出,我们可以将聚类用作预处理步骤。具体来说,我们使用无监督学习算法(如 k 均值)将观测分成组。结果是一个分类特征,具有相似观测的成员属于同一组。
如果您没有理解所有这些,不要担心:只需将聚类可用于预处理的想法存档。如果您真的等不及,现在就可以翻到第十九章。
4.10 删除具有缺失值的观测
问题
您需要删除包含缺失值的观测。
解决方案
使用 NumPy 的巧妙一行代码轻松删除具有缺失值的观测:
# Load library
import numpy as np
# Create feature matrix
features = np.array([[1.1, 11.1],
[2.2, 22.2],
[3.3, 33.3],
[4.4, 44.4],
[np.nan, 55]])
# Keep only observations that are not (denoted by ~) missing
features[~np.isnan(features).any(axis=1)]
array([[ 1.1, 11.1],
[ 2.2, 22.2],
[ 3.3, 33.3],
[ 4.4, 44.4]])
或者,我们可以使用 pandas 删除缺失的观测:
# Load library
import pandas as pd
# Load data
dataframe = pd.DataFrame(features, columns=["feature_1", "feature_2"])
# Remove observations with missing values
dataframe.dropna()
| 功能 _1 | 功能 _2 | |
|---|---|---|
| 0 | 1.1 | 11.1 |
| 1 | 2.2 | 22.2 |
| 2 | 3.3 | 33.3 |
| 3 | 4.4 | 44.4 |
讨论
大多数机器学习算法无法处理目标和特征数组中的任何缺失值。因此,我们不能忽略数据中的缺失值,必须在预处理过程中解决这个问题。
最简单的解决方案是删除包含一个或多个缺失值的每个观测,可以使用 NumPy 或 pandas 快速轻松地完成此任务。
也就是说,我们应该非常不情愿地删除具有缺失值的观测。删除它们是核心选项,因为我们的算法失去了观测的非缺失值中包含的信息。
同样重要的是,根据缺失值的原因,删除观测可能会向我们的数据引入偏差。有三种类型的缺失数据:
完全随机缺失(MCAR)
缺失值出现的概率与一切无关。例如,调查对象在回答问题之前掷骰子:如果她掷出六点,她会跳过那个问题。
随机缺失(MAR)
值缺失的概率并非完全随机,而是依赖于其他特征捕获的信息。例如,一项调查询问性别身份和年薪,女性更有可能跳过薪水问题;然而,她们的未响应仅依赖于我们在性别身份特征中捕获的信息。
缺失非随机(MNAR)
值缺失的概率并非随机,而是依赖于我们特征未捕获的信息。例如,一项调查询问年薪,女性更有可能跳过薪水问题,而我们的数据中没有性别身份特征。
如果数据是 MCAR 或 MAR,有时可以接受删除观测值。但是,如果值是 MNAR,缺失本身就是信息。删除 MNAR 观测值可能会在数据中引入偏差,因为我们正在删除由某些未观察到的系统效应产生的观测值。
另请参阅
4.11 填补缺失值
问题
您的数据中存在缺失值,并希望通过通用方法或预测来填补它们。
解决方案
您可以使用 k 最近邻(KNN)或 scikit-learn 的SimpleImputer类来填补缺失值。如果数据量较小,请使用 KNN 进行预测和填补缺失值:
# Load libraries
import numpy as np
from sklearn.impute import KNNImputer
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs
# Make a simulated feature matrix
features, _ = make_blobs(n_samples = 1000,
n_features = 2,
random_state = 1)
# Standardize the features
scaler = StandardScaler()
standardized_features = scaler.fit_transform(features)
# Replace the first feature's first value with a missing value
true_value = standardized_features[0,0]
standardized_features[0,0] = np.nan
# Predict the missing values in the feature matrix
knn_imputer = KNNImputer(n_neighbors=5)
features_knn_imputed = knn_imputer.fit_transform(standardized_features)
# Compare true and imputed values
print("True Value:", true_value)
print("Imputed Value:", features_knn_imputed[0,0])
True Value: 0.8730186114
Imputed Value: 1.09553327131
或者,我们可以使用 scikit-learn 的imputer模块中的SimpleImputer类,将缺失值用特征的均值、中位数或最频繁的值填充。然而,通常情况下,与 KNN 相比,我们通常会获得更差的结果:
# Load libraries
import numpy as np
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs
# Make a simulated feature matrix
features, _ = make_blobs(n_samples = 1000,
n_features = 2,
random_state = 1)
# Standardize the features
scaler = StandardScaler()
standardized_features = scaler.fit_transform(features)
# Replace the first feature's first value with a missing value
true_value = standardized_features[0,0]
standardized_features[0,0] = np.nan
# Create imputer using the "mean" strategy
mean_imputer = SimpleImputer(strategy="mean")
# Impute values
features_mean_imputed = mean_imputer.fit_transform(features)
# Compare true and imputed values
print("True Value:", true_value)
print("Imputed Value:", features_mean_imputed[0,0])
True Value: 0.8730186114
Imputed Value: -3.05837272461
讨论
替换缺失数据的两种主要策略都有各自的优势和劣势。首先,我们可以使用机器学习来预测缺失数据的值。为此,我们将带有缺失值的特征视为目标向量,并使用其余子集特征来预测缺失值。虽然我们可以使用各种机器学习算法来填补值,但一个流行的选择是 KNN。在第十五章深入讨论了 KNN,简而言之,该算法使用k个最近的观测值(根据某个距离度量)来预测缺失值。在我们的解决方案中,我们使用了五个最接近的观测值来预测缺失值。
KNN 的缺点在于为了知道哪些观测值最接近缺失值,需要计算缺失值与每个观测值之间的距离。在较小的数据集中这是合理的,但是如果数据集有数百万个观测值,则很快会变得问题重重。在这种情况下,近似最近邻(ANN)是一个更可行的方法。我们将在第 15.5 节讨论 ANN。
与 KNN 相比,一种可替代且更可扩展的策略是用平均值、中位数或众数填补数值数据的缺失值。例如,在我们的解决方案中,我们使用 scikit-learn 将缺失值填充为特征的均值。填充的值通常不如我们使用 KNN 时接近真实值,但我们可以更轻松地将均值填充应用到包含数百万观察值的数据中。
如果我们使用填充,创建一个二进制特征指示观察是否包含填充值是一个好主意。
另请参阅
第五章:处理分类数据
5.0 介绍
通常有用的是,我们不仅仅用数量来衡量物体,而是用某种质量来衡量。我们经常用类别如性别、颜色或汽车品牌来表示定性信息。然而,并非所有分类数据都相同。没有内在排序的类别集称为名义。名义类别的例子包括:
-
蓝色,红色,绿色
-
男,女
-
香蕉,草莓,苹果
相比之下,当一组类别具有一些自然顺序时,我们称之为序数。例如:
-
低,中,高
-
年轻,年老
-
同意,中立,不同意
此外,分类信息通常以向量或字符串列(例如"Maine"、"Texas"、"Delaware")的形式表示在数据中。问题在于,大多数机器学习算法要求输入为数值。
k 最近邻算法是需要数值数据的一个例子。算法中的一步是计算观测之间的距离,通常使用欧氏距离:
∑ i=1 n (x i -y i ) 2
其中x和y是两个观测值,下标i表示观测的第i个特征的值。然而,如果xi的值是一个字符串(例如"Texas"),显然是无法进行距离计算的。我们需要将字符串转换为某种数值格式,以便可以将其输入到欧氏距离方程中。我们的目标是以一种能够正确捕捉类别信息(序数性,类别之间的相对间隔等)的方式转换数据。在本章中,我们将涵盖使这种转换以及克服处理分类数据时经常遇到的其他挑战的技术。
5.1 编码名义分类特征
问题
您有一个没有内在排序的名义类别特征(例如苹果,梨,香蕉),并且希望将该特征编码为数值。
解决方案
使用 scikit-learn 的LabelBinarizer对特征进行独热编码:
# Import libraries
import numpy as np
from sklearn.preprocessing import LabelBinarizer, MultiLabelBinarizer
# Create feature
feature = np.array([["Texas"],
["California"],
["Texas"],
["Delaware"],
["Texas"]])
# Create one-hot encoder
one_hot = LabelBinarizer()
# One-hot encode feature
one_hot.fit_transform(feature)
array([[0, 0, 1],
[1, 0, 0],
[0, 0, 1],
[0, 1, 0],
[0, 0, 1]])
我们可以使用classes_属性来输出类别:
# View feature classes
one_hot.classes_
array(['California', 'Delaware', 'Texas'],
dtype='<U10')
如果我们想要反向进行独热编码,我们可以使用inverse_transform:
# Reverse one-hot encoding
one_hot.inverse_transform(one_hot.transform(feature))
array(['Texas', 'California', 'Texas', 'Delaware', 'Texas'],
dtype='<U10')
我们甚至可以使用 pandas 来进行独热编码:
# Import library
import pandas as pd
# Create dummy variables from feature
pd.get_dummies(feature[:,0])
| 加利福尼亚 | 特拉华州 | 德克萨斯州 | |
|---|---|---|---|
| 0 | 0 | 0 | 1 |
| 1 | 1 | 0 | 0 |
| 2 | 0 | 0 | 1 |
| 3 | 0 | 1 | 0 |
| 4 | 0 | 0 | 1 |
scikit-learn 的一个有用特性是能够处理每个观测列表包含多个类别的情况:
# Create multiclass feature
multiclass_feature = [("Texas", "Florida"),
("California", "Alabama"),
("Texas", "Florida"),
("Delaware", "Florida"),
("Texas", "Alabama")]
# Create multiclass one-hot encoder
one_hot_multiclass = MultiLabelBinarizer()
# One-hot encode multiclass feature
one_hot_multiclass.fit_transform(multiclass_feature)
array([[0, 0, 0, 1, 1],
[1, 1, 0, 0, 0],
[0, 0, 0, 1, 1],
[0, 0, 1, 1, 0],
[1, 0, 0, 0, 1]])
再次,我们可以使用classes_方法查看类别:
# View classes
one_hot_multiclass.classes_
array(['Alabama', 'California', 'Delaware', 'Florida', 'Texas'], dtype=object)
讨论
我们可能认为正确的策略是为每个类分配一个数值(例如,Texas = 1,California = 2)。然而,当我们的类没有内在的顺序(例如,Texas 不是比 California “更少”),我们的数值值误创建了一个不存在的排序。
适当的策略是为原始特征的每个类创建一个二进制特征。在机器学习文献中通常称为 独热编码,而在统计和研究文献中称为 虚拟化。我们解决方案的特征是一个包含三个类(即 Texas、California 和 Delaware)的向量。在独热编码中,每个类都成为其自己的特征,当类出现时为 1,否则为 0。因为我们的特征有三个类,独热编码返回了三个二进制特征(每个类一个)。通过使用独热编码,我们可以捕捉观察值在类中的成员身份,同时保持类缺乏任何层次结构的概念。
最后,经常建议在对一个特征进行独热编码后,删除结果矩阵中的一个独热编码特征,以避免线性相关性。
参见
5.2 编码序数分类特征
问题
您有一个序数分类特征(例如高、中、低),并且希望将其转换为数值。
解决方案
使用 pandas DataFrame 的 replace 方法将字符串标签转换为数值等价物:
# Load library
import pandas as pd
# Create features
dataframe = pd.DataFrame({"Score": ["Low", "Low", "Medium", "Medium", "High"]})
# Create mapper
scale_mapper = {"Low":1,
"Medium":2,
"High":3}
# Replace feature values with scale
dataframe["Score"].replace(scale_mapper)
0 1
1 1
2 2
3 2
4 3
Name: Score, dtype: int64
讨论
经常情况下,我们有一个具有某种自然顺序的类的特征。一个著名的例子是 Likert 量表:
-
强烈同意
-
同意
-
中立
-
不同意
-
强烈不同意
在将特征编码用于机器学习时,我们需要将序数类转换为保持排序概念的数值。最常见的方法是创建一个将类的字符串标签映射到数字的字典,然后将该映射应用于特征。
根据我们对序数类的先前信息,选择数值值是很重要的。在我们的解决方案中,high 比 low 大三倍。在许多情况下这是可以接受的,但如果假设的类之间间隔不均等,这种方法可能失效:
dataframe = pd.DataFrame({"Score": ["Low",
"Low",
"Medium",
"Medium",
"High",
"Barely More Than Medium"]})
scale_mapper = {"Low":1,
"Medium":2,
"Barely More Than Medium":3,
"High":4}
dataframe["Score"].replace(scale_mapper)
0 1
1 1
2 2
3 2
4 4
5 3
Name: Score, dtype: int64
在此示例中,Low 和 Medium 之间的距离与 Medium 和 Barely More Than Medium 之间的距离相同,这几乎肯定不准确。最佳方法是在映射到类的数值值时要注意:
scale_mapper = {"Low":1,
"Medium":2,
"Barely More Than Medium":2.1,
"High":3}
dataframe["Score"].replace(scale_mapper)
0 1.0
1 1.0
2 2.0
3 2.0
4 3.0
5 2.1
Name: Score, dtype: float64
5.3 编码特征字典
问题
您有一个字典,并希望将其转换为特征矩阵。
解决方案
使用 DictVectorizer:
# Import library
from sklearn.feature_extraction import DictVectorizer
# Create dictionary
data_dict = [{"Red": 2, "Blue": 4},
{"Red": 4, "Blue": 3},
{"Red": 1, "Yellow": 2},
{"Red": 2, "Yellow": 2}]
# Create dictionary vectorizer
dictvectorizer = DictVectorizer(sparse=False)
# Convert dictionary to feature matrix
features = dictvectorizer.fit_transform(data_dict)
# View feature matrix
features
array([[ 4., 2., 0.],
[ 3., 4., 0.],
[ 0., 1., 2.],
[ 0., 2., 2.]])
默认情况下,DictVectorizer输出一个仅存储值非 0 的稀疏矩阵。当我们遇到大规模矩阵(通常在自然语言处理中)并希望最小化内存需求时,这非常有帮助。我们可以使用sparse=False来强制DictVectorizer输出一个密集矩阵。
我们可以使用get_feature_names方法获取每个生成特征的名称:
# Get feature names
feature_names = dictvectorizer.get_feature_names()
# View feature names
feature_names
['Blue', 'Red', 'Yellow']
虽然不必要,为了说明我们可以创建一个 pandas DataFrame 来更好地查看输出:
# Import library
import pandas as pd
# Create dataframe from features
pd.DataFrame(features, columns=feature_names)
| 蓝色 | 红色 | 黄色 | |
|---|---|---|---|
| 0 | 4.0 | 2.0 | 0.0 |
| 1 | 3.0 | 4.0 | 0.0 |
| 2 | 0.0 | 1.0 | 2.0 |
| 3 | 0.0 | 2.0 | 2.0 |
讨论
字典是许多编程语言中常用的数据结构;然而,机器学习算法期望数据以矩阵的形式存在。我们可以使用 scikit-learn 的DictVectorizer来实现这一点。
这是自然语言处理时常见的情况。例如,我们可能有一系列文档,每个文档都有一个字典,其中包含每个单词在文档中出现的次数。使用DictVectorizer,我们可以轻松创建一个特征矩阵,其中每个特征是每个文档中单词出现的次数:
# Create word count dictionaries for four documents
doc_1_word_count = {"Red": 2, "Blue": 4}
doc_2_word_count = {"Red": 4, "Blue": 3}
doc_3_word_count = {"Red": 1, "Yellow": 2}
doc_4_word_count = {"Red": 2, "Yellow": 2}
# Create list
doc_word_counts = [doc_1_word_count,
doc_2_word_count,
doc_3_word_count,
doc_4_word_count]
# Convert list of word count dictionaries into feature matrix
dictvectorizer.fit_transform(doc_word_counts)
array([[ 4., 2., 0.],
[ 3., 4., 0.],
[ 0., 1., 2.],
[ 0., 2., 2.]])
在我们的示例中,只有三个唯一的单词(红色,黄色,蓝色),所以我们的矩阵中只有三个特征;然而,如果每个文档实际上是大学图书馆中的一本书,我们的特征矩阵将非常庞大(然后我们将希望将sparse设置为True)。
参见
5.4 填充缺失的类值
问题
您有一个包含缺失值的分类特征,您希望用预测值替换它。
解决方案
理想的解决方案是训练一个机器学习分类器算法来预测缺失值,通常是 k 近邻(KNN)分类器:
# Load libraries
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
# Create feature matrix with categorical feature
X = np.array([[0, 2.10, 1.45],
[1, 1.18, 1.33],
[0, 1.22, 1.27],
[1, -0.21, -1.19]])
# Create feature matrix with missing values in the categorical feature
X_with_nan = np.array([[np.nan, 0.87, 1.31],
[np.nan, -0.67, -0.22]])
# Train KNN learner
clf = KNeighborsClassifier(3, weights='distance')
trained_model = clf.fit(X[:,1:], X[:,0])
# Predict class of missing values
imputed_values = trained_model.predict(X_with_nan[:,1:])
# Join column of predicted class with their other features
X_with_imputed = np.hstack((imputed_values.reshape(-1,1), X_with_nan[:,1:]))
# Join two feature matrices
np.vstack((X_with_imputed, X))
array([[ 0\. , 0.87, 1.31],
[ 1\. , -0.67, -0.22],
[ 0\. , 2.1 , 1.45],
[ 1\. , 1.18, 1.33],
[ 0\. , 1.22, 1.27],
[ 1\. , -0.21, -1.19]])
另一种解决方案是使用特征的最频繁值填充缺失值:
from sklearn.impute import SimpleImputer
# Join the two feature matrices
X_complete = np.vstack((X_with_nan, X))
imputer = SimpleImputer(strategy='most_frequent')
imputer.fit_transform(X_complete)
array([[ 0\. , 0.87, 1.31],
[ 0\. , -0.67, -0.22],
[ 0\. , 2.1 , 1.45],
[ 1\. , 1.18, 1.33],
[ 0\. , 1.22, 1.27],
[ 1\. , -0.21, -1.19]])
讨论
当分类特征中存在缺失值时,我们最好的解决方案是打开我们的机器学习算法工具箱,预测缺失观测值的值。我们可以通过将具有缺失值的特征视为目标向量,其他特征视为特征矩阵来实现此目标。常用的算法之一是 KNN(在第十五章中详细讨论),它将缺失值分配给k个最近观测中最频繁出现的类别。
或者,我们可以使用特征的最频繁类别填充缺失值,甚至丢弃具有缺失值的观测。虽然不如 KNN 复杂,但这些选项在处理大数据时更具可扩展性。无论哪种情况,都建议包含一个二元特征,指示哪些观测包含了填充值。
参见
5.5 处理不平衡类别
问题
如果您有一个具有高度不平衡类别的目标向量,并且希望进行调整以处理类别不平衡。
解决方案
收集更多数据。如果不可能,请更改用于评估模型的指标。如果这样做不起作用,请考虑使用模型的内置类权重参数(如果可用),下采样或上采样。我们将在后面的章节中介绍评估指标,因此现在让我们专注于类权重参数、下采样和上采样。
为了演示我们的解决方案,我们需要创建一些具有不平衡类别的数据。Fisher 的鸢尾花数据集包含三个平衡类别的 50 个观察,每个类别表示花的物种(Iris setosa、Iris virginica 和 Iris versicolor)。为了使数据集不平衡,我们移除了 50 个 Iris setosa 观察中的 40 个,并合并了 Iris virginica 和 Iris versicolor 类别。最终结果是一个二元目标向量,指示观察是否为 Iris setosa 花。结果是 10 个 Iris setosa(类别 0)的观察和 100 个非 Iris setosa(类别 1)的观察:
# Load libraries
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
# Load iris data
iris = load_iris()
# Create feature matrix
features = iris.data
# Create target vector
target = iris.target
# Remove first 40 observations
features = features[40:,:]
target = target[40:]
# Create binary target vector indicating if class 0
target = np.where((target == 0), 0, 1)
# Look at the imbalanced target vector
target
array([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, 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])
scikit-learn 中的许多算法在训练期间提供一个参数来加权类别,以抵消其不平衡的效果。虽然我们尚未涵盖它,RandomForestClassifier 是一种流行的分类算法,并包含一个 class_weight 参数;在 14.4 节 中了解更多关于 RandomForestClassifier 的信息。您可以传递一个参数显式指定所需的类权重:
# Create weights
weights = {0: 0.9, 1: 0.1}
# Create random forest classifier with weights
RandomForestClassifier(class_weight=weights)
RandomForestClassifier(class_weight={0: 0.9, 1: 0.1})
或者您可以传递 balanced,它会自动创建与类别频率成反比的权重:
# Train a random forest with balanced class weights
RandomForestClassifier(class_weight="balanced")
RandomForestClassifier(class_weight='balanced')
或者,我们可以对多数类进行下采样或者对少数类进行上采样。在 下采样 中,我们从多数类中无放回随机抽样(即观察次数较多的类别)以创建一个新的观察子集,其大小等于少数类。例如,如果少数类有 10 个观察,我们将从多数类中随机选择 10 个观察,然后使用这 20 个观察作为我们的数据。在这里,我们正是利用我们不平衡的鸢尾花数据做到这一点:
# Indicies of each class's observations
i_class0 = np.where(target == 0)[0]
i_class1 = np.where(target == 1)[0]
# Number of observations in each class
n_class0 = len(i_class0)
n_class1 = len(i_class1)
# For every observation of class 0, randomly sample
# from class 1 without replacement
i_class1_downsampled = np.random.choice(i_class1, size=n_class0, replace=False)
# Join together class 0's target vector with the
# downsampled class 1's target vector
np.hstack((target[i_class0], target[i_class1_downsampled]))
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
# Join together class 0's feature matrix with the
# downsampled class 1's feature matrix
np.vstack((features[i_class0,:], features[i_class1_downsampled,:]))[0:5]
array([[ 5\. , 3.5, 1.3, 0.3],
[ 4.5, 2.3, 1.3, 0.3],
[ 4.4, 3.2, 1.3, 0.2],
[ 5\. , 3.5, 1.6, 0.6],
[ 5.1, 3.8, 1.9, 0.4]])
我们的另一种选择是对少数类进行上采样。在 上采样 中,对于多数类中的每个观察,我们从少数类中随机选择一个观察,可以重复选择。结果是来自少数和多数类的相同数量的观察。上采样的实现非常类似于下采样,只是反向操作:
# For every observation in class 1, randomly sample from class 0 with
# replacement
i_class0_upsampled = np.random.choice(i_class0, size=n_class1, replace=True)
# Join together class 0's upsampled target vector with class 1's target vector
np.concatenate((target[i_class0_upsampled], target[i_class1]))
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, 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, 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])
# Join together class 0's upsampled feature matrix with class 1's feature matrix
np.vstack((features[i_class0_upsampled,:], features[i_class1,:]))[0:5]
array([[ 5\. , 3.5, 1.6, 0.6],
[ 5\. , 3.5, 1.6, 0.6],
[ 5\. , 3.3, 1.4, 0.2],
[ 4.5, 2.3, 1.3, 0.3],
[ 4.8, 3\. , 1.4, 0.3]])
讨论
在现实世界中,不平衡的类别随处可见—大多数访问者不会点击购买按钮,而许多类型的癌症又是相当罕见的。因此,在机器学习中处理不平衡的类别是一项常见的活动。
我们最好的策略就是简单地收集更多的观察数据—尤其是来自少数类的观察数据。然而,通常情况下这并不可能,所以我们必须求助于其他选择。
第二种策略是使用更适合于不平衡类别的模型评估指标。准确率通常被用作评估模型性能的指标,但在存在不平衡类别的情况下,准确率可能并不合适。例如,如果只有 0.5%的观察数据属于某种罕见的癌症,那么即使是一个简单的模型预测没有人有癌症,准确率也会达到 99.5%。显然,这并不理想。我们将在后面的章节中讨论一些更好的指标,如混淆矩阵、精确度、召回率、*F[1]*分数和 ROC 曲线。
第三种策略是使用一些模型实现中包含的类别加权参数。这使得算法能够调整不平衡的类别。幸运的是,许多 scikit-learn 分类器都有一个class_weight参数,这使得它成为一个不错的选择。
第四和第五种策略是相关的:下采样和上采样。在下采样中,我们创建一个与少数类相同大小的多数类的随机子集。在上采样中,我们从少数类中重复有放回地抽样,使其大小与多数类相等。选择使用下采样还是上采样是与上下文相关的决定,通常我们应该尝试两种方法,看看哪一种效果更好。
第六章:处理文本
6.0 引言
非结构化的文本数据,如书籍内容或推文,既是最有趣的特征来源之一,也是最复杂的处理之一。在本章中,我们将介绍将文本转换为信息丰富特征的策略,并使用一些出色的特征(称为嵌入),这些特征在涉及自然语言处理(NLP)的任务中变得日益普遍。
这并不意味着这里涵盖的配方是全面的。整个学术学科都专注于处理文本等非结构化数据。在本章中,我们将涵盖一些常用的技术;掌握这些将为我们的预处理工具箱增添宝贵的工具。除了许多通用的文本处理配方外,我们还将演示如何导入和利用一些预训练的机器学习模型来生成更丰富的文本特征。
6.1 清理文本
问题
你有一些非结构化文本数据,想要完成一些基本的清理工作。
解决方案
在下面的例子中,我们查看三本书的文本,并通过 Python 的核心字符串操作,特别是strip、replace和split,对其进行清理:
# Create text
text_data = [" Interrobang. By Aishwarya Henriette ",
"Parking And Going. By Karl Gautier",
" Today Is The night. By Jarek Prakash "]
# Strip whitespaces
strip_whitespace = [string.strip() for string in text_data]
# Show text
strip_whitespace
['Interrobang. By Aishwarya Henriette',
'Parking And Going. By Karl Gautier',
'Today Is The night. By Jarek Prakash']
# Remove periods
remove_periods = [string.replace(".", "") for string in strip_whitespace]
# Show text
remove_periods
['Interrobang By Aishwarya Henriette',
'Parking And Going By Karl Gautier',
'Today Is The night By Jarek Prakash']
我们还创建并应用了一个自定义转换函数:
# Create function
def capitalizer(string: str) -> str:
return string.upper()
# Apply function
[capitalizer(string) for string in remove_periods]
['INTERROBANG BY AISHWARYA HENRIETTE',
'PARKING AND GOING BY KARL GAUTIER',
'TODAY IS THE NIGHT BY JAREK PRAKASH']
最后,我们可以使用正则表达式进行强大的字符串操作:
# Import library
import re
# Create function
def replace_letters_with_X(string: str) -> str:
return re.sub(r"[a-zA-Z]", "X", string)
# Apply function
[replace_letters_with_X(string) for string in remove_periods]
['XXXXXXXXXXX XX XXXXXXXXX XXXXXXXXX',
'XXXXXXX XXX XXXXX XX XXXX XXXXXXX',
'XXXXX XX XXX XXXXX XX XXXXX XXXXXXX']
讨论
一些文本数据在用于构建特征或在输入算法之前需要进行基本的清理。大多数基本的文本清理可以使用 Python 的标准字符串操作完成。在实际应用中,我们很可能会定义一个自定义的清理函数(例如capitalizer),结合一些清理任务,并将其应用于文本数据。虽然清理字符串可能会删除一些信息,但它使数据更易于处理。字符串具有许多有用的固有方法用于清理和处理;一些额外的例子可以在这里找到:
# Define a string
s = "machine learning in python cookbook"
# Find the first index of the letter "n"
find_n = s.find("n")
# Whether or not the string starts with "m"
starts_with_m = s.startswith("m")
# Whether or not the string ends with "python"
ends_with_python = s.endswith("python")
# Is the string alphanumeric
is_alnum = s.isalnum()
# Is it composed of only alphabetical characters (not including spaces)
is_alpha = s.isalpha()
# Encode as utf-8
encode_as_utf8 = s.encode("utf-8")
# Decode the same utf-8
decode = encode_as_utf8.decode("utf-8")
print(
find_n,
starts_with_m,
ends_with_python,
is_alnum,
is_alpha,
encode_as_utf8,
decode,
sep = "|"
)
5|True|False|False|False|b'machine learning in python cookbook'|machine learning
in python cookbook
参见
6.2 解析和清理 HTML
问题
你有包含 HTML 元素的文本数据,并希望仅提取文本部分。
解决方案
使用 Beautiful Soup 广泛的选项集来解析和从 HTML 中提取:
# Load library
from bs4 import BeautifulSoup
# Create some HTML code
html = "<div class='full_name'>"\
"<span style='font-weight:bold'>Masego"\
"</span> Azra</div>"
# Parse html
soup = BeautifulSoup(html, "lxml")
# Find the div with the class "full_name", show text
soup.find("div", { "class" : "full_name" }).text
'Masego Azra'
讨论
尽管名字奇怪,Beautiful Soup 是一个功能强大的 Python 库,专门用于解析 HTML。通常 Beautiful Soup 用于实时网页抓取过程中处理 HTML,但我们同样可以使用它来提取静态 HTML 中嵌入的文本数据。Beautiful Soup 的全部操作远超出本书的范围,但即使是我们在解决方案中使用的方法,也展示了使用find()方法可以轻松地解析 HTML 并从特定标签中提取信息。
参见
6.3 删除标点符号
问题
你有一项文本数据的特征,并希望去除标点符号。
解决方案
定义一个使用translate和标点字符字典的函数:
# Load libraries
import unicodedata
import sys
# Create text
text_data = ['Hi!!!! I. Love. This. Song....',
'10000% Agree!!!! #LoveIT',
'Right?!?!']
# Create a dictionary of punctuation characters
punctuation = dict.fromkeys(
(i for i in range(sys.maxunicode)
if unicodedata.category(chr(i)).startswith('P')
),
None
)
# For each string, remove any punctuation characters
[string.translate(punctuation) for string in text_data]
['Hi I Love This Song', '10000 Agree LoveIT', 'Right']
讨论
Python 的 translate 方法因其速度而流行。在我们的解决方案中,首先我们创建了一个包含所有标点符号字符(按照 Unicode 标准)作为键和 None 作为值的字典 punctuation。接下来,我们将字符串中所有在 punctuation 中的字符翻译为 None,从而有效地删除它们。还有更可读的方法来删除标点,但这种有些“hacky”的解决方案具有比替代方案更快的优势。
需要意识到标点包含信息这一事实是很重要的(例如,“对吧?”与“对吧!”)。在需要手动创建特征时,删除标点可能是必要的恶;然而,如果标点很重要,我们应该确保考虑到这一点。根据我们试图完成的下游任务的不同,标点可能包含我们希望保留的重要信息(例如,使用“?”来分类文本是否包含问题)。
6.4 文本分词
问题
你有一段文本,希望将其分解成单独的单词。
解决方案
Python 的自然语言工具包(NLTK)具有强大的文本操作集,包括词分词:
# Load library
from nltk.tokenize import word_tokenize
# Create text
string = "The science of today is the technology of tomorrow"
# Tokenize words
word_tokenize(string)
['The', 'science', 'of', 'today', 'is', 'the', 'technology', 'of', 'tomorrow']
我们还可以将其分词成句子:
# Load library
from nltk.tokenize import sent_tokenize
# Create text
string = "The science of today is the technology of tomorrow. Tomorrow is today."
# Tokenize sentences
sent_tokenize(string)
['The science of today is the technology of tomorrow.', 'Tomorrow is today.']
讨论
分词,尤其是词分词,在清洗文本数据后是一项常见任务,因为它是将文本转换为我们将用来构建有用特征的数据的第一步。一些预训练的自然语言处理模型(如 Google 的 BERT)使用特定于模型的分词技术;然而,在从单词级别获取特征之前,词级分词仍然是一种相当常见的分词方法。
6.5 移除停用词
问题
给定标记化的文本数据,你希望移除极其常见的单词(例如,a、is、of、on),它们的信息价值很小。
解决方案
使用 NLTK 的 stopwords:
# Load library
from nltk.corpus import stopwords
# You will have to download the set of stop words the first time
# import nltk
# nltk.download('stopwords')
# Create word tokens
tokenized_words = ['i',
'am',
'going',
'to',
'go',
'to',
'the',
'store',
'and',
'park']
# Load stop words
stop_words = stopwords.words('english')
# Remove stop words
[word for word in tokenized_words if word not in stop_words]
['going', 'go', 'store', 'park']
讨论
虽然“停用词”可以指任何我们希望在处理前移除的单词集,但通常这个术语指的是那些本身包含很少信息价值的极其常见的单词。是否选择移除停用词将取决于你的具体用例。NLTK 有一个常见停用词列表,我们可以用来查找并移除我们标记化的单词中的停用词:
# Show stop words
stop_words[:5]
['i', 'me', 'my', 'myself', 'we']
注意,NLTK 的 stopwords 假定标记化的单词都是小写的。
6.6 词干提取
问题
你有一些标记化的单词,并希望将它们转换为它们的根形式。
解决方案
使用 NLTK 的 PorterStemmer:
# Load library
from nltk.stem.porter import PorterStemmer
# Create word tokens
tokenized_words = ['i', 'am', 'humbled', 'by', 'this', 'traditional', 'meeting']
# Create stemmer
porter = PorterStemmer()
# Apply stemmer
[porter.stem(word) for word in tokenized_words]
['i', 'am', 'humbl', 'by', 'thi', 'tradit', 'meet']
讨论
词干提取 通过识别和移除词缀(例如动名词),将单词减少到其词干,同时保持单词的根本含义。例如,“tradition” 和 “traditional” 都有 “tradit” 作为它们的词干,表明虽然它们是不同的词,但它们代表同一个一般概念。通过词干提取我们的文本数据,我们将其转换为不太可读但更接近其基本含义的形式,因此更适合跨观察进行比较。NLTK 的 PorterStemmer 实现了广泛使用的 Porter 词干提取算法,以移除或替换常见的后缀,生成词干。
参见
6.7 标记词性
问题
您拥有文本数据,并希望标记每个单词或字符的词性。
解决方案
使用 NLTK 的预训练词性标注器:
# Load libraries
from nltk import pos_tag
from nltk import word_tokenize
# Create text
text_data = "Chris loved outdoor running"
# Use pretrained part of speech tagger
text_tagged = pos_tag(word_tokenize(text_data))
# Show parts of speech
text_tagged
[('Chris', 'NNP'), ('loved', 'VBD'), ('outdoor', 'RP'), ('running', 'VBG')]
输出是一个包含单词和词性标签的元组列表。NLTK 使用宾树库的词性标签。宾树库的一些示例标签包括:
| Tag | 词性 |
|---|---|
| NNP | 专有名词,单数 |
| NN | 名词,单数或集合名词 |
| RB | 副词 |
| VBD | 动词,过去式 |
| VBG | 动词,动名词或现在分词 |
| JJ | 形容词 |
| PRP | 人称代词 |
一旦文本被标记,我们可以使用标签找到特定的词性。例如,这里是所有的名词:
# Filter words
[word for word, tag in text_tagged if tag in ['NN','NNS','NNP','NNPS'] ]
['Chris']
更现实的情况可能是有数据,每个观察都包含一条推文,并且我们希望将这些句子转换为各个词性的特征(例如,如果存在专有名词,则为 1,否则为 0):
# Import libraries
from sklearn.preprocessing import MultiLabelBinarizer
# Create text
tweets = ["I am eating a burrito for breakfast",
"Political science is an amazing field",
"San Francisco is an awesome city"]
# Create list
tagged_tweets = []
# Tag each word and each tweet
for tweet in tweets:
tweet_tag = nltk.pos_tag(word_tokenize(tweet))
tagged_tweets.append([tag for word, tag in tweet_tag])
# Use one-hot encoding to convert the tags into features
one_hot_multi = MultiLabelBinarizer()
one_hot_multi.fit_transform(tagged_tweets)
array([[1, 1, 0, 1, 0, 1, 1, 1, 0],
[1, 0, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 1]])
使用 classes_,我们可以看到每个特征都是一个词性标签:
# Show feature names
one_hot_multi.classes_
array(['DT', 'IN', 'JJ', 'NN', 'NNP', 'PRP', 'VBG', 'VBP', 'VBZ'], dtype=object)
讨论
如果我们的文本是英语且不涉及专业主题(例如医学),最简单的解决方案是使用 NLTK 的预训练词性标注器。但是,如果 pos_tag 不太准确,NLTK 还为我们提供了训练自己标注器的能力。训练标注器的主要缺点是我们需要一个大型文本语料库,其中每个词的标签是已知的。构建这种标记语料库显然是劳动密集型的,可能是最后的选择。
参见
6.8 执行命名实体识别
问题
您希望在自由文本中执行命名实体识别(如“人物”,“州”等)。
解决方案
使用 spaCy 的默认命名实体识别管道和模型从文本中提取实体:
# Import libraries
import spacy
# Load the spaCy package and use it to parse the text
# make sure you have run "python -m spacy download en"
nlp = spacy.load("en_core_web_sm")
doc = nlp("Elon Musk offered to buy Twitter using $21B of his own money.")
# Print each entity
print(doc.ents)
# For each entity print the text and the entity label
for entity in doc.ents:
print(entity.text, entity.label_, sep=",")
(Elon Musk, Twitter, 21B)
Elon Musk, PERSON
Twitter, ORG
21B, MONEY
讨论
命名实体识别是从文本中识别特定实体的过程。像 spaCy 这样的工具提供预配置的管道,甚至是预训练或微调的机器学习模型,可以轻松识别这些实体。在本例中,我们使用 spaCy 识别文本中的人物(“Elon Musk”)、组织(“Twitter”)和金额(“21B”)。利用这些信息,我们可以从非结构化文本数据中提取结构化信息。这些信息随后可以用于下游机器学习模型或数据分析。
训练自定义命名实体识别模型超出了本示例的范围;但是,通常使用深度学习和其他自然语言处理技术来完成此任务。
另请参阅
6.9 将文本编码为词袋模型
问题
您有文本数据,并希望创建一组特征,指示观察文本中包含特定单词的次数。
解决方案
使用 scikit-learn 的CountVectorizer:
# Load library
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
# Create text
text_data = np.array(['I love Brazil. Brazil!',
'Sweden is best',
'Germany beats both'])
# Create the bag of words feature matrix
count = CountVectorizer()
bag_of_words = count.fit_transform(text_data)
# Show feature matrix
bag_of_words
<3x8 sparse matrix of type '<class 'numpy.int64'>'
with 8 stored elements in Compressed Sparse Row format>
此输出是一个稀疏数组,在我们有大量文本时通常是必要的。但是,在我们的玩具示例中,我们可以使用toarray查看每个观察结果的单词计数矩阵:
bag_of_words.toarray()
array([[0, 0, 0, 2, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 0]], dtype=int64)
我们可以使用get_feature_names方法查看与每个特征关联的单词:
# Show feature names
count.get_feature_names_out()
array(['beats', 'best', 'both', 'brazil', 'germany', 'is', 'love',
'sweden'], dtype=object)
注意,I从I love Brazil中不被视为一个标记,因为默认的token_pattern只考虑包含两个或更多字母数字字符的标记。
然而,这可能会令人困惑,为了明确起见,这里是特征矩阵的外观,其中单词作为列名(每行是一个观察结果):
| beats | best | both | brazil | germany | is | love | sweden |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 2 | 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 |
| 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
讨论
将文本转换为特征的最常见方法之一是使用词袋模型。词袋模型为文本数据中的每个唯一单词输出一个特征,每个特征包含在观察中出现的次数计数。例如,在我们的解决方案中,句子“I love Brazil. Brazil!”中,“brazil”特征的值为2,因为单词brazil出现了两次。
我们解决方案中的文本数据故意很小。在现实世界中,文本数据的单个观察结果可能是整本书的内容!由于我们的词袋模型为数据中的每个唯一单词创建一个特征,因此生成的矩阵可能包含数千个特征。这意味着矩阵的大小有时可能会在内存中变得非常大。幸运的是,我们可以利用词袋特征矩阵的常见特性来减少我们需要存储的数据量。
大多数单词可能不会出现在大多数观察中,因此单词袋特征矩阵将主要包含值为 0 的值。我们称这些类型的矩阵为 稀疏。我们可以只存储非零值,然后假定所有其他值为 0,以节省内存,特别是在具有大型特征矩阵时。CountVectorizer 的一个好处是默认输出是稀疏矩阵。
CountVectorizer 配备了许多有用的参数,使得创建单词袋特征矩阵变得容易。首先,默认情况下,每个特征是一个单词,但这并不一定是情况。我们可以将每个特征设置为两个单词的组合(称为 2-gram)甚至三个单词(3-gram)。ngram_range 设置了我们的n-gram 的最小和最大大小。例如,(2,3) 将返回所有的 2-gram 和 3-gram。其次,我们可以使用 stop_words 轻松地去除低信息的填充词,可以使用内置列表或自定义列表。最后,我们可以使用 vocabulary 限制我们希望考虑的单词或短语列表。例如,我们可以仅为国家名称的出现创建一个单词袋特征矩阵:
# Create feature matrix with arguments
count_2gram = CountVectorizer(ngram_range=(1,2),
stop_words="english",
vocabulary=['brazil'])
bag = count_2gram.fit_transform(text_data)
# View feature matrix
bag.toarray()
array([[2],
[0],
[0]])
# View the 1-grams and 2-grams
count_2gram.vocabulary_
{'brazil': 0}
另请参阅
6.10 加权词重要性
问题
您希望一个单词袋,其中单词按其对观察的重要性加权。
解决方案
通过使用词频-逆文档频率(tf-idf)比较单词在文档(推文、电影评论、演讲文稿等)中的频率与单词在所有其他文档中的频率。scikit-learn 通过 TfidfVectorizer 轻松实现这一点:
# Load libraries
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
# Create text
text_data = np.array(['I love Brazil. Brazil!',
'Sweden is best',
'Germany beats both'])
# Create the tf-idf feature matrix
tfidf = TfidfVectorizer()
feature_matrix = tfidf.fit_transform(text_data)
# Show tf-idf feature matrix
feature_matrix
<3x8 sparse matrix of type '<class 'numpy.float64'>'
with 8 stored elements in Compressed Sparse Row format>
就像在食谱 6.9 中一样,输出是一个稀疏矩阵。然而,如果我们想将输出视为密集矩阵,我们可以使用 toarray:
# Show tf-idf feature matrix as dense matrix
feature_matrix.toarray()
array([[ 0\. , 0\. , 0\. , 0.89442719, 0\. ,
0\. , 0.4472136 , 0\. ],
[ 0\. , 0.57735027, 0\. , 0\. , 0\. ,
0.57735027, 0\. , 0.57735027],
[ 0.57735027, 0\. , 0.57735027, 0\. , 0.57735027,
0\. , 0\. , 0\. ]])
vocabulary_ 展示了每个特征的词汇:
# Show feature names
tfidf.vocabulary_
{'love': 6,
'brazil': 3,
'sweden': 7,
'is': 5,
'best': 1,
'germany': 4,
'beats': 0,
'both': 2}
讨论
单词在文档中出现的次数越多,该单词对该文档的重要性就越高。例如,如果单词 economy 经常出现,这表明文档可能与经济有关。我们称之为 词频 (tf)。
相反,如果一个词在许多文档中出现,它可能对任何单个文档的重要性较低。例如,如果某个文本数据中的每个文档都包含单词 after,那么它可能是一个不重要的词。我们称之为 文档频率 (df)。
通过结合这两个统计量,我们可以为每个单词分配一个分数,代表该单词在文档中的重要性。具体来说,我们将 tf 乘以文档频率的倒数 idf:
tf-idf ( t , d ) = t f ( t , d ) × idf ( t )
其中 t 是一个单词(术语),d 是一个文档。关于如何计算 tf 和 idf 有许多不同的变体。在 scikit-learn 中,tf 简单地是单词在文档中出现的次数,idf 计算如下:
idf ( t ) = l o g 1+n d 1+df(d,t) + 1
其中 nd 是文档数量,df(d,t) 是术语 t 的文档频率(即术语出现的文档数量)。
默认情况下,scikit-learn 使用欧几里得范数(L2 范数)对 tf-idf 向量进行归一化。结果值越高,单词对文档的重要性越大。
另请参阅
6.11 使用文本向量计算搜索查询中的文本相似度
问题
您想要使用 tf-idf 向量来实现 Python 中的文本搜索功能。
解决方案
使用 scikit-learn 计算 tf-idf 向量之间的余弦相似度:
# Load libraries
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel
# Create searchable text data
text_data = np.array(['I love Brazil. Brazil!',
'Sweden is best',
'Germany beats both'])
# Create the tf-idf feature matrix
tfidf = TfidfVectorizer()
feature_matrix = tfidf.fit_transform(text_data)
# Create a search query and transform it into a tf-idf vector
text = "Brazil is the best"
vector = tfidf.transform([text])
# Calculate the cosine similarities between the input vector and all other
vectors
cosine_similarities = linear_kernel(vector, feature_matrix).flatten()
# Get the index of the most relevent items in order
related_doc_indicies = cosine_similarities.argsort()[:-10:-1]
# Print the most similar texts to the search query along with the cosine
similarity
print([(text_data[i], cosine_similarities[i]) for i in related_doc_indicies])
[
(
'Sweden is best', 0.6666666666666666),
('I love Brazil. Brazil!', 0.5163977794943222),
('Germany beats both', 0.0
)
]
讨论
文本向量对于诸如搜索引擎之类的 NLP 用例非常有用。计算了一组句子或文档的 tf-idf 向量后,我们可以使用相同的 tfidf 对象来向量化未来的文本集。然后,我们可以计算输入向量与其他向量矩阵之间的余弦相似度,并按最相关的文档进行排序。
余弦相似度的取值范围为[0, 1.0],其中 0 表示最不相似,1 表示最相似。由于我们使用tf-idf向量来计算向量之间的相似度,单词出现的频率也被考虑在内。然而,在一个小的语料库(文档集合)中,即使是“频繁”出现的词语也可能不频繁出现。在这个例子中,“瑞典是最好的”是最相关的文本,与我们的搜索查询“巴西是最好的”最相似。由于查询提到了巴西,我们可能期望“我爱巴西。巴西!”是最相关的;然而,由于“是”和“最好”,“瑞典是最好的”是最相似的。随着我们向语料库中添加的文档数量的增加,不重要的词语将被加权较少,对余弦相似度计算的影响也将减小。
参见
6.12 使用情感分析分类器
问题
您希望对一些文本的情感进行分类,以便作为特征或在下游数据分析中使用。
解决方案
使用transformers库的情感分类器。
# Import libraries
from transformers import pipeline
# Create an NLP pipeline that runs sentiment analysis
classifier = pipeline("sentiment-analysis")
# Classify some text
# (this may download some data and models the first time you run it)
sentiment_1 = classifier("I hate machine learning! It's the absolute worst.")
sentiment_2 = classifier(
"Machine learning is the absolute"
"bees knees I love it so much!"
)
# Print sentiment output
print(sentiment_1, sentiment_2)
[
{
'label': 'NEGATIVE',
'score': 0.9998020529747009
}
]
[
{
'label': 'POSITIVE',
'score': 0.9990628957748413
}
]
讨论
transformers库是一个极为流行的自然语言处理任务库,包含许多易于使用的 API,用于训练模型或使用预训练模型。我们将在第二十二章更详细地讨论 NLP 和这个库,但这个例子作为使用预训练分类器在您的机器学习流水线中生成特征、分类文本或分析非结构化数据的强大工具的高级介绍。