在本章中,我们会使用回归模型来讲解,对于分类模型其原理一样。
(下图为一般模型的训练误差及测试误差与模型复杂度的关系)
验证集方法(validation set approach)的步骤:
(1).随机地把观测集合分成两部分:训练集(training set) 和 测试集(validation set)或保留集(hold-out set)。
(2).模型在训练集上拟合,然后用拟合的模型来预测验证集的观测得响应变量。
(3).计算验证集的错误率,一般用均方误差。
下面我们用一个例子来说明这种方法:
在Auto数据集上,我们经过数据初步探索分析出来:mpg(因变量)和horsepower(自变量)之间似乎存在非线性关系,而我们应该使用多少次多项式拟合mpg与horsepower 之间的关系比较合适呢?下面我们使用验证集方法(validation set approach)来解决这个问题:
(1).我们 随机 将392个观测分为两个集合,一个包含196个数据点的训练集,一个包含剩余196个数据点的验证集。
(2).我们用包含196个数据点的训练集拟合多项式次数1-10的回归模型。
(3).对这10个中的每个模型,我们用包含剩余196个数据点的测试集来预测每个模型的均方误差(Mean Squared Error)。
(4).将每个次数的多项式模型的均方误差的点画在图上(下图左图每个红色点代表每个多项式的均方误差)
通过图像,我们发现基于验证集的二次项拟合的均方误差比线性模型小的多,而用三次项拟合回归模型并不比只用二次项拟合的模型好,这就验证了我们上图中一般模型的训练误差及测试误差与模型复杂度的关系,增加模型复杂度不一定减少测试集的误差率。
下面我们来评价验证集方法(validation set approach)的优缺点:
我们把验证集方法(validation set approach)的步骤(1)重复10次,即执行10种不同的抽样,然后每种抽样都执行验证集方法(validation set approach)的步骤(2)~(4),我们得出了上图的右图的10条不同的折线,10条折线都说明了用二次项拟合的模型比线性模型的验证集均方误差小得多,也说明了用三次或者更高次项拟合模型的效果并没有显著提升。(但是这些折线并不能说明哪个模型有最小的验证集均方误差,基于这些折线的波动性,只能确定这个数据做线性拟合不合适!)
验证集方法的原理很简单,且易于执行,蛋挞有两个潜在缺陷:
(1).正如上图右图所示,测试错误率的验证集方法估计的波动很大,这取决于具体哪些观测被包括在训练集中,哪些观测被包含在验证集中。
(2).在验证集方法中,我们仅使用了全体数据的一部分(训练集)去拟合模型,由于被训练的观测越少,统计方法的效果就越不好,这意味着验证集错误率可能 高估了在整个数据集上拟合所得到的的模型所得到的测试错误率。
针对以上验证集方法(validation set approach)的两个缺陷,我们使用了改进的方法:交叉验证法(cross validation)。
留一交叉验证法(leave-one-out cross-validation,LOOCV)的步骤:
(1).假设全体样本为 { ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . . . . , ( x n , y n ) } {\{(x_1,y_1),(x_2,y_2),......,(x_n,y_n)\}} {(x1,y1),(x2,y2),......,(xn,yn)} 共n个样本,取其中的 ( x i , y i ) {(x_i,y_i)} (xi,yi)作为验证集,其余n-1个样本的作为训练集。
(2).将n-1个样本拟合模型,然后用拟合的模型来预测 ( x i , y i ) {(x_i,y_i)} (xi,yi)得响应变量,并计算均方误差 M S E i {MSE_i} MSEi。
(3).将上述步骤(1)(2)重复n次,也就是每个点都成为了一次验证集最后计算n个测试误差的均值: C V ( n ) = 1 n ∑ i = 1 n M S E i {CV_{(n)}=\frac{1}{n}\sum\limits_{i=1}^nMSE_i} CV(n)=n1i=1∑nMSEi。
下面我们来举个例子使用这个方法:
我们使用LOOCV方法对Auto数据集上的mpg和horsepower的1-10次多项式拟合模型,得到下左图,由于LOOCV对取样没有随机性,我们无法像验证集方法上对随机抽取的样本建模然后绘制10条折线:(对比验证集方法上的图,有什么异同?)
LOOCV方法的优点(与验证集方法对比):
(1).相对于验证集方法,LOOCV 的方法的偏差(bias)较少,因为他几乎使用了所有的样本去拟合数据,所以这个方法提供了对于测试误差的一个渐进无偏估计: lim n → ∞ E ( C V ( n ) ) = t e s t e r r o r \lim\limits_{n\to\infty}E(CV_{(n)}) = test \;error n→∞limE(CV(n))=testerror(真实值)。因此LOOCV方法比验证集方法更不容易高估测试错误率。
(2).由于LOOCV方法在训练集和验证集的分割不存在随机性,所以反复运用LOOCV方法总会得到相同的结果 C V ( n ) {CV_{(n)}} CV(n)。
LOOCV方法的缺点(与验证集方法对比):
LOOCV方法 的计算量可能很大,因为模型需要被拟合n次,如果n很大或者模型拟合起来很慢的话,这种方法很耗时。但是,当应用最小二乘法去拟合模型时候,LOOCV方法所花费的时间还会神奇的缩减为拟合一个模型相同: C V ( n ) = 1 n ∑ i = 1 n ( y i − y ^ i 1 − h i ) 2 {CV_{(n)} = \frac{1}{n}\sum\limits_{i=1}^n(\dfrac{y_i-\hat{y}_i}{1-h_i})^2} CV(n)=n1i=1∑n(1−hiyi−y^i)2,其中 h i = 1 n + ( x i − x ˉ ) 2 ∑ i = 1 n ( x i ′ − x ˉ ) 2 {h_i=\frac{1}{n}+\dfrac{(x_i-\bar{x})^2}{\sum\limits_{i=1}^n(x_{i^{'}}-\bar{x})^2}} hi=n1+i=1∑n(xi′−xˉ)2(xi−xˉ)2 ( h i h_i hi在线性回归中被称为杠杆值)。
由于LOOCV方法在n很大时,这种方法会非常耗时,因此K折交叉验证(k-fold CV是LOOCV方法的一种替代。
我们将使用Auto数据集进行演示:
library(ISLR)
library(RColorBrewer)
miscolores = brewer.pal(8,"Set2")[1:8]
head(Auto)
attach(Auto)
par(mar = c(5, 5, 0.1, 0.1))
plot(horsepower, mpg, pch = 12, col = miscolores[5], xlab = "Horsepower", ylab = "mpg")
detach()
首先,我们使用函数sample()把数据分成不相交的两部分,函数sample()可以得到随机样本。第一个参数一般是一个向量,函数sample()的返回值随机的在这个向量中抽取,在我们的例子中,该向量是1到n的自然数;第二个参数表示要抽取的随机数的个数,在这里,我们将用200个观测点组成的数据作为训练数据;第三个参数表示是否是有放回的抽取。因此,在上面的例子中,函数sample()的结果是一个向量,该向量有200个元素,这200个元素是从1到n这些自然数等概率的抽取出来的。而且这200元素全都是不一样的,因为我们设置replace=F,是没有放回的抽取。
set.seed(1)
n <- nrow(Auto) # "nrow()"得到数据框行的个数
train <- sample(1:n, 200, replace = F)
length(train)
train[1:10]
接着,我们使用包含在train里面的观测点建立线性回归模型,然后使用不在train里的观测点计算预测误差,
lm.fit <- lm(mpg ~ horsepower, data = Auto, subset = train)
mean((Auto$mpg[-train] - predict(lm.fit, Auto)[-train])^2)
我们可以再做一次类似的实验,使用不同的随机数,那么划分到训练集和测试集的观测点将会不一样,
set.seed(2)
train <- sample(1:n, 200, replace = F)
lm.fit <- lm(mpg ~ horsepower, data = Auto, subset = train)
mean((Auto$mpg[-train] - predict(lm.fit, Auto)[-train])^2)
我们可以试试用同样的方式,得到100个预测误差,
err <- rep(0, 100)
for (i in 1:100) {
+ train <- sample(1:n, 200, replace = F)
+ lm.fit <- lm(mpg ~ horsepower, data = Auto, subset = train)
+ err[i] <- mean((Auto$mpg[-train] - predict(lm.fit, Auto)[-train])^2)
+ }
length(err)
err[1:10]
summary(err)
par(mfrow = c(1, 2), mar = c(5,5,0.1,0.1))
hist(err, col = miscolores[1], xlab = "Prediction Error", main = "")
boxplot(err, col = miscolores[2], ylab = "Prediction Error")
我们重复了100次实验,最小的预测误差为19.7312618, 最大的预测误差为29.5977063。在这个数据中,我们以mpg为因变量,以horsepower为自变量建立的线性模型的预测误差的平均值是24.4187267。 可以看到,不同实验得到的结果差别是很大的。
从上面的分析,我们可以看到validation method估计的预测误差稳定性不太好,k-fold CV可以有效的降低这种不稳定性,提供一个更好的预测误差的估计。在CV中, 我们随机的把数据分成k份,然后把第一份数据当成是测试数据,其他的数据作为训练数据;接着把第二份数据看成是测试数据,其他的数据作为训练数据;以此类推。 我们依然使用函数sample()实现上述目的,
nfolds <- 2
tmp <- rep(1:nfolds, length = n)
length(tmp)
tmp[1:25]
index <- sample(tmp, n, replace = F)
index[1:25]
table(index)
首先我们先得到一个向量tmp, 重复列出1到nfolds的自然数,总的长度是数据的行数n。然后再使用函数sample(),无放回,等概率的从tmp中抽取n个数。这时sample()的效果就是把tmp的数打乱。我们可以看看函数sample()得到的结果index。index中只包含了nfolds个不同的数,这时我们可以把所有“1”对应的观测值看成第一组,所有“2”对应的观测值看成第二组… 这样就实现了随机的对数据进行分组。接着,使用for循环实现CV,
nfolds <- 2
mse <- rep(0, nfolds)
index <- sample(rep(1:nfolds, length = n), n, replace = F)
for(i in 1:nfolds) {
+ train <- which(index != i)
+ lm.fit <- lm(mpg ~ horsepower, data = Auto, subset = train)
+ mse[i] <- mean((Auto$mpg[-train] - predict(lm.fit, Auto)[-train])^2)
+}
cv_value <- mean(mse)
cv_value
为了描述上更加简单,在这里我们先设定nfolds=2。我们先产生一个随机的向量index, 该向量包含了1到nfolds的自然数。 然后使用for循环, i先等于1, index != i判断那些index里的元素不等于1,得到的是一个逻辑向量,然后函数which()可以得到index中哪些元素是不等于1的, 接着我们用train里面的元素所对应的观测点建立回归模型,最后使用不在train里面的观测点计算预测误差,完成第一次循环;第二次循环时,i等于2,函数which()可以得到index中哪些元素是不等于2的, 接着我们用train里面的元素所对应的观测点建立回归模型,最后使用不在train里面的观测点计算预测误差。最后,我们求2次预测的平均值,即我们要得到的CV值。
我们也可以把CV的过程写进一个函数中,方便以后使用,我们自定义了一个函数cv_fun计算线性回归模型的CV值。函数cv_fun有两个参数,第一个参数是数据,第二个参数k表示把数据分成的份数。这时我们还给参数k设定了默认值,10。使用函数cv_fun时,如果我们没有明确的给k设定一个量,k就等于它的默认值10。函数的最后没有写明return时,默认返回写在函数内部的最后一个对象,在这个例子,返回的是cv_value。 例如,
cv_fun <- function(dat, k = 10) {
+
+ n <- nrow(dat)
+ index <- sample(rep(1:k, length = n), n, replace = F)
+
+ mse <- rep(0, k)
+ for(i in 1:k) {
+ train <- which(index != i)
+ lm.fit <- lm(mpg ~ horsepower, data = dat, subset = train)
+ mse[i] <- mean((dat$mpg[-train] - predict(lm.fit, dat)[-train])^2)
+ }
+ cv_value <- mean(mse)
+ cv_value
+ }
cv_fun(dat = Auto, k = 2)
cv_fun(dat = Auto) # 这时 k = 10
我们还可以考虑建立一个多项式模型,以mpg作为因变量,horsepower和horsepower2作为自变量,用CV估计模型的预测误差,我们自定义了一个函数cv_fun_poly(), 和函数cv_fun()唯一的区别在使用函数lm()时,公式mpg ~ horsepower改成了函数cv_fun_poly()里的 mpg ~ horsepower + I(horsepower2)。I(horsepower2)表示horsepower的平方项。
cv_fun_poly <- function(dat, k = 10) {
+
+ n <- nrow(dat)
+ index <- sample(rep(1:k, length = n), n, replace = F)
+
+ mse <- rep(0, k)
+ for(i in 1:k) {
+ train <- which(index != i)
+ lm.fit <- lm(mpg ~ horsepower + I(horsepower^2), data = dat, subset = train)
+ mse[i] <- mean((dat$mpg[-train] - predict(lm.fit, dat)[-train])^2)
+ }
+ cv_value <- mean(mse)
+ cv_value
+ }
cv_fun_poly(dat = Auto, k = 5)
cv_fun_poly(dat = Auto) # 这时 k = 10
可以看到,加了二次项的模型的预测误差大概等于20, 而只有一次项的模型的预测误差约等于25。根据这两个模型的预测误差,我们可以选择预测误差小的模型,即加了二次项的线性回归模型。
在R中,我们也可以使用函数cv.glm()计算广义线性模型的CV值,函数glm()也可以用来拟合线性回归模型,这时需把参数family=gaussian。然后再使用包boot里的函数cv.glm()得到CV值。函数cv.glm()的第一个参数是数据,第二个参数是拟合的线性回归模型,第三个参数是CV分组的个数。
require(boot)
glm.fit <- glm(mpg ~ horsepower, data = Auto, family = gaussian)
cv.glm(Auto, glm.fit, K = 10)$delta[1]
我们使用数据Portfolio学习如何实现bootstrap计算估计量的方差。数据Portfolio在包ISLR内。数据Portfolio有两个投资品种,X和Y的100天的收益率。
require(ISLR)
head(Portfolio)
dim(Portfolio)
我们的目的是计算资金分配到X和Y的比例,α。可以通过最小化投资组合的方差得到,即 m i n α = V a r ( α X + ( 1 − α ) Y ) {min_{\alpha} = Var(\alpha X+(1-\alpha)Y)} minα=Var(αX+(1−α)Y),通过对 V a r ( α X + ( 1 − α ) Y ) {Var(\alpha X+(1-\alpha)Y)} Var(αX+(1−α)Y)求导数,可以得到α的估计值: α = σ Y 2 − σ X Y σ X 2 + σ Y 2 − 2 σ X Y {\alpha = \dfrac{\sigma_Y^2-\sigma_{XY}}{\sigma_X^2+\sigma_Y^2-2\sigma_{XY}}} α=σX2+σY2−2σXYσY2−σXY
X <- Portfolio$X
Y <- Portfolio$Y
alpha <- (var(Y) - cov(X, Y)) / (var(X) + var(Y) - 2*cov(X, Y))
alpha
Boostrap产生大量bootstrap样本,然后计算每个bootstrap样本下,α的估计值,最后再根据这些bootstrap样本的估计值,计算α的估计值的标准差。我们可以使用函数sample()得到bootstrap样本,
n <- nrow(Portfolio)
idx <- sample(1:n, n, replace = T)
length(idx)
idx[1:15]
我们通过有放回的抽取得到bootstrap样本,所以把函数sample()的参数replace设成了TRUE。 bootstrap样本的观测点数量和原数据是一样的。
X_boot <- X[idx]
Y_boot <- Y[idx]
alpha_boot <- (var(Y_boot) - cov(X_boot, Y_boot)) / (var(X_boot) + var(Y_boot) - 2*cov(X_boot, Y_boot))
bootstrap样本即(X_boot, Y_boot), 然后计算这个样本下的投资分配比例,alpha_boot。把上述的代码整理,放到for循环中,
B <- 1000
alpha_vec <- rep(0, B)
for(i in 1:B) {
+ idx <- sample(1:n, n, replace = T)
+ X_boot <- X[idx]
+ Y_boot <- Y[idx]
+ alpha_boot <- (var(Y_boot) - cov(X_boot, Y_boot)) / (var(X_boot) + var(Y_boot) - 2*cov(X_boot, Y_boot))
+ alpha_vec[i] <- alpha_boot
+ }
sd(alpha_vec)
par(mar = c(5,5,0.1,0.1))
hist(alpha_vec, col = miscolores[2], xlab = expression(alpha), main = "")
因此,α^的bootstrap估计是0.0904551。上面代码中,B表示的是bootstrap样本的个数。我们也可以上面的代码放在函数中,方便之后使用,
boot_portfolio <- function(X, Y, B = 100) {
+
+ n <- length(X)
+ alpha_vec <- rep(0, B)
+
+ for(i in 1:B) {
+ idx <- sample(1:n, n, replace = T)
+ X_boot <- X[idx]
+ Y_boot <- Y[idx]
+ alpha_boot <- (var(Y_boot) - cov(X_boot, Y_boot)) / (var(X_boot) + var(Y_boot) - 2*cov(X_boot, Y_boot))
+ alpha_vec[i] <- alpha_boot
+ }
+ sd(alpha_vec)
+ }
set.seed(8)
boot_portfolio(X, Y, 100)
boot_portfolio(X, Y, 1000)
R的包boot中有函数boot()也可以帮助我们实现boottrap。使用函数boot()之前,我们先做一些准备,
函数alpha_fn()主要的功能是计算α的估计值,只是alpha_fn()可以通过改变参数index, 实现改变计算bootstrap样本的α。如果index = 1:n, 那么data X [ i n d e x ] 和 d a t a X[index]和data X[index]和dataY[index]就是原数据的data X 和 d a t a X和data X和dataY。这时得到的就是原数据的α估计值。
alpha_fn <- function(data, index) {
+ X <- data$X[index]
+ Y <- data$Y[index]
+ return((var(Y) - cov(X, Y)) / (var(X) + var(Y) - 2*cov(X, Y)))
+ }
lpha_fn(Portfolio, index = 1:100)
如果index表示的是从1:n等可能有放回抽取的长度为n的向量,那么data X [ i n d e x ] 和 d a t a X[index]和data X[index]和dataY[index]是一个bootstrap样本。这时函数alpha_fn()返回的结果就是一次bootstrap样本的α估计值。
idx <- sample(1:100, 100, replace = T)
alpha_fn(Portfolio, index = idx)
现在我们可以使用函数boot()实现bootstrap, 不需要自己使用for循环:
set.seed(123)
boot(Portfolio, alpha_fn, R = 1000)
通过这个方法得到的α的估计的标准差是0.0875, 而通过我们自己写的函数boot_portfolio()计算的标准差是0.091(bootstrap的次数是1000时),两者是比较接近的。
通过本章的学习,我们知道了在现实的数据挖掘中,我们怎么去评价一个模型的好坏,或者选择更好的模型的方法,包括验证集方法,留一法,k折交叉验证法,自助法。那在下一章中,我们就来解决本章开头没有解决的问题:通过数学修正Cp , AIC ,BIC等将训练错误率修正估计训练错误率,以及如何在选定模型后选择合适的特征建模,包括子集选择,压缩估计和降维。
参考文献
[1]加雷斯.詹姆斯,丹妮拉.威腾 .统计学习导论[M].北京:机械工业出版社,2015.6.
[2]李航.统计学习方法[M].北京:清华大学出版社,2012.3.
[3]杰克.万托布拉斯.Python数据科学手册[M].北京:人民邮电出版社,2018.2.
[4]富朗索瓦.肖莱.Python深度学习[M].北京:人民邮电出版社,2018.8