这个分析笔记由Jake Vanderplas编辑汇总。 源代码和license文件在GitHub。 中文翻译由派兰数据派兰大数据分析平台上完成。 源代码在GitHub上。

机器学习的基本原理

现在我们将介绍机器学习的基本原理,以及怎么通过 Scikit-Learn 的API去实现这些算法。

在简单的介绍scikit-learn的Estimator对象后,我们将介绍监督学习,包括分类问题和回归问题。我们还将介绍无监督学习,包括降维聚类问题。


In [1]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

# 设置 seaborn 绘图的默认参数
# 可以安全地注释掉(即不用seaborn)
import seaborn; seaborn.set()

Scikit-learn 中的 Estimator 对象

每一个在scikit-learn中实现的算法都是表示为一个''Estimator''的对象。比如,一个线性回归的算法如下这样实现的:


In [2]:
from sklearn.linear_model import LinearRegression

Estimator 参数:一个 Estimator 对象的所有系数可以在它初始化的时候设置,这些系数拥有普适性的初始值。


In [3]:
model = LinearRegression(normalize=True)
print(model.normalize)


True

In [4]:
print(model)


LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=True)

Estimator 模型参数:当Estimator用数据来拟合系数时,模型的系数是根据数据一步步得到的。所有的模型系数都是Estimator对象的属性,并且以一个下划线(underscore)结尾。


In [5]:
x = np.arange(10)
y = 2 * x + 1

In [6]:
print(x)
print(y)


[0 1 2 3 4 5 6 7 8 9]
[ 1  3  5  7  9 11 13 15 17 19]

In [7]:
plt.plot(x, y, 'o');



In [8]:
# sklearn 的输入数据要求是2维的: (samples == 10 x features == 1)
X = x[:, np.newaxis]
print(X)
print(y)


[[0]
 [1]
 [2]
 [3]
 [4]
 [5]
 [6]
 [7]
 [8]
 [9]]
[ 1  3  5  7  9 11 13 15 17 19]

In [9]:
# 用数据来拟合系数(fit the model)
model.fit(X, y)


Out[9]:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=True)

In [10]:
# 以下划线结尾,代表了一个拟合的系数
print(model.coef_)
print(model.intercept_)


[ 2.]
1.0

In [11]:
# 剩余误差(残差)
model.residues_


/Users/linyong/anaconda/envs/pydata/lib/python3.6/site-packages/sklearn/utils/deprecation.py:70: DeprecationWarning: Function residues_ is deprecated; ``residues_`` is deprecated and will be removed in 0.19
  warnings.warn(msg, category=DeprecationWarning)
Out[11]:
3.2540512340366737e-30

这个模型按照我们所预期的,找出了一条斜率为2,截距为1的线。

监督学习:分类和回归

监督学习中,我们会有一个同时包含特征(feature)和标签(label)的数据集。我们的任务是,对于给定的对象特征,去建立一个Estimator预测对该对象的标签。一个相对简单的例子是,我们给出对于iris这种花的测量,去预测iris花的具体品种。这是一个相对简单的任务,更多的较为复杂的任务见下方:

  • 给出一个通过望远镜得到的彩色图片,预测出该物体是一个恒星,一个类星体或者一个星系。
  • 给出一个人的照片,识别出这个人的信息。
  • 给出一个人看过的电影,和这些电影的得分,推荐给这个人他可能喜欢的其他电影(也就是:推荐系统: 一个非常著名的例子可以参考 Netflix Prize)。

这些任务的共同特点就是,我们需要利用事物观测到的各种特征,去预测一个或多个未知的,与这个事物有关的其他特征。

进一步的,监督学习可以被划分成两个大类,即分类(classification)和回归(regression)。在分类问题中,对象的标签是离散的,在回归问题中,对象的标签是连续的。举个例子,在天文学中,判断一个物体是一个恒星,一个类星体或者是一个星系是一个分类的问题:这个三种标签来自于三个独立的类别。另一方面,我们可能希望去根据这些观测,预测物体的年龄,这就是一个回归的问题了,因为这个标签(年龄)是一个连续的数字。

分类问题的例子

K个最近邻(kNN)算法是最简单的机器学习算法之一:给出新的、未知的观测结果,在参考数据库中找出与这些特征最接近的分类,然后把这个对象归入最主要的分类中。

我们在iris分类问题上试一试我们这个算法:


In [12]:
from sklearn import neighbors, datasets

iris = datasets.load_iris()
X, y = iris.data, iris.target

# 建立模型
knn = neighbors.KNeighborsClassifier(n_neighbors=5)

# 训练模型 (fit the model)
knn.fit(X, y)

# 一个 3cm x 5cm 萼片 and 4cm x 2cm 花瓣 的 iris 花,属于哪一个分类?
# 调用"predict"方法:
result = knn.predict([[3, 5, 4, 2],])

print(iris.target_names[result])


['versicolor']

您还可以去做分类概率预测:


In [13]:
knn.predict_proba([[3, 5, 4, 2],])


Out[13]:
array([[ 0. ,  0.8,  0.2]])

In [14]:
from fig_code import plot_iris_knn
plot_iris_knn()



练习

对上述的问题,用一个不同的Esitimator: sklearn.svm.SVC.

请注意你并不需要知道Estimator内部的实现细节,我们在这里只是试着使用这些接口

如果你很快就完成了,可以尝试去用 SVC estimator 去画一副类似的示意图。


In [15]:
from sklearn.svm import SVC

回归算法的例子

回归算法的最简单的例子之一就是和我们之前看到的一样,根据给出的点去拟合一条线。 Scikit-learn 也包含很多更复杂的回归算法。


In [16]:
# 创建一些简单的数据
import numpy as np

np.random.seed(0)
X = np.random.random(size=(20, 1))
y = 3 * X.squeeze() + 2 + np.random.randn(20)

plt.plot(X.squeeze(), y, 'o');


像上面说的一样,我们还画出一条最佳的拟合曲线:


In [17]:
model = LinearRegression()
model.fit(X, y)

# 画出数据和模型预测之间的图形
X_fit = np.linspace(0, 1, 100)[:, np.newaxis]
y_fit = model.predict(X_fit)

plt.plot(X.squeeze(), y, 'o')
plt.plot(X_fit.squeeze(), y_fit);


scikit-learn 还有一些更加复杂的算法,可以根据模型中的特征进行更好的拟合。


In [18]:
# 训练一个随机森林(Random Forest)模型
from sklearn.ensemble import RandomForestRegressor

model = RandomForestRegressor()
model.fit(X, y)

# 画出数据和模型预测之间的图形
X_fit = np.linspace(0, 1, 100)[:, np.newaxis]
y_fit = model.predict(X_fit)

plt.plot(X.squeeze(), y, 'o')
plt.plot(X_fit.squeeze(), y_fit);


这些是不是一个好的拟合取决于很多因素;我们之后在教程中会去讨论如何去选择一个模型的细节。


练习

通过Ipython的帮助,探索RandomForestRegressor对象(比如在对象加一个问号)。 RandomForestRegressor的参数有哪些? 如果您改变这些参数的话,对上面的图片会有哪些影响?

这些类参数就是我们所说的 hyperparameters, 我们之后会在验证一节中讨论如何选择这些 hyperparameters。


无监督学习:降维(Dimensionality Reduction)和聚类(Clustering)

无监督学习从问题的另一个角度去考虑。数据在这里是没有标签的,我们感兴趣的是问题中对象的相似点。从某种程度上说,您可以认为无监督学习是一种从数据本身去发掘标签的方法。无监督学习有降维(dimensionality reduction) 、聚类(clustering)和密度估计(density estimation)等算法。举个例子,在我们之前所说的iris数据中,我们可以利用无监督学习去发现哪一种测量的组合最能体现数据的结构。我们接下来会看到,这样一种数据的投影能够被用来在二维空间中展现四维的数据集。还有一些更贴切的无监督学习的问题:

  • 给出遥远星系的详细的观测数据,决定哪一个或哪几个特征是最能体现其数据的

  • 给出两种声源的混合(比如一个人在音乐声中谈话),分离出两种声音(这叫做 盲源分离

  • 给出一段视频,分离出其中运动的物体,并且与其他运动物体比较,给出分类

有些时候监督学习和无监督学习会组合在一起:无监督学习可以用来从有多个类别的数据中找到有用的特征,而获取的这些特征可以被用在一个监督学习的框架中。

降维:PCA

主成分分析(Principle Component Analysis, PCA)是一个降维算法,它可以找到能达到最大方差的最佳变量的组合。

考虑iris数据集。因为它有4种特征,所以并不可以在一个简单的2D图中显示出来。我们即将从中抓取出萼片和花瓣特征的组合去显示它们:


In [19]:
X, y = iris.data, iris.target

from sklearn.decomposition import PCA

pca = PCA(n_components=0.95)
pca.fit(X)
X_reduced = pca.transform(X)
print("Reduced dataset shape:", X_reduced.shape)


Reduced dataset shape: (150, 2)

In [20]:
import matplotlib.pyplot as plt

plt.scatter(X_reduced[:, 0], X_reduced[:, 1], 
            c=y, cmap='RdYlBu')

print("Meaning of the 2 components:")
for component in pca.components_:
    print(" + ".join("%.3f x %s" % (value, name)
                     for value, name in zip(component,
                                            iris.feature_names)))


Meaning of the 2 components:
0.362 x sepal length (cm) + -0.082 x sepal width (cm) + 0.857 x petal length (cm) + 0.359 x petal width (cm)
0.657 x sepal length (cm) + 0.730 x sepal width (cm) + -0.176 x petal length (cm) + -0.075 x petal width (cm)

聚类:K-means

聚类算法针对观测到的数据,根据给定的准则发现它们的共同点,在数据集中寻找“群”。

注意,这些聚类(cluster)会从数据中发现一些隐藏的相关结构如果所使用到的标准展现出了这些结构 (Note that these clusters will uncover relevent hidden structure of the data only if the criterion used highlights it.)


In [21]:
from sklearn.cluster import KMeans

k_means = KMeans(n_clusters=3, random_state=0) # Fixing the RNG in kmeans
k_means.fit(X)
y_pred = k_means.predict(X)

plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=y_pred,
           cmap='RdYlBu');


总结: Scikit-learn 的 Estimator 接口

Scikit-learn 致力于为所有的机器学习方法创造一个统一的接口,我们将会在下面的看到这些例子。对于一个叫model的scikit-learn的Estimator对象,我们有下面这些方法:

  • 适用所有Estimators
    • model.fit() : 用训练数据集来训练算法。 对监督学习,这个方法接受两个参数: 特征向量 X和对应的标签 y (例如: model.fit(X, y))。 对于无监督学习,这个方法只接受一个参数,即特征向量 X (例如: model.fit(X))。
  • 适用 监督学习(supervised)的 方法
    • model.predict() : 给定一个训练模型,预测一个新的数据集的返回值。 这个方法接受一个参数,即新的数据集 X_new (例如: model.predict(X_new)), 方法返回特征向量中每一组值预测返回的结果。
    • model.predict_proba() : 对于分类(classification)问题,一些模型提供了这个方法,用于返回给定一个新的数值可能返回不同分类的概率。 在这种情况下,model.predict() 返回概率最高的分类值。
    • model.score() : 对分类(classification)或者回归(regression)问题,绝大多数的模型都实现了这个得分(score) 方法。 得分数值介于 0 和 1 之间,数值越大说明模型训练的越好。
  • 适用无监督Estimators
    • model.predict(): 在聚类算法中,预测数据的标签。
      • model.transform() : 给定一个无监督模型,将一组新数据变换(transform)为新的基准(basis)。这个方法接受一个参数 X_new,将这组新数据根据无监督模型进行数据变换并返回。
    • model.fit_transform(): 有一些estimators实现这个方法,它更有效的训练数据并根据模型进行数据转换。

模型验证

机器学习的一个重要部分就是模型验证: 就是得出你的训练模型的优劣,即从训练数据中去预测没有被标签的数据的性能和准确性。我们看一个采用最近邻分类器的例子。这是一个非常简单的分类器:它简单的存储了所有的训练数据,对于任何未知的数据的标签,仅仅采用与之最近的一个数据标签。

对于iris数据,它非常简便的返回了正确的预测结果:


In [22]:
from sklearn.neighbors import KNeighborsClassifier

X, y = iris.data, iris.target
clf = KNeighborsClassifier(n_neighbors=1)
clf.fit(X, y)
y_pred = clf.predict(X)
print(np.all(y == y_pred))


True

一个查看数据更有用的方法是查看混淆矩阵,或者是一个可以显示输入和输出频率的矩阵:


In [23]:
from sklearn.metrics import confusion_matrix

print(confusion_matrix(y, y_pred))


[[50  0  0]
 [ 0 50  0]
 [ 0  0 50]]

50个训练的数据都被正确的分类了。但是这并不代表我们的模型就是完美的!特别的,这个模型应用于新的数据时会效果会很糟糕。我们可以把数据集划分成训练集(training set)和测试集(testing set),并再次进行训练和测试。 Scikit-learn 拥有方便的划分流程:


In [24]:
# from sklearn.cross_validation import train_test_split
# 从 v0.18 起,使用 sklearn.model_selection 中 train_test_split
from sklearn.model_selection import train_test_split

Xtrain, Xtest, ytrain, ytest = train_test_split(X, y)
clf.fit(Xtrain, ytrain)
ypred = clf.predict(Xtest)
print(confusion_matrix(ytest, ypred))


[[12  0  0]
 [ 0  6  0]
 [ 0  2 18]]

上面真是的反映了我们的分类器的性能:显然对于第二类和第三类出现了一点混淆,我们也许可以通过上面的数据看出这一点。

这就是为什么将模型划分为训练集和测试集非常重要。我们将会在后续的教程中深入讨论模型的验证。

流程图:如何选择Estimator

下面是由sckit-learn的最重要的贡献者 Andreas Mueller 提供的一张图。它给出了什么情况下应该应用什么算法的总结。保存它吧!


In [25]:
from IPython.display import Image
Image("http://scikit-learn.org/stable/_static/ml_map.png", width=800)


Out[25]:

快速案例:光学字符识别

为了在一个更有趣的问题中实践我们的这些准则,让我们考虑光学字符识别问题——也就是,识别手写数字。 从更广的层面来说,这个问题包含了数字定位技术和识别数字的技术。在这里我们会走一个捷径,scikit-learn的库中已经包含了已经预处理好的数字。

加载并显示数字

我们用scikit-learn的数据获取接口去看一下数字数据规模。


In [26]:
from sklearn import datasets
digits = datasets.load_digits()
digits.images.shape


Out[26]:
(1797, 8, 8)

我们打印出其中的一部分:


In [27]:
fig, axes = plt.subplots(10, 10, figsize=(8, 8))
fig.subplots_adjust(hspace=0.1, wspace=0.1)

for i, ax in enumerate(axes.flat):
    ax.imshow(digits.images[i], cmap='binary', interpolation='nearest')
    ax.text(0.05, 0.05, str(digits.target[i]),
            transform=ax.transAxes, color='green')
    ax.set_xticks([])
    ax.set_yticks([])



In [28]:
# 这些图像的数字
print(digits.images.shape)
print(digits.images[0])


(1797, 8, 8)
[[  0.   0.   5.  13.   9.   1.   0.   0.]
 [  0.   0.  13.  15.  10.  15.   5.   0.]
 [  0.   3.  15.   2.   0.  11.   8.   0.]
 [  0.   4.  12.   0.   0.   8.   8.   0.]
 [  0.   5.   8.   0.   0.   9.   8.   0.]
 [  0.   4.  11.   0.   1.  12.   7.   0.]
 [  0.   2.  14.   5.  10.  12.   0.   0.]
 [  0.   0.   6.  13.  10.   0.   0.   0.]]

In [29]:
# 对应的标签(label)
print(digits.target)


[0 1 2 ..., 8 9 8]

所以我们有了1797个样本,其中每个样本有64个维度。

无监督学习:降维

我们想在64维空间中去查看我们的数据,但是在64维空间中画图是一件无法想象的事情! 我们可以使用无监督学习的方法将维度减少到2。在这里我们采用一种功能强大的机器学习算法 Isomap , 将这些数据转换成两维。


In [30]:
from sklearn.manifold import Isomap

In [31]:
iso = Isomap(n_components=2)
data_projected = iso.fit_transform(digits.data)

In [32]:
data_projected.shape


Out[32]:
(1797, 2)

In [33]:
plt.scatter(data_projected[:, 0], data_projected[:, 1], c=digits.target,
            edgecolor='none', alpha=0.5, cmap=plt.cm.get_cmap('nipy_spectral', 10));
plt.colorbar(label='digit label', ticks=range(10))
plt.clim(-0.5, 9.5)


我们在这里可以看到,这些数字在参数域被非常好的分隔开了。这说明了我们的监督学习分类算法也应当取得非常好的结果。让我们试试看。

数字分类

让我们在这些数字上尝试我们的分类工作。我们首先需要做的是将数字分离成训练集和测试集:


In [34]:
# 从 sklearn.cross_validation 中引入 train_test_split
from sklearn.model_selection import train_test_split

Xtrain, Xtest, ytrain, ytest = train_test_split(digits.data, digits.target,
                                                random_state=2)
print(Xtrain.shape, Xtest.shape)


(1347, 64) (450, 64)

我们用一个简单的逻辑回归算法(logistic regression,尽管名称中含有'回归',但它是一个分类算法)去完成分类:


In [35]:
from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(penalty='l2')
clf.fit(Xtrain, ytrain)
ypred = clf.predict(Xtest)

我们可以通过比较预测结果和真实结果来得到我们的分类准确率:


In [36]:
from sklearn.metrics import accuracy_score
accuracy_score(ytest, ypred)


Out[36]:
0.94666666666666666

单纯的数字没有告诉我们哪里出错了,一个比较推荐的方法是使用混淆矩阵。


In [37]:
from sklearn.metrics import confusion_matrix
print(confusion_matrix(ytest, ypred))


[[42  0  0  0  0  0  0  0  0  0]
 [ 0 45  0  1  0  0  0  0  3  1]
 [ 0  0 47  0  0  0  0  0  0  0]
 [ 0  0  0 42  0  2  0  3  1  0]
 [ 0  2  0  0 36  0  0  0  1  1]
 [ 0  0  0  0  0 52  0  0  0  0]
 [ 0  0  0  0  0  0 42  0  1  0]
 [ 0  0  0  0  0  0  0 48  1  0]
 [ 0  2  0  0  0  0  0  0 38  0]
 [ 0  0  0  1  0  1  0  1  2 34]]

In [38]:
plt.imshow(np.log(confusion_matrix(ytest, ypred)),
           cmap='Blues', interpolation='nearest')
plt.grid(False)
plt.ylabel('true')
plt.xlabel('predicted');


/Users/linyong/anaconda/envs/pydata/lib/python3.6/site-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in log
  """Entry point for launching an IPython kernel.

我们可以查看我们的输出和其对应的预测的数字。我们会将预测错误的标签置红:


In [39]:
fig, axes = plt.subplots(10, 10, figsize=(8, 8))
fig.subplots_adjust(hspace=0.1, wspace=0.1)

for i, ax in enumerate(axes.flat):
    ax.imshow(Xtest[i].reshape(8, 8), cmap='binary')
    ax.text(0.05, 0.05, str(ypred[i]),
            transform=ax.transAxes,
            color='green' if (ytest[i] == ypred[i]) else 'red')
    ax.set_xticks([])
    ax.set_yticks([])


一个有趣的现象是,即使我们采用的是最简单的逻辑回归算法,很多它预测错误的图片我们自己也很有可能预测错误!

我们有很多方法去提高这个分类器,但是我们现在没有时间了。我们可以通过使用更成熟的模型,运用交叉验证或者采用其他技术去提高这个分类器。 我们会在之后的教程中详细的说明这些。