本文主要内容来自周志华《机器学习》
本文中代码
问题: 对于一个只包含\(m\)个样例的数据集\(D=\{(x_1,y_1),(x_2,y_2),\cdots,(x_m,y_m)\),如何适当处理,从\(D\)中产生训练集\(S\)和测试集\(T\)?
下面介绍三种常见的做法:
- 留出法
- 交叉验证法
- 自助法
留出法(hold-out)
留出法直接将数据集\(D\)划分为两个互斥的集合,其中一个集合作为训练集\(S\),留下的集合作为测试集\(T\),即\(D=S \cup T, S \cap T=\emptyset\)。在\(S\)上训练出模型后,用\(T\)来评估其测试误差,作为对泛化误差的估计。以二分类任务为例,假设\(D\)包含1000个样本,我们采取7/3分样,将其划分为\(S\)包含700个样本,\(T\)包含300个样本。
\(S/T\)的划分要尽可能的保持数据分布的一致性(\(S/T\)中的数据分布跟\(D\)是一样的),才能避免因数据划分过程引入额外的偏差而对最终结果产生影响。例如,在分类任务中,至少要保持样本的类别比例相似。保留类别比例的采样方式,即分层采样(stratified sampling)。例如,\(D\)中含有500个正例,500个反例,当采用分层采样获取70%的样本的训练集\(S\)和30%的赝本的测试集\(T\)时,则\(S\)包含有350个正例和350个反例,\(T\)有150个正例和150个反例。
给定样本比例,有多种划分方式对\(D\)进行分割。如在上面的例子中,我们可以把\(D\)的样本排序,然后把前350个正例放到\(S\)中,也可以把后350个正例放入\(S\)... 这种不同的划分将导致不同的训练/测试集,相应的模型评估也是有差别的。因此,使用留出法时,一般要采用若干次随机划分、重复进行实验评估后取平均值作为留出法的评估结果。例如进行100次随机划分,每次产生一个\(S/T\)进行实验评估,得到100个结果,而留出法返回的则是这100个结果的平均。
常见做法将大约2/3~4/5的样本作为\(S\),剩下的作为\(T\)。
通过调用sklearn.model_selection.train_test_split
按比例划分训练集和测试集:
import numpy as np
from sklearn.model_selection import train_test_split
X, Y = np.arange(10).reshape((5, 2)), range(5)
print("X=", X)
print("Y=", Y)
X_train, X_test, Y_train, Y_test = train_test_split(
X, Y, test_size=0.30, random_state=42)
print("X_train=", X_train)
print("X_test=", X_test)
print("Y_train=", Y_train)
print("Y_test=", Y_test)
其中test_size=0.30
表示\(T\)占30%, 那么\(S\)占70%。运行结果:
X= [[0 1]
[2 3]
[4 5]
[6 7]
[8 9]]
Y= range(0, 5)
X_train= [[4 5]
[0 1]
[6 7]]
X_test= [[2 3]
[8 9]]
Y_train= [2, 0, 3]
Y_test= [1, 4]
交叉验证法(cross validation)
交叉验证法将\(D\)划分为\(k\)个大小相似的互斥子集,即\(D=D_1 \cup D_2 \cup \dots \cup D_k, D_i \cap D_j = \emptyset (i \not = j)\)。每个子集\(D_i\)都尽可能保持数据分布的一致性,即从\(D\)中通过分层采样得到。然后,每次用\(k-1\)个子集的并集作为\(S\),剩下的那个子集作为\(T\),这样可获得\(k\)组\(S/T\),从而可进行\(k\)次训练和测试,最终返回这\(k\)个测试结果的平均。
交叉验证法评估结果的稳定性和保真性在很大程度上取决于\(k\)的取值、为强调这一点,通常把交叉验证法称为“\(k\)折交叉验证”(\(k\)-fold cross validation)。\(k\)最常用的取值是10,有时也取5和20等。
通过调用sklearn.model_selection.KFold
按\(k\)折交叉划分训练集和测试集:
import numpy as np
from sklearn.model_selection import KFold
X= np.arange(10).reshape((5, 2))
print("X=", X)
kf = KFold(n_splits=2)
for train_index, test_index in kf.split(X):
print('X_train:%s ' % X[train_index])
print('X_test: %s ' % X[test_index])
其中n_splits=2
表示\(k=2\)。运行结果:
X= [[0 1]
[2 3]
[4 5]
[6 7]
[8 9]]
X_train:[[6 7]
[8 9]]
X_test: [[0 1]
[2 3]
[4 5]]
X_train:[[0 1]
[2 3]
[4 5]]
X_test: [[6 7]
[8 9]]
当\(D\)包含\(m\)个样本,令\(k=m\),则得到交叉验证法的一个特例——留一法(Leave-One-Out,简称LOO)。留一法使用的\(S\)只比\(D\)少一个样本,所以在绝大多数情况下,实际评估结果与用\(D\)训练的模型相似。因此,留一法被认为比较准确。但留一法对于大数据集,计算开销太大;另外也不见得永远比其他方法准确。
通过调用sklearn.model_selection.LeaveOneOut
按留一法划分训练集和测试集:
import numpy as np
from sklearn.model_selection import LeaveOneOut
X= np.arange(10).reshape((5, 2))
print("X=", X)
loo = LeaveOneOut()
for train_index, test_index in loo.split(X):
print('X_train:%s ' % X[train_index])
print('X_test: %s ' % X[test_index])
运行结果:
X= [[0 1]
[2 3]
[4 5]
[6 7]
[8 9]]
X_train:[[2 3]
[4 5]
[6 7]
[8 9]]
X_test: [[0 1]]
X_train:[[0 1]
[4 5]
[6 7]
[8 9]]
X_test: [[2 3]]
X_train:[[0 1]
[2 3]
[6 7]
[8 9]]
X_test: [[4 5]]
X_train:[[0 1]
[2 3]
[4 5]
[8 9]]
X_test: [[6 7]]
X_train:[[0 1]
[2 3]
[4 5]
[6 7]]
X_test: [[8 9]]
自助法(bootstrapping)
在前两种方法中都保留部分样本作为\(T\)用于测试,因此实际评估模型使用的训练集\(T\)总是比期望评估模型使用的训练集\(D\)小,这样会引入一些因训练样本规模不同而导致的估计偏差。
自助法,以自助采样(bootstrap sampling)为基础。对\(D\)进行采样产生\(D^{\prime}\):每次随机从\(D\)中挑选一个样本,将其拷贝一份放入\(D^{\prime}\)中,保持\(D\)不变,重复以上过程\(m\)次。显然,\(D\)中有部分样本会多次出现在\(D^{\prime}\)中,而另一部分不会出现。样本在\(m\)次采样中的始终不被采到的概率为\((1-\frac{1}{m})^m\),当\(m \to \infty\)时,\((1-\frac{1}{m})^m \to \frac{1}{e} \approx 0.368\)。
通过自助法,\(D\)中有36.8%不会出现\(D^{\prime}\)中。于是我们把\(D^{\prime}\)当作训练集\(S\),把\(D \setminus D^{\prime}\)当作测试集\(T\),这样实际评估模型与期望评估模型都为\(m\)个样本,而仍有数据总量1/3的、没有出现在训练集中的样本用于测试。这样的测试结果为包外估计(out-of-bag estimate)。
自助法在数据集较小、难以划分\(S/T\)时很有用。此外,自助法能从\(D\)中产生不同的\(S\),对集成学习等方法有好吃。自助法产生的\(S\)改变了\(D\)的分布,会引入估计偏差。当数据量足够时,留出法和交叉验证法更常用、