跳至主要内容

数据科学中的预处理(第 3 部分):缩放合成数据

您可以对数据进行大量预处理,但检验标准在于结果:您的模型到底表现如何?
· 10分钟 阅读

使用 AI 探索

在前两篇文章中,我探讨了数据预处理在机器学习流水线中的作用。具体来说,我考察了k 近邻(k-NN)和逻辑回归算法,发现对数值数据进行缩放会显著影响前者的性能,但对后者影响不大,例如以准确率衡量(有关缩放、k-NN 及其他相关术语的定义,请参见下方术语表或前文)。这里真正要传达的信息是:预处理并不是脱离具体情境的,您可以对数据进行大量预处理,但结果要看实际效果:模型最终表现如何?

对数值数据进行缩放(即将某一变量的所有取值乘以一个常数以改变该变量的取值范围)有两个相关目的:i)如果您的度量单位是米而我的是英里,那么我们各自对数据进行缩放后,最终会处于相同尺度;ii)如果两个变量的取值范围差异很大,取值范围更大的变量可能会主导您的预测模型,即使它对目标变量的重要性不如取值范围更小的变量。我们看到,问题 ii)在 k-NN 中会出现,因为它明确基于样本之间的“接近程度”;而在逻辑回归中,训练时相关系数会收缩以弥补未缩放的影响,因此问题不明显。

此前使用的是真实世界数据,我们只能观察模型在缩放前后的表现差异。为了观察以“干扰变量”(对目标变量无影响但可能影响模型的变量)形式出现的噪声如何在缩放前后改变模型表现,这里我将合成一个数据集,以便精确控制干扰变量的性质。我们将看到:合成数据越嘈杂,缩放对 k-NN 就越重要。本文中的所有示例均使用 Python。如果您不熟悉 Python,可以在这里查看我们的 DataCamp 课程。我将使用 pandas 来处理 DataFrame,使用 scikit-learn 来完成机器学习任务。

在下面的代码块中,我们使用 scikit-learn 的 make_blobs 函数生成 2000 个数据点,分为 4 个簇(每个数据点包含 2 个特征变量和 1 个目标变量)。

# Generate some clustered data (blobs!)
import numpy as np
from sklearn.datasets.samples_generator import make_blobs
n_samples=2000
X, y = make_blobs(n_samples, centers=4, n_features=2,
                  random_state=0)

绘制合成数据

现在我们在平面上绘制合成数据。每个坐标轴代表一个特征变量,颜色对应目标变量:

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('ggplot')
plt.figure(figsize=(20,5));
plt.subplot(1, 2, 1 );
plt.scatter(X[:,0] , X[:,1],  c = y, alpha = 0.7);
plt.subplot(1, 2, 2);
plt.hist(y)
plt.show()

Scaling Synthesized Data

注:我们可以在第二幅图中看到,所有可能的目标类别都被均匀地代表。在这种情况下(或即使只是大致均衡),我们称 y 类是平衡的。

接下来我想绘制特征(预测变量)的直方图:

import pandas as pd
df = pd.DataFrame(X)
pd.DataFrame.hist(df, figsize=(20,5));

Scaling Synthesized Data

现在将数据划分为测试集和训练集,并绘制两者:

from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
plt.figure(figsize=(20,5));
plt.subplot(1, 2, 1 );
plt.title('training set')
plt.scatter(X_train[:,0] , X_train[:,1],  c = y_train, alpha = 0.7);
plt.subplot(1, 2, 2);
plt.scatter(X_test[:,0] , X_test[:,1],  c = y_test, alpha = 0.7);
plt.title('test set')
plt.show()

Scaling Synthesized Data

看起来不错!现在我们实例化一个 k 近邻投票分类器,并用训练集进行训练:

from sklearn import neighbors, linear_model
knn = neighbors.KNeighborsClassifier()
knn_model = knn.fit(X_train, y_train)

有了训练好的模型,我们可以在测试集上评估准确率:

print('k-NN score for test set: %f' % knn_model.score(X_test, y_test))
`k-NN score for test set: 0.935000`

我们也可以在训练集上重新评估准确率。通常预期模型在训练集上的表现会优于测试集:

print('k-NN score for training set: %f' % knn_model.score(X_train, y_train))
`k-NN score for training set: 0.941875`

值得重申的是,scikit-learn 中 k-NN 的默认评分方法是准确率。若要查看其他多种指标,我们还可以使用 scikit-learn 的分类报告:

from sklearn.metrics import classification_report
y_true, y_pred = y_test, knn_model.predict(X_test)
print(classification_report(y_true, y_pred))
                 precision    recall  f1-score   support
    
              0       0.87      0.90      0.88       106
              1       0.98      0.93      0.95       102
              2       0.90      0.92      0.91       100
              3       1.00      1.00      1.00        92
    
    avg / total       0.94      0.94      0.94       400

加入缩放后

现在我将对特征变量进行缩放,然后再次使用 k-NN:

from sklearn.preprocessing import scale
Xs = scale(X)
Xs_train, Xs_test, y_train, y_test = train_test_split(Xs, y, test_size=0.2, random_state=42)
plt.figure(figsize=(20,5));
plt.subplot(1, 2, 1 );
plt.scatter(Xs_train[:,0] , Xs_train[:,1],  c = y_train, alpha = 0.7);
plt.title('scaled training set')
plt.subplot(1, 2, 2);
plt.scatter(Xs_test[:,0] , Xs_test[:,1],  c = y_test, alpha = 0.7);
plt.title('scaled test set')
plt.show()

Scaling Synthesized Data

knn_model_s = knn.fit(Xs_train, y_train)
print('k-NN score for test set: %f' % knn_model_s.score(Xs_test, y_test))
`k-NN score for test set: 0.935000`

加入缩放后并没有更好!最可能的原因是两个特征本就处于相近的取值范围。当变量的范围差异很大时,再进行缩放才更有意义。为了直观看到这一点,我们将添加另一个特征。此外,这个特征与目标变量毫无关联:它只是噪声。

向信号中加入噪声:

我们添加第三个变量,为均值为 0、标准差为 \(\sigma\) 的高斯噪声。我们称 \(\sigma\) 为噪声强度,我们将看到噪声越强,k 近邻的表现越差。

# Add noise column to predictor variables
ns = 10**(3) # Strength of noise term
newcol = np.transpose([ns*np.random.randn(n_samples)])
Xn = np.concatenate((X, newcol), axis = 1)

接下来使用 mplot3d 包绘制三维数据:

from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(15,10));
ax = fig.add_subplot(111, projection='3d' , alpha = 0.5);
ax.scatter(Xn[:,0], Xn[:,1], Xn[:,2], c = y);

Scaling Synthesized Data

现在看看模型在新数据上的表现:

Xn_train, Xn_test, y_train, y_test = train_test_split(Xn, y, test_size=0.2, random_state=42)
knn = neighbors.KNeighborsClassifier()
knn_model = knn.fit(Xn_train, y_train)
print('k-NN score for test set: %f' % knn_model.score(Xn_test, y_test))

k-NN score for test set: 0.400000

这表现糟糕透了!我们来做缩放再看一下表现如何?

Xns = scale(Xn)
s = int(.2*n_samples)
Xns_train = Xns[s:]
y_train = y[s:]
Xns_test = Xns[:s]
y_test = y[:s]
knn = neighbors.KNeighborsClassifier()
knn_models = knn.fit(Xns_train, y_train)
print('k-NN score for test set: %f' % knn_models.score(Xns_test, y_test))
`k-NN score for test set: 0.907500`

很好,数据缩放后,模型表现几乎与未引入噪声时相当。现在我们考察模型表现如何随噪声强度而变化。

噪声越强,问题越大:

我们现在来观察噪声强度如何影响模型准确率。由于需要多次使用相同代码,不妨将主要部分封装在一个小函数中:

def accu( X, y):
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    knn = neighbors.KNeighborsClassifier()
    knn_model = knn.fit(X_train, y_train)
    return(knn_model.score(X_test, y_test))
noise = [10**i for i in np.arange(-1,6)]
A1 = np.zeros(len(noise))
A2 = np.zeros(len(noise))
count = 0
for ns in noise:
    newcol = np.transpose([ns*np.random.randn(n_samples)])
    Xn = np.concatenate((X, newcol), axis = 1)
    Xns = scale(Xn)
    A1[count] = accu( Xn, y)
    A2[count] = accu( Xns, y)
    count += 1

现在绘制准确率随噪声强度变化的曲线(注意对数坐标的 x 轴):

plt.scatter( noise, A1 )
plt.plot( noise, A1, label = 'unscaled', linewidth = 2)
plt.scatter( noise, A2 , c = 'r')
plt.plot( noise, A2 , label = 'scaled', linewidth = 2)
plt.xscale('log')
plt.xlabel('Noise strength')
plt.ylabel('Accuracy')
plt.legend(loc=3);

Scaling Synthesized Data

从上图可以看到,干扰变量中的噪声越大,对于 k-NN 模型而言,对数据进行缩放就越重要!下面,您将有机会对逻辑回归进行同样的实验。最后,我们看到,预处理(尤其是缩放与居中)在数据科学流水线中占据着基础地位,而我们之所以这样做,是为了以全局视角应对机器学习的挑战。今后的文章中,我希望将讨论扩展到其他类型的预处理,例如数值数据的变换以及分类数据的预处理——这些都是每位数据科学家工具箱中的关键环节。

给积极读者的练习:尝试将逻辑回归模型拟合到以上合成数据集,并查看模型表现。对于已缩放与未缩放的数据,准确率分别如何随噪声强度变化?您可以在下方的 DataCamp Light 小部件中完成!更改 10 的幂即可调整噪声大小(先尝试我在上文中用于 k-NN 的范围),若要对特征进行缩放,请设置sc = True。您也可以在 Github 上查看 DataCamp Light!

	# Below, change the exponent of 10 to alter the amount of noise
	ns = 10**(3) # Strength of noise term
	# Set sc = True if you want to scale your features
	sc = False
    
	#Import packages
	import numpy as np
	from sklearn.cross_validation import train_test_split
	from sklearn import neighbors, linear_model
	from sklearn.preprocessing import scale
	from sklearn.datasets.samples_generator import make_blobs
    
	#Generate some data
	n_samples=2000
	X, y = make_blobs(n_samples, centers=4, n_features=2,
                  random_state=0)

	# Add noise column to predictor variables
	newcol = np.transpose([ns*np.random.randn(n_samples)])
	Xn = np.concatenate((X, newcol), axis = 1)

	#Scale if desired
	if sc == True:
		Xn = scale(Xn)
    
	#Train model and test after splitting
	Xn_train, Xn_test, y_train, y_test = train_test_split(Xn, y, test_size=0.2, random_state=42)
	lr = linear_model.LogisticRegression()
	lr_model = lr.fit(Xn_train, y_train)
	print('logistic regression score for test set: %f' % lr_model.score(Xn_test, y_test))

术语表

监督学习:从特征变量推断目标变量的任务。例如,从“年龄”“性别”“是否吸烟”等特征变量推断“是否患心脏病”这一目标变量。

分类任务:如果目标变量由类别组成(例如“点击/未点击”“恶性/良性肿瘤”),那么该监督学习任务就是分类任务。

回归任务:如果目标变量为连续变化的变量(例如房价)或有序的类别变量(例如“葡萄酒品质评级”),那么该监督学习任务就是回归任务。

k 近邻:一种用于分类任务的算法,将数据点的标签设定为其 k 个最近邻居的多数投票结果。

预处理:数据科学家为使数据更适合后续任务而进行的各种操作。例如,在对推特数据进行情感分析之前,您可能需要去除任何 html 标签、空白字符,展开缩写,并将推文拆分为所含词语的列表。

居中与缩放:它们都是对数值数据进行预处理的方式,即由数字组成的数据(相对于类别或字符串等)。居中是指从每个数据点中减去该变量的均值,使新变量的均值为 0;缩放是指将每个数据点乘以一个常数以改变数据范围。有关其重要性及示例,请参见正文。

本文由 Jupyter 笔记本生成。您可在这里下载该笔记本。

主题