这篇博文介绍了机器学习的理论基础,主要内容如下:
- 模型过拟合和欠拟合
- 模型成本及成本函数的含义
- 评价一个模型好坏的标准
- 学习曲线,以及用学习曲线来对模型进行诊断
- 算法模型性能优化
- 查准率和召回率
第一部分:过拟合和欠拟合
过拟合是指能很好地拟合训练样本,但对新数据的预测准确性很差。
欠拟合是指模型不能很好地拟合训练样本,且对新数据的预测准确性也不好。
先来看一个简单的例子,生成一个20个点训练样本:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
n_dots = 20
x = np.linspace(0, 1, n_dots) # [0, 1] 之间创建 20 个点
y = np.sqrt(x) + 0.2*np.random.rand(n_dots) - 0.1;
def plot_polynomial_fit(x, y, order):
p = np.poly1d(np.polyfit(x, y, order))
# 画出拟合出来的多项式所表达的曲线以及原始的点
t = np.linspace(0, 1, 200)
plt.plot(x, y, 'ro', t, p(t), '-', t, np.sqrt(t), 'r--')
return p
plt.figure(figsize=(18, 4), dpi=200)
titles = ['Under Fitting', 'Fitting', 'Over Fitting']
models = [None, None, None]
for index, order in enumerate([1, 3, 10]):
plt.subplot(1, 3, index + 1)
models[index] = plot_polynomial_fit(x, y, order)
plt.title(titles[index], fontsize=20)
【显示结果】
训练样本是,其中r是[-0.1,0.1]之间的一个随机数。然后分别用一阶多项式、三阶多项式和十阶多项式3个模型来拟合这个数据集,如上图所示。
说明:图中的小红点是生成的20个训练样本;虚线中实际的模型;实线是用训练样本拟合出来的模型。
左边是欠拟合(underfitting),也称为高偏差,用一条直线来拟合数据。右边是过拟合(overfitting),也称为高方差(high variance),用了十阶多项式来拟合数据,虽然模型对现有的数据拟合的很好,但对新数据预测误差却很大。中间的模型较好地拟合了数据集,可以看到虚线和实线基本重合。
第二部分:成本函数
成本是衡量模型与训练样本符合程度的指标。简单地理解,成本就是针对所有的训练样本,模型拟合出来的值与训练样本的真实值的误差平均值。而成本函数就是成本与模型参数的函数关系。模型训练的过程,就是找出合适的模型参数,使得成本函数的最小。成本函数记为,其中表示模型参数。
对于第一部分的例子,使用一阶多项式来拟合数据,则得到的模型是。此时,构成的向量就是模型参数。训练这个模型的目标,就是找出合适的模型参数,使得所有的点到这条直线的距离最短。
for m in models:
print('model coeffs: {0}'.format(m.coeffs))
model coeffs: [0.78873042 0.2831822 ] model coeffs: [ 1.48137254 -2.83944854 2.27237428 0.12291568] model coeffs: [-1.93879237e+03 9.99692603e+03 -2.26286394e+04 2.96402242e+04 -2.48223736e+04 1.37462158e+04 -4.98086547e+03 1.12105086e+03 -1.42856980e+02 1.00403158e+01 4.23669484e-02]
# 针对一阶多项式的模型,不同的参数拟合出来的直线和训练样本对应的位置关系
coeffs_1d = [0.2, 0.6]
plt.figure(figsize=(9, 6), dpi=200)
t = np.linspace(0, 1, 200)
plt.plot(x, y, 'ro', t, models[0](t), '-', t, np.poly1d(coeffs_1d)(t), 'r-')
plt.annotate(r'L1: $y = {1} + {0}x$'.format(coeffs_1d[0], coeffs_1d[1]),
xy=(0.8, np.poly1d(coeffs_1d)(0.8)), xycoords='data',
xytext=(-90, -50), textcoords='offset points', fontsize=16,
arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.annotate(r'L2: $y = {1} + {0}x$'.format(models[0].coeffs[0], models[0].coeffs[1]),
xy=(0.3, models[0](0.3)), xycoords='data',
xytext=(-90, -50), textcoords='offset points', fontsize=16,
arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
输出:
Text(-90,-50,'L2: $y = 0.28318220499113633 + 0.7887304225978029x$')
显示:
如图所示,不同的模型参数对应不同的直线,明显可以看出来L2比L1更好地拟合数据集。根据成本函数的定义,可以很容易地得出模型的成本函数公式:
m是训练样本个数(20个点),就是模型对每个样本的预测值,是每个样本的真实值。这个公式实际上就是线性回归算法的成本函数的简化表达。
一个数据集可能有多个模型可以用来拟合它,而一个模型有无穷多个模型参数,针对特定的数据集和特定的模型,只有一个模型参数能最好地拟合这个数据集,这就是模型和模型参数的关系。
总结:针对一个数据集,可以选择很多个模型来拟合数据,一旦选定了某个模型,就需要从这个模型的无穷多个参数里找出一个最优的参数,使得成本函数的值最小。那么要怎么知道一个模型的好坏?这就要用到模型准确率。
第三部分:模型准确率
测试数据集成本,是评估模型准确性的最直观的指标,值越小说明模型预测出来的值与实际值差异越小,对新数据的预测准确性就越好。需要注意的是,用来测试模型准确性的数据集,必须是模型“没见过”的数据。
那么,我们要怎么计算测试数据集的误差呢?简单地说,就是用测试数据集和训练出来的模型参数代入相应的成本函数里,计算测试数据集的成本。
针对前面提到过的线性回归算法,可以使用下面的公式计算测试数据集的误差,其中m是测试数据集的个数:
1、模型性能的不同表述方式
在scikit-learn里,不使用成本函数来表达模型的性能,而是用分数来表达,这个分数总是在[0,1]之间,数值越大说明模型的准确性越好。当模型训练完成后,调用模型的score[X_test,y_test]即可算出模型的分数值,其中X_test和y_test是测试数据集样本。
模型分数(准确性)与成本成反比。即分数越大,准确性越高,误差越小,成本越低;反之,分数越小,准确性越低,误差越大,成本越高。
2、交叉验证数据集
有一个更科学的方法是数据集分成3份,分别是训练数据集、交叉验证数据集和测试数据集,推荐比例为6:2:2。
在模型选择时,可以使用训练数据集来训练算法参数,用交叉验证数据集来验证参数,选择交叉验证数据集的成本最小的多项式来作为数据拟合模型,最后再用测试数据集来测试选择出来的模型针对测试数据集的准确性。这样就保证了选择的模型是没有见过测试数据的目的。
在实践过程中,很多人直接把数据集分成训练数据集和测试数据集,而没有分出交叉验证数据集。是因为很多时候并不需要去横向对比不同的模型。
第四部分:学习曲线
把和作为纵坐标,训练数据m作为横坐标,画出与训练数据m到的大小关系,这就是学习曲线。通过学习曲线,可以直观地观察到模型的准确性与训练数据集大小的关系。
如果数据集的大小为m,则通过下面的流程即可画出学习曲线:
- 把数据集分成训练数据集和交叉验证数据集;
- 提取训练数据集的20%作为训练样本,训练出模型参数;
- 使用交叉验证数据集来计算训练出来的模型的准确性;
- 以训练数据集的准确性,交叉验证的准确性作为纵坐标想,训练数据集个数作为横坐标,在坐标轴上画出上述步骤计算出来的模型准确性;
- 训练数据集增加10%,跳到步骤3继续执行,直到训练数据集大小为100%为止。
学习曲线要表达的是,当训练数据集增加时,模型对训练数据集拟合的准确性以及对交叉验证数据集预测的准确性的变化规律。
【示例:画出学习曲线】
下面通过一个例子来看看在scikit-learn里如何画出模型的学习曲线,从而判断模型的准确性及优化方向。
(1)先生成一个在附近波动的点来作为训练样本,不过这次要多生成一些点,因为要考虑当训练样本数量增加的时候,模型的准确性是怎样变化的。
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
n_dots = 200
X = np.linspace(0, 1, n_dots)
y = np.sqrt(X) + 0.2*np.random.rand(n_dots) - 0.1;
X = X.reshape(-1, 1)
y = y.reshape(-1, 1)
(2)构造一个多项式模型。在scikit-learn里,需要用Pipeline来构造多项式模型,Pipeline的意思是流水线,即这个流水线里可以包含多个数据处理模型,前一个模型处理完,转到下一个模型处理。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
def polynomial_model(degree=1):
polynomial_features = PolynomialFeatures(degree=degree,include_bias=False)
linear_regression = LinearRegression()
pipeline = Pipeline([("polynomial_features", polynomial_features),("linear_regression", linear_regression)])
return pipeline
polynomial_model()函数来生成一个多项式模型,其中参数degree表示多项式的阶数,比如polynomial_model(3)将生成一个三阶多项式的模型。
(3)在scikit-learn里面,我们不用自己去实现学习曲线算法,直接使用sklearn.model_selection.learning_curve()函数来画出学习曲线,它会自动把训练样本的数量按照预定的规则逐渐增加,然后画出不同训练样本数量时的模型准确性。其中train_sizes参数就是指定训练样本数量的变化规则,比如train_sizes=np.linespace(.1,1.0,5)表示把训练样本数量从0.1~1分成五等分,生成[0.1,0.325,0.55,0.775,1]的序列,从序列中取出训练样本数量百分比,逐个计算在当前训练样本数量情况下训练出来的模型准确性。
from sklearn.model_selection import learning_curve
from sklearn.model_selection import ShuffleSplit
def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,
n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):
plt.title(title)
if ylim is not None:
plt.ylim(*ylim)
plt.xlabel("Training examples")
plt.ylabel("Score")
train_sizes, train_scores, test_scores = learning_curve(
estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes)
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std = np.std(test_scores, axis=1)
plt.grid()
plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
train_scores_mean + train_scores_std, alpha=0.1,
color="r")
plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
test_scores_mean + test_scores_std, alpha=0.1, color="g")
plt.plot(train_sizes, train_scores_mean, 'o--', color="r",
label="Training score")
plt.plot(train_sizes, test_scores_mean, 'o-', color="g",
label="Cross-validation score")
plt.legend(loc="best")
return plt
这个函数实现的功能就是画出模型的学习曲线。当计算模型的准确性时,是随机从数据集中分配出训练样本和交叉验证样本,这样会导致数据分布不均匀。即同样训练样本数量的模型,由于随机分配,导致每次计算出来的准确性都不一样。为了解决这个问题,计算模型的准确性时,多次计算,并求准确性的平均值和方差。fill_between()函数会把模型准确性的平均值的上下方差的空间里用颜色填充。然后用plt.plot()函数画出模型准确性的平均值。上述函数画出了训练样本的准确性,也画出了交叉验证样本的准确性。
(4)使用polynomial_model()函数构造出3个模型,分别是一阶多项式、三阶多项式、十阶多项式,分别画出这3个模型的学习曲线。
# 为了让学习曲线更平滑,交叉验证数据集的得分计算 10 次,每次都重新选中 20% 的数据计算一遍
cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)
titles = ['Learning Curves (Under Fitting)',
'Learning Curves',
'Learning Curves (Over Fitting)']
degrees = [1, 3, 10]
plt.figure(figsize=(18, 4), dpi=200)
for i in range(len(degrees)):
plt.subplot(1, 3, i + 1)
plot_learning_curve(polynomial_model(degrees[i]), titles[i], X, y, ylim=(0.75, 1.01), cv=cv)
plt.show()
【完整代码】
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import learning_curve
from sklearn.model_selection import ShuffleSplit
n_dots = 200
X = np.linspace(0, 1, n_dots)
y = np.sqrt(X) + 0.2*np.random.rand(n_dots) - 0.1;
X = X.reshape(-1, 1)
y = y.reshape(-1, 1)
def polynomial_model(degree=1):
polynomial_features = PolynomialFeatures(degree=degree,
include_bias=False)
linear_regression = LinearRegression()
pipeline = Pipeline([("polynomial_features", polynomial_features),
("linear_regression", linear_regression)])
return pipeline
def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,
n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):
plt.title(title)
if ylim is not None:
plt.ylim(*ylim)
plt.xlabel("Training examples")
plt.ylabel("Score")
train_sizes, train_scores, test_scores = learning_curve(
estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes)
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std = np.std(test_scores, axis=1)
plt.grid()
plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
train_scores_mean + train_scores_std, alpha=0.1,
color="r")
plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
test_scores_mean + test_scores_std, alpha=0.1, color="g")
plt.plot(train_sizes, train_scores_mean, 'o--', color="r",
label="Training score")
plt.plot(train_sizes, test_scores_mean, 'o-', color="g",
label="Cross-validation score")
plt.legend(loc="best")
return plt
# 为了让学习曲线更平滑,交叉验证数据集的得分计算 10 次,每次都重新选中 20% 的数据计算一遍
cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)
titles = ['Learning Curves (Under Fitting)',
'Learning Curves',
'Learning Curves (Over Fitting)']
degrees = [1, 3, 10]
plt.figure(figsize=(18, 4), dpi=200)
for i in range(len(degrees)):
plt.subplot(1, 3, i + 1)
plot_learning_curve(polynomial_model(degrees[i]), titles[i], X, y, ylim=(0.75, 1.01), cv=cv)
plt.show()
最终得出的曲线如图所示:
【分析】
左图:一阶多项式,欠拟合;中图:三阶多项式,较好地拟合了数据集;右图:十阶多项式,过拟合。虚线:针对训练数据集计算出来的分数,即针对训练数据集拟合的准确性,实线:针对交叉验证数据集计算出来的分数,即针对交叉验证数据集预测的准确性。
从左图可以观察到,当模型欠拟合(High Bias,Under Fitting)时,随着训练数据集的增加,交叉验证数据集的准确性(实线)逐渐增大,逐渐和训练数据集的准确性(虚线)靠近,但其总体水平比较低,收敛在0.88左右。其训练数据集的准确性也比较低,收敛在0.90左右。这就是过拟合的表现。从这个关系可以看出来,当发生高偏差时,增加训练样本数量不会对算法准确性有较大的改善。
从右图可以观察到,当模型过拟合(High Variance,Over Fitting)时,随着训练数据集的增加,交叉验证数据集的准确性(实线)也在增加,逐渐和训练数据集的准确性(虚线)靠近,但两者之间的间隙比较大。训练数据集的准确性很高,收敛在0.95左右,是三者中最高的,但其交叉验证数据集的准确性值却较低,最终收敛在0.91左右。
中图,选择的三阶多项式较好地拟合了数据,最终训练数据集的准确性(虚线)和交叉验证数据集的准确性(实线)靠得很近,最终交叉验证数据集收敛在0.93附近,训练数据集的准确性收敛在0.94附近。3个模型对比,这个模型的准确性最好。
2、过拟合和欠拟合的特征
过拟合:模型对训练数据集的准确性比较高,其成本比较低,对交叉验证数据集的准确性比较低,其成本比较高。
欠拟合:模型对训练数据集的准确性比较低,其成本比较高,对交叉验证数据集的准确性也比较低,其成本也比较高。
一个好的机器学习算法应该是对训练数据集准确性高、成本低,即比较准确地拟合数据,同时对交叉验证数据集准确性高、成本低、误差小,即对未知数具有良好的预测性。
第五部分:算法模型与性能优化
欠拟合和过拟合的算法模型怎么进行优化?
1、过拟合
获取更多的训练数据:从学习曲线的规律来看,更多的数据有助于改善过拟合问题。
减少输入的特征数量:比如,针对书写识别系统,原来使用200x200的图片,总共40000个特征。优化后,我们可以把图片等比例缩小为10x10的图片,总共100个特征。这样可以大大减少模型的计算量,同时也缩减模型的复杂度,改善过拟合问题。
2、欠拟合,说明模型太简单,需要增加模型的复杂度。
- 增加有价值的特征:重新解读并理解训练数据。比如针对一个房产价格预测的机器学习任务,原来只根据房子面积来预测价格,结果模型出现了欠拟合。优化后,我们增加其他的特征,比如房子的朝向、户型、年代、房子旁边的学校的质量等。
- 增加多项式特征
第六部分:查准率和召回率
而 召回率是针对我们原来的 样本而言的,它表示的是样本中的正例有多少被预测正确了。那也有两种可能,一种是把原来的正类预测成 正类(TP),另一种就是把原来的正类预测为 负类(FN)。
查准率=检索出的相关信息量 / 检索出的信息总量
查全率=检索出的相关信息量 / 系统中的相关信息总量
假设我们手上有60个正样本,40个负样本,我们要找出所有的正样本,系统查找出50个,其中只有40个是真正的正样本,计算上述各指标。
- TP: 将正类预测为正类数 40
- FN: 将正类预测为负类数 20
- FP: 将负类预测为正类数 10
- TN: 将负类预测为负类数 30
查准率(precision) = TP/(TP+FP) = 80%
召回率(recall) = TP/(TP+FN) = 2/3
第七部分:F1 Score
如果有多个算法,彼此之间的查准率和召回率是不一样的,那么应该怎么确定哪个算法更好呢?
这里就涉及到了查准率,公式为:
P为查准率,R为召回率。这样就可以用一个数值直接判断哪个算法性能更好。
通常我们选择在验证集上,会选择F1 Score 数值最大的那个模型假设。
参考资料:
《scikit-learn机器学习常用算法原理及编程实践》 黄永昌
知乎