跳至主要内容

数据科学中的预处理(第一部分):居中、缩放与 KNN

本文将通过研究居中与缩放如何提升模型表现,解释预处理在机器学习流程中的重要性。
· 11分钟 阅读

使用 AI 探索

数据预处理是一个总括性术语,涵盖了数据科学家为使数据更适合其目标任务而进行的一系列操作。举例来说,在对 Twitter 数据做情感分析之前,您可能需要去除所有 HTML 标签、空白字符、展开缩写,并将推文拆分为其包含的词列表。处理空间数据时,您可能会对其进行缩放,使其与单位无关,也就是说无论原始测量是以英里还是厘米为单位,算法都不受影响。然而,数据预处理并非孤立进行。换言之,预处理是达成目标的手段,并无一成不变的规则:如我们将看到的,有一些常见做法,您也可以逐步形成对何种方法有效的直觉,但归根结底,预处理通常是以结果为导向的流程的一部分,其效果需要在具体情境中评估。

本文将以数值数据的缩放为例(数值数据:由数字组成的数据,与类别/字符串相对;缩放:用基本算术改变数据取值范围;后文详述),展示为何应将预处理视作更大结构——机器学习(ML)流水线——的一部分。为此,我们将看到一个现实案例,说明缩放如何提升模型表现。

我将先介绍机器学习中的分类问题,以及其中一种最简单的算法——k 近邻(k-Nearest Neighbors,k-NN)。为体会在此类场景中缩放数值数据的重要性,我还会介绍模型性能度量,以及训练集与测试集的概念。您将结合一个数据集看到这些概念与实践:我们尝试对红酒的品质进行分类。同时,我会把预处理放在最有用的位置——迭代式数据科学流水线的开端附近。文中所有示例均使用 Python。若您不熟悉 Python,可在此处查看我们的 DataCamp 课程。我将使用 pandas 处理数据框,使用 scikit-learn 进行机器学习。

本文是三部分教程的第一部分:

第二部分:居中、缩放与逻辑回归

第三部分:合成数据的缩放

机器学习中分类问题的简要介绍

对现实世界事物进行分类与标注是一门古老的技艺。公元前 4 世纪,亚里士多德构建了一个生物分类体系,沿用达 2000 年之久。在现代,分类通常被表述为机器学习任务,具体来说,是一种监督学习任务。监督学习的基本原则很直接:我们有一堆由预测变量与一个目标变量组成的数据。监督学习的目标,是在给定预测变量的情况下,构建一个“擅长”预测目标变量的模型。若目标变量由类别构成(例如“点击”或“未点击”,“恶性”或“良性”肿瘤),我们称该学习任务为分类。相反,若目标是连续变化的变量(例如房价),则是回归任务。

一个示例有助于理解:考虑心脏病数据集,其中包含 75 个预测变量,如“年龄”“性别”“是否吸烟”等,目标变量表示是否患有心脏病,取值范围为 0(无心脏病)到 4。关于该数据集的许多研究都集中在区分是否患病上。这是一个分类任务。若您尝试预测目标变量的实际取值(0 到 4),则是回归问题(因为目标变量是有序的)。我将在下一篇讨论回归。这里我将考察分类任务中最简单的算法之一:k 近邻算法。

机器学习中的 k 近邻分类

假设我们有带标签的数据,例如包含红酒特征的数据(如酒精含量、密度、柠檬酸含量、pH 等;这些是预测变量),目标变量为“质量”,标签为“好”“差”。那么,给定一款新的、未标注的红酒的特征,分类任务就是预测其“质量”。当所有预测变量都是数值型(类别型的情形也有对应方法)时,我们可以将每一行/每一款酒视为 n 维空间中的一个点。在这种情况下,k 近邻(k-NN)是一个概念上与计算上都很简单的分类方法:对于每一款新的未标注红酒,我们选定某个整数 k,在 n 维预测变量空间中计算其 k 个最近邻。然后查看这 k 个邻居的标签(即“好”或“差”),并将出现次数最多的标签赋给新红酒(例如 k = 5,若 3 个邻居投“好”、2 个投“差”,则模型将该酒标为“好”)。需要注意的是,这里训练模型仅仅是存储数据点:没有需要拟合的参数!

k 近邻的可视化描述

下图展示了二维情形下的 k-NN:如何对中间的数据点分类?如果 k=3,您会将其分类为红色;如果 k=5,则为绿色。

 
二维中的 k-NN 示例

用 Python(scikit-learn)实现 k-NN

现在来看看一个 k-NN 的实际示例。为此,我们将使用红酒质量数据集:将其导入 pandas 数据框,然后为各预测变量绘制直方图以初步了解数据。
import pandas as pd%matplotlib inlineimport matplotlib.pyplot as pltplt.style.use('ggplot')df = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv ' , sep = ';')X = df.drop('quality' , 1).values # drop target variabley1 = df['quality'].valuespd.DataFrame.hist(df, figsize = [15,15]);
柱状图

首先注意各预测变量的取值范围:“游离二氧化硫”的范围从 0 到约 70,而“挥发性酸度”从约 0 到约 1.2。更具体地说,前者的范围比后者大两个数量级。任何关注数据点间距离的算法(例如 k-NN),都可能因此把注意力集中(且不公平地)放在取值范围更大的变量上,比如“游离二氧化硫”,而该变量很可能只是噪声。由此我们有了对数据进行缩放的动机,稍后将会进行。

目标变量是红酒的“质量”评分,范围为 3 到 8。为便于讲解,让我们把它转为二分类变量:“好”(评分 > 5)与“差”(评分 <= 5)。我们还将分别绘制目标变量原始形式与聚合形式的直方图,以便把握整体情况。

y = y1 <= 5 # is the rating <= 5?# plot histograms of original target variable# and aggregated target variableplt.figure(figsize=(20,5));plt.subplot(1, 2, 1 );plt.hist(y1);plt.xlabel('original target value')plt.ylabel('count')plt.subplot(1, 2, 2);plt.hist(y)plt.xlabel('aggregated target value')plt.show()
柱状图
现在我们几乎可以进行 k 近邻了。不过在比较有无预处理时模型的表现之前,我们需要先弄清如何衡量模型的“好坏”:

k 近邻:表现如何?

分类任务有多种性能度量。至关重要的一点是要认识到,度量的选择高度依赖于领域与问题本身。对于类别较为平衡的数据集(所有目标值大致同样常见),数据科学家通常会使用准确率作为性能度量。事实上,如我们将看到的,准确率是 scikit-learn 中 k 近邻与逻辑回归的默认评分方法。那么什么是准确率?它只是正确预测的数量除以预测总数:
$$\text{Accuracy}=\frac{\text{Number of Correct Predictions}}{\text{Total Number of Predictions}}.$$

注:也可通过混淆矩阵来定义准确率,在二分类问题中常以真正例与假负例来表述;从混淆矩阵还能导出其他常见的模型性能度量,例如精确率(真正例数除以真阳性与假阳性之和)与召回率(真正例数除以真正例与假负例之和);另一个度量F1 分数是精确率与召回率的调和平均。参见 machine learning mastery 对这些度量的清晰讲解;也可查看维基百科关于混淆矩阵与F1 分数的条目。

k 近邻:实践中的表现与训练-测试切分

拥有诸如准确率这样的性能度量固然很好,但如果我们用所有数据来拟合模型,那么该从哪个数据集上报告准确率呢?请记住,我们希望模型能很好地泛化到新数据。因此,若我们在数据集D上训练模型,而在同一数据集D上报告准确率,结果可能会让模型看起来比实际表现更好。这被称为过拟合。为解决此问题,数据科学家通常会在数据的一个子集(称为训练集)上训练模型,并在其余数据(测试集)上评估性能。我们这里正要这样做!一个通用经验法则是大约 80% 数据用于训练,20% 用于测试。现在让我们切分红酒质量数据:
from sklearn.cross_validation import train_test_splitX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)We now build the k-NN model, make predictions on the test set and compare these predictions to the ground truth in order to get a measure of model performance:from sklearn import neighbors, linear_modelknn = neighbors.KNeighborsClassifier(n_neighbors = 5)knn_model_1 = knn.fit(X_train, y_train)print('k-NN accuracy for test set: %f' % knn_model_1.score(X_test, y_test))k-NN accuracy for test set: 0.612500

值得再次强调的是,scikit-learn 中 k-NN 的默认评分方法是
准确率。61% 的准确率并不出色,但对于未经任何预处理的开箱即用模型来说也不算糟。要查看更多其他指标,我们还可以使用 scikit-learn 的分类报告:

from sklearn.metrics import classification_reporty_true, y_pred = y_test, knn_model_1.predict(X_test)print(classification_report(y_true, y_pred))
 
             precision    recall  f1-score   support      False       0.66      0.64      0.65       179       True       0.56      0.57      0.57       141avg / total       0.61      0.61      0.61       320

现在我们将引入缩放与居中,这是对数值数据最基础的预处理方法,看看它们是否以及如何影响模型表现。

预处理机制:缩放与居中

在对数据运行模型(如回归:预测连续变量;或分类:预测离散变量)之前,几乎总需要做一些预处理。对于数值变量,常见做法是对数据进行归一化或标准化。这些术语是什么意思?

归一化的含义是将数据集缩放到最小值为 0、最大值为 1。为此,我们将每个数据点 x 变换为

$$x_{normalized} = \frac{x-x_{min}}{x_{max}-x_{min}}.$$

标准化略有不同;其作用是围绕 0 对数据进行居中,并按标准差进行缩放:

$$x_{standardized} = \frac{x-\mu}{\sigma},$$

其中 $\mu$ 和 $\sigma$ 分别为数据集的均值与标准差。首先要注意,这些变换只改变数据的取值范围,而不改变其分布。您可能还会进一步使用其他变换,如对数变换或 Box-Cox 变换,使数据更接近高斯分布(钟形曲线)。但在继续之前,重要的是要问:为什么要缩放数据?在什么情况下更合适?例如,在分类问题中是否比在回归中更重要?

我们先深入分类问题,看看缩放如何影响 k 近邻的表现:

预处理:实践中的缩放

下面我将(i)对数据进行缩放,(ii)使用 k 近邻,(iii)检查模型表现。我将使用 scikit-learn 的scale 函数,它会对传入数组的所有特征(列)进行标准化。

from sklearn.preprocessing import scaleXs = scale(X)from sklearn.cross_validation import train_test_splitXs_train, Xs_test, y_train, y_test = train_test_split(Xs, y, test_size=0.2, random_state=42)knn_model_2 = knn.fit(Xs_train, y_train)print('k-NN score for test set: %f' % knn_model_2.score(Xs_test, y_test))print('k-NN score for training set: %f' % knn_model_2.score(Xs_train, y_train))y_true, y_pred = y_test, knn_model_2.predict(Xs_test)print(classification_report(y_true, y_pred))
 
 k-NN score for test set: 0.712500k-NN score for training set: 0.814699             precision    recall  f1-score   support      False       0.72      0.79      0.75       179       True       0.70      0.62      0.65       141avg / total       0.71      0.71      0.71       320

所有这些指标都提升了 0.1,约合 16% 的提升,且意义明显!正如上文所提,缩放前有许多预测变量的数值范围相差数个数量级,这意味着在 k-NN 等算法的语境中,一两个变量可能会占据主导。缩放数据的两个主要原因是:

  1. 预测变量的取值范围可能差异显著,在某些情况下(如实现 k-NN)需要缓解这种差异,以避免某些特征主导算法;
  2. 希望特征与单位无关,即不依赖于测量尺度:例如,您用米表示某一特征,而我用厘米表示同一特征。如果我们都对各自数据做缩放,该特征对于我们双方而言将保持一致。

我们已经看到,预处理(以缩放与居中为代表)在数据科学流水线中占据着关键位置,而我们这样做是为了倡导一种整体化的机器学习思路。后续文章中,我希望将讨论扩展到其他类型的预处理,例如数值数据的变换与类别数据的预处理——它们都是数据科学家工具箱中的要件。在此之前,下一篇我将探讨缩放在基于回归的方法处理分类问题时的作用。具体而言,我会考察逻辑回归,您将看到其结果与刚才在 k 近邻背景下看到的非常不同。

在下方的交互窗口中,您可以自行尝试。先修改变量 n_neig,即 k-NN 算法使用的邻居数量。您也可以通过将 sc = True 来对数据进行缩放。然后运行整个脚本,查看模型的准确率与分类报告。

import pandas as pdimport matplotlib.pyplot as pltfrom sklearn.cross_validation import train_test_splitfrom sklearn import neighborsfrom sklearn.preprocessing import scalefrom sklearn.metrics import classification_report # Set the the number of neighbors for k-NNn_neig = 5# Set sc = True if you want to scale your featuressc = False# Load datadf = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv ' , sep = ';')X = df.drop('quality' , 1).values# drop target variable# Here we scale, if desiredif sc == True: X = scale(X)# Target valuey1 = df['quality'].values# original target variabley = y1 <= 5# new target variable: is the rating <= 5?# Split the data into a test set and a training setX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)# Train k-NN model and print performance on the test setknn = neighbors.KNeighborsClassifier(n_neighbors = n_neig)knn_model = knn.fit(X_train, y_train)y_true, y_pred = y_test, knn_model.predict(X_test)print('k-NN accuracy for test set: %f' % knn_model.score(X_test, y_test))print(classification_report(y_true, y_pred))

术语表

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

分类任务:若监督学习中的目标变量由类别构成(例如“点击/未点击”“恶性/良性”肿瘤),则该任务为分类任务。

回归任务:若监督学习中的目标变量是连续变化变量(如房价)或有序类别变量(如“红酒质量评分”),则该任务为回归任务。

k 近邻:一种用于分类任务的算法,将数据点赋予其 k 个最近邻多数表决所决定的标签。

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

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

本文由 Jupyter 笔记本生成。您可以在此处下载该笔记本。

主题