机器学习系列2-监督学习

  • Post author:
  • Post category:其他


监督学习是最常用也是最成功的机器学习类型之一。本节介绍几种常用的监督学习算法。

每当想要根据给定输入预测某个结果,并且还有输入/输出对的示例时,都应该使用监督学习。输入/输出对构成了训练集,用它来构建机器学习模型。通常需要人力来构建训练集。

1. 分类与回归

监督学习的问题主要有两种,

分类(classification)



回归(regression)

分类问题有时可分为

二分类(binary classification)



多分类(multiclass classification)

。二分类问题中,通常将其中一个类别称为正类(positive class),另一个类别称为反类(negative class)。“正”并不代表好的方面或者正数,而是代表研究对象。比如在寻找垃圾邮件时,“正”可能指的是垃圾邮件这一类别。将两个类别中的哪一个最为“正”,往往是主观判断,与具体的领域有关。多分类的例子是之前的鸢尾花分类,另外一个例子是根据网站上的文本预测网站所用的语言,这里的类别就是预定义的语言列表。

回归任务的目标是预测一个连续值,通常是浮点数。比如根据教育水平、年龄和居住地来预测一个人的年收入,预测值是一个金额,可以在给定范围内任意取值;另外一个例子是根据上一年的产量、天气和农场员工数等属性来预测农场的产量。

区分分类任务和回归任务的依据就是判断输出是否具有某种连续性。如果在可能的结果之间具有连续性,那么就是一个回归问题。预测收入一年40000与一年40001并没有实质区别。与此相反,语言之间不存在连续性,网站使用的要么是这张要么是那种语言,所以是一个分类问题。

2. 泛化、过拟合与欠拟合

监督学习中,如果一个模型能够对没见过的数据做出准备的预测,则认为模型能够从训练集

泛化(generalize)

到测试集,任务是构建一个泛化精度尽可能高的模型。

判断一个算法在新数据上表现好坏的唯一度量,就是在测试集上的评估。构建一个对现有信息量来说过于复杂的模型,被称为

过拟合(overfitting)

。通常在拟合模型时过分关注训练集的细节,得到了一个在训练集上表现很好,但不能泛化到新数据上的模型,就会存在过拟合。与之相反,如果模型过于简单,就无法抓住数据的全部内容及数据中的变化,甚至在训练集上的变现就很差,则称为

欠拟合(underfitting)

模型越复杂,在训练数据上的预测结果就越好。但是,如果模型过于复杂,就会过多关注训练集中每个单独的数据点,模型就不能很好的泛化到新数据上。二者之间存在一个最佳位置,可以得到很好的泛化性能,这就是想要得到的模型。

模型复杂度与数据集大小的关系:数据集中包含的数据点的变化范围越大,在不发生过拟合的前提下你可以使用的模型就越复杂,通常来说,收集更多的数据点可以有更大的变化范围,所以更大的数据集可以用来构建更复杂的模型。但是仅复制仙童的数据点或收集非常相似的数据是用处不大的。收集更多数据,适当构建更复杂的模型,对监督学习任务非常有用。

3. 监督学习算法

扩展数据集的一个思路——输入特征不仅仅包括直接的测量结果,还可以将特征之间的乘积(叫做

交互项

)作为新的特征,这样包含导出特征的方法叫做

特征工程(feature engineering)

3.1 k近邻(k-NN算法)

最简单的机器学习算法,构建模型只需要保存训练数据集即可。需要对新数据点进行预测时,算法会在训练数据集中找到最近的数据点,也就是所谓的”最近邻“。

3.1.1 k近邻分类

k-NN算法最简单的版本只考虑一个最近邻。除了仅考虑最近邻,还可以考虑任意个(k个)邻居。在考虑多余一个邻居的情况时,用”投票法”来指定标签。也就是说,对于每个测试点,统计多少个邻居属于类别0,多少个邻居属于类别1,然后将出现次数更多的类别(也就是k个近邻中占多数的类别)作为预测结果。该方法同样适用于多分类的问题。

单近邻

多近邻(n_neighbors=3)

通过scikit-learn来应用k近邻算法:

# 拆分数据集
from sklearn.model_selection import train_test_split
X, y = mglearn.datasets.make_forge()
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

# 导入类并将其实例化,这时可以甚至参数,比如邻居的个数。
from sklearn.neighbors import KNeighborsClassifier
clf = KNeighborsClassifier(n_neighbors=3)

# 训练模型
clf.fit(X_train, y_train)

# 预测结果
print("Test set predictions: {}".format(clf.predict(X_test)))

预测之后为了评估模型的泛化能力,可以对测试数据和测试标签调用score方法:

# 评估模型
print("Test set accurcy: {:.2f}".format(clf.score(X_test, y_test)))

返回结果为描述为测试数据中,模型对其中样本预测的正确数据占总样本的比例。

对于二维数据集,可以在平面上画出所有可能的测试点的预测结果,根据平面中每个点所属的类别对平面进行着色,这样可以查看

决策边界(decision boundary)

,即算法对类别0和类别1的分界线。

# 决策边界可视化
fig, axes = plt.subplots(1, 3, figsize=(10, 3))

for n_neighbors, ax in zip([1, 3, 9], axes):
    # fit方法返回对象本身,所以我们可以将实例化和拟合放在一行代码中
    clf = KNeighborsClassifier(n_neighbors=n_neighbors).fit(X, y)
    mglearn.plots.plot_2d_separator(clf, X, fill=True, eps=0.5, ax=ax, alpha=.4)
    mglearn.discrete_scatter(X[:, 0], X[:, 1], y, ax=ax)
    ax.set_title("{} neighbor(s)".format(n_neighbors))
    ax.set_xlabel("feature 0")
    ax.set_ylabel("feature 1")
axes[0].legend(loc=3)

从上图可以看出,使用单一邻居绘制的决策边界紧跟着训练数据。随着邻居个数越来越多,决策边界也越来越平滑,更平滑的边界对应更简单的模型。 即更少的邻居对应更高的模型复杂度,更多的邻居对应更低的模型复杂度。考虑极端情况,即邻居个数等于训练集中所有数据点的个数,那么每个测试点的邻居都完全相同(即所有训练点),所有预测结果也完全相同(即训练集中出现次数最多的类别)。

考虑模型复杂度与泛化能力之间的关系:

# 利用cancer数据集验证模型复杂度与泛化能力之间的关系(邻居个数越多,决策边界越平滑,模型更简单)
from sklearn.datasets import load_breast_cancer

cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    cancer.data, cancer.target, stratify=cancer.target, random_state=66)

training_accuracy = []
test_accuracy = []
# n_neighbors取值从1到10
neighbors_settings = range(1, 11)

for n_neighbors in neighbors_settings:
    # 构建模型
    clf = KNeighborsClassifier(n_neighbors=n_neighbors)
    clf.fit(X_train, y_train)
    # 记录训练集精度
    training_accuracy.append(clf.score(X_train, y_train))
    # 记录测试集精度(泛化精度)
    test_accuracy.append(clf.score(X_test, y_test))
    
plt.plot(neighbors_settings, training_accuracy, label="training accuracy")
plt.plot(neighbors_settings, test_accuracy, label="test accuracy")
plt.ylabel("Accuracy")
plt.xlabel("n_neighbors")
plt.legend()

以上图中,仅考虑单一近邻时,训练集上的预测结果十分完美,但随着邻居个数的增多,模型变得简单,训练精度也随之下降。单一邻居时的测试集精度比使用更多邻居时要低,这表示单一近邻的模型过于复杂。与之相反,当考虑10个邻居时,模型又过于简单,性能设置变得更差。最佳性能在中间的某处,邻居个数大约为6个。

3.1.2 k近邻回归

k近邻算法还可以用于回归。利用单一邻居的预测结果就是最近邻的目标值,使用多个近邻时,预测结果为这些邻居的平均值。

单近邻

多近邻(n_neighbors=3)

用于回归的k近邻算法在scikit-learn的KNeighborsRegressor类中实现:

from sklearn.neighbors import KNeighborsRegressor

X, y = mglearn.datasets.make_wave(n_samples=40)
# 拆分wave数据集为测试集和训练集
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

# 模型实例化,并将邻居个数设为3
reg = KNeighborsRegressor(n_neighbors=3)

# 利用训练数据和训练目标值来拟合模型
reg.fit(X_train, y_train)

# 对测试集进行预测
print("Test set predictions:\n{}".format(reg.predict(X_test)))

可以用score方法来评估模型,对于回归问题,score方法返回的是
R^{2}
分数,也叫决定系数,是回归模型预测的优度度量,位于0到1之间。等于1对应完美预测,等于0对应常数模型,即总是预测训练集响应的平均值。

# 评估模型
print("Test set R^2: {:.2f}".format(reg.score(X_test, y_test)))
Test set R^2: 0.83

分数为0.83,表示模型的拟合相对较好。

对于一维数据集,可以查看所有特征取值对应的预测结果,以下创建一个由许多点组成的测试数据集,并分别用1个、3个或9个邻居进行预测:

# 分析KNeighborsRegressor

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# 创建1000个数据点,在-3和3之间均匀分布
line = np.linspace(-3, 3, 1000).reshape(-1, 1)

for n_neighbors, ax in zip([1, 3, 9], axes):
    # 分别利用1个,3个,9个邻居进行预测
    reg = KNeighborsRegressor(n_neighbors=n_neighbors)
    reg.fit(X_train, y_train)
    
    ax.plot(line, reg.predict(line))
    ax.plot(X_train, y_train, "^", c=mglearn.cm2(0), markersize=8)
    ax.plot(X_test, y_test, "v", c=mglearn.cm2(1), markersize=8)
    
    ax.set_title(
        "{} neighbor(s)\n train score: {:.2f} test score: {:.2f}".format(
            n_neighbors, reg.score(X_train, y_train), reg.score(X_test, y_test)))
    ax.set_xlabel("Feature")
    ax.set_ylabel("Target")
    
axes[0].legend(["Model predictions", "Training data/target", "Test data/target"], loc="best")

上图中,使用单一邻居,训练集中的每个点都对预测结果由显著影响,预测结果的图像经过所有数据点,这导致预测结果十分不稳定。考虑更多的邻居之后,预测结果变得更加平滑,但对训练数据的拟合也不好。

3.1.3 总结

参数:一般来说,KNeighbors分类器由两个重要参数——邻居个数和数据点之间距离的度量方法。实践中使用较小的邻居个数(比如3个或5个)往往可以取得比较好的结果,但也应该调节这个参数。默认使用欧式距离,在许多情况下效果都很好。

优点:模型很好理解,通常不需要过多调节就可以得到不错的性能,所以在考虑使用更高级的技术前,尝试此算法是一种很好的基准方法;构建最近邻模型的速度通常很快,但如果训练集很大(特征数很多或者样本数很大),预测速度可能会比较慢。

缺点:使用k-NN算法时,对数据进行预处理时很重要的,这一算法对于由很多特征(几百或更多)的数据集往往效果不好,对于大多数特征的大多数取值都为0的数据集(所谓的稀疏数据集)来说,效果尤其不好;虽然很容易理解,但由于预测速度慢且不能处理具有很多特征的数据集,所以实践中往往不会用到。

3.2 线性模型

线性模型是在实践中广泛使用的一类模型,主要利用输入特征的

线性函数(linear function)

进行预测。

3.2.1 用于回归的线性模型

对于回归问题,线性模型预测的一般公式如下:

\hat{y}=w[0]*x[0]+w[1]*x[1]+...+w[p]*x[p]+b

x[0]到x[p]表示单个数据点的特征(特征个数为p+1),w和b是学习模型的参数,
\hat{y}
是模型的预测结果。对于单一特征的数据集,公式如下:

\hat{y}=w[0]*x[0]+b

就是高中数学里的直线方程,w[0]是斜率,b是y轴偏移,对于有更多特征的数据集,w包含沿每个特征坐标轴的斜率。也可以将预测的响应值看作输入特征的加权求和,权重由w的元素给出(可以取负值)。

用于回归的线性模型可以表示为这样的回归模型:对单一特征的预测结果是一条直线,两个特征时是一个平面,在更高纬度(即更多特征)时是一个超平面。

假设目标y是特征的线性组合,这是一个非常强的(也有点不现实的)假设。但对于有多个特征的数据集而言,线性模型可以非常强大。特别地,如果特征数量大于训练数据的数量,任何目标y都可以(在训练集上)用线性函数完美拟合。不同的线性模型之间的区别在于如何从训练数据中学习参数w和b,以及如何控制模型复杂度。

3.2.2 线性回归

线性回归,又称

普通最小二乘法(ordinary least squares, OLS)

,是回归问题最简单也最经典的线性方法。寻找w和b,使得对训练集的预测值与真实的回归目标值y之间的

均方误差

最小。

均方误差(mean squared error)

是预测值与真实值之差的平方和除以样本数。线性回归没有参数,这是一个优点,但也因此无法控制模型的复杂度。

from sklearn.linear_model import LinearRegression
X, y = mglearn.datasets.make_wave(n_samples=60)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

lr = LinearRegression().fit(X_train, y_train)

斜率参数w,也叫做权重或系数,保存在coef_属性中,偏移或截距b保存在intercept_属性中:(scikit-learn总是将从训练数据中得出的值保存在以下划线结尾的属性中,这是为了将其与用户设置的参数区分开)

print("lr.coef_: {}".format(lr.coef_)) # 单特征,数组只有一个值
print("lr.intercept_: {}".format(lr.intercept_))

coef_属性是一个Numpy数组,每个元素对应一个输入特征;intercept_属性是一个浮点数。

# 评估训练集和测试集的性能
# 分数非常接近说明可能存在欠拟合
print("Training set score: {:.2f}".format(lr.score(X_train, y_train)))
print("Test set score: {:.2f}".format(lr.score(X_test, y_test)))
Training set score: 0.67
Test set score: 0.66

训练集和测试集上的分数非常接近,这说明可能存在欠拟合。对于一维数据集来说,过拟合的风险很小,因为模型非常简单(或受限)。然而,对于更高维的数据集,线性模型将变得更加强大,过拟合的可能性也会变大。

# 多特征数据集——波士顿房价(506个样本和105个特征)

X, y = mglearn.datasets.load_extended_boston()

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
lr = LinearRegression().fit(X_train, y_train)

# 评估训练集和测试集的性能
# 性能差异是过拟合的明显标志
print("Training set score: {:.2f}".format(lr.score(X_train, y_train)))
print("Test set score: {:.2f}".format(lr.score(X_test, y_test)))
Training set score: 0.95
Test set score: 0.61

以上模型在训练集上的预测非常准确,但测试集上的
R^{2}
要低很多,

训练集和测试集之间的性能差异是过拟合的明显标志,

因此试图找到一个可以控制复杂度的模型。

3.2.3 岭回归

标准线性回归最常用的替代方法之一就是

岭回归(ridge regression)

。其预测公式与普通最小二乘法相同,区别在于岭回归对系数w的选择不仅要在训练数据上得到好的预测结果,而且还要拟合附加约束。另外还希望系数尽量小,即w的所有元素都应接近于0。直观上来看,这意味着每个特征对输入的影响应尽可能小(即斜率很小),同时仍给出很好的预测结果。这种约束是所谓的

正则化(regularization)

的一个例子。正则化是指对模型做显式约束,以避免过拟合。岭回归用到的这种被称为L2正则化。

from sklearn.linear_model import Ridge

ridge = Ridge().fit(X_train, y_train)
print("Training set score: {:.2f}".format(ridge.score(X_train, y_train)))
print("Test set score: {:.2f}".format(ridge.score(X_test, y_test)))
Training set score: 0.89
Test set score: 0.75

Ridge在训练集上的分数低于LinearRegression,但在测试集上的分数更高。线性回归对数据存在过拟合,Ridge是一种约束更强的模型,所以更不容易过拟合。复杂度更小的模型意味着在训练集上的性能更差,但泛化性能更好。

Ridge模型在模型的简单性(系数都接近于0)与训练集性能之间做出权衡。简单性和训练集性能二者对于模型的重要程度可以通过设置alpha参数来指定,默认的alpha=1.0。alpha的最佳设定值取决于用到的具体数据集,增大alpha会使得系数更趋向于0,从而降低训练集性能,

但可能

会提高泛化性能。

ridge10 = Ridge(alpha=10).fit(X_train, y_train)
print("Training set score: {:.2f}".format(ridge10.score(X_train, y_train)))
print("Test set score: {:.2f}".format(ridge10.score(X_test, y_test)))
Training set score: 0.79
Test set score: 0.64

减小alpha可以让系数受到的限制更小,对于非常小的alpha值,系数几乎没有受到限制,会得到一个与LinearRegression类似的模型:

ridge01 = Ridge(alpha=0.1).fit(X_train, y_train)
print("Training set score: {:.2f}".format(ridge01.score(X_train, y_train)))
print("Test set score: {:.2f}".format(ridge01.score(X_test, y_test)))
Training set score: 0.93
Test set score: 0.77

更大的alpha表示约束更强的模型,所以更大的alpha对应的coef_元素比较小的alpha对应的coef_元素小,查看不同alpha值时模型对应的coef_属性:

plt.plot(ridge.coef_, 's', label="Ridege alpha=1")
plt.plot(ridge.coef_, '^', label="Ridege alpha=10")
plt.plot(ridge.coef_, 'v', label="Ridege alpha=0.1")

plt.plot(lr.coef_, 'o', label="LinearRegression")

plt.xlabel("Cofficient index")
plt.ylabel("Cofficient magnitude")
plt.hlines(0, 0, len(lr.coef_))
plt.ylim(-25, 25)
plt.legend()

上图中,对于没有做正则化的线性回归(即alpha=0),点的范围很大,许多点都超出了图像的范围。

另外一种理解正则化影响的方法,是固定alpha值,但改变训练数据量。将模型性能作为数据集大小的函数进行绘图,这样的图像叫做

学习曲线

以上,由于岭回归是正则化的,因此它的训练分数要整体低于线性回归的训练分数,但岭回归的测试分数要高,特别是对较小的数据集。随着模型可用的数据越来越多,两个模型的性能都在提升,最终线性回归的性能追上了岭回归。如果有足够多的训练数据,正则化变得不那么重要,并且岭回归和线性回归将具有相同的性能。如果添加更多的数据,模型将更加难以过拟合或记住所有的数据。



版权声明:本文为Luuuuuuuuuuke原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。