给模型加个油

作者: 鸣人吃土豆 | 来源:发表于2018-06-28 09:30 被阅读97次

    最近半年有点偷懒了,想想不能再这么过了,要改过自新,把自己学到的用到的做个总结和分享,任重道远。首先小编我是个运营,所以要总结分享的也是和运营搭边的东西。其次,之所以第一篇分享是关于模型提升的东西,是因为最近小编在干这活,顺便记录下。第三,数据运营是个技术活,但是更大的是艺术活,更加需要的是分析思维,所以后续的分享会夹带着技术和思维方面的,尤以思维为主。毕竟关于技术,网上已经满天飞了,我也不凑这个热闹了,还是多补补思维方面。

    好了,废话不多说,上正题。

    在机器学习中,如何选用合适的算法进行预测是一个很重大的问题,其中如何提升模型的预测能力也是对我们的一个考验,对于同样的算法,使用不同的参数方法会得到不同性能的模型。所以我们要掌握提升模型性能的方法,更好的服务于我们的工作


    1.将数据分为训练集和测试集

    在模型选择中,我们要估计不同模型的性能并从中选出最优的模型。这里的最优指的是模型对新数据的泛化性能最优。在给定训练集的情况下,模型在训练集上的错误并不是该模型泛化性能的良好估计。当我们增加模型的复杂度时,模型在训练集上的错误会逐渐降低,只要模型够复杂,错误基本会降到或者接近0。

    但是这并不代表在新的数据集上的表现也是这么好的,很有可能我们的模型过拟合了。为了更加准确地估计模型的泛化性能,在训练和选择模型的过程中,可以将训练数据分为两部分

    训练集(training set)

    测试集(validation set)

    在训练集上,根据选定的模型参数训练出相应的模型。由于检验集上的数据相对训练集

    来说都是新的,因此可以计算训练所得的模型在检验集上的错误,从而估计所得模型的泛化

    性能。在实际中,可以使用 2/3 的数据来训练样本,而使用剩余的 1/3 作为检验集来估计模型的性能。

    这个可以利用后面会使用的createDataPartition 函数来进行分层抽样:在分类问题中按照正类和负类样本的比例取样,使得抽样取得的样本集中的正负类样本的比例和原数据集保持不变

    2.交叉验证和参数调整

    还有一种情况也会经常遇到,我们所持有的数据量并不是很大,在这种情况下,测试集必定会比较少,使用测试集来估计模型的性能可能会存在偏差。

    而利用交叉检验,整个训练集都可以作为测试集,同时也能使用整个训练集来训练模型,

    因此对于模型性能的估计会更加准确。

    具体来讲,有以下三种交叉方式

    2.1 Holdout 验证 一般来说,Holdout 验证并非一种交叉验证,因为数据并没有交叉使用。 随机从最初的样本中选出部分,形成交叉验证数据,而剩余的就当做训练数据。 一般来说,少于原本样本三分之一的数据被选做验证数据。
    2.2 K-fold cross-validation K折交叉验证,初始采样分割成K个子样本,即下图中的Training folds。一个单独的子样本被保留作为验证模型的数据(Test fold),其他K-1个样本用来训练。交叉验证重复K次(即下图中的iteration),每个子样本验证一次,平均K次的结果或者使用其它结合方式,最终得到一个单一估测。这个方法的优势在于,同时重复运用随机产生的子样本进行训练和验证,每次的结果验证一次,10折交叉验证是最常用的。

    image

    这里把整个思路写一下:

    以下以R语言的for循环来写的

    确定一组要比较的模型参数P1,P2,....Pm
    将训练集S划分为k个含有相同样本数的子集:S=S1∪S2∪...∪Sk
    for(i in 1:m){
       for(j in 1:k){
         使用S-Sj作为训练集,用参数Pi来训练模型fij
         使用Sj作为测试集,得到模型fij在测试集上的输出
    }
    将输出综合起来,得到使用模型参数Pi得到的所有输出
    }
    比较每个模型参数对应的输出,选出最优的参数P
    使用整个训练集S和P来训练一个新的模型f作为输出
    

    2.3 留一验证 留一验证(LOOCV)意指只使用原本样本中的一项来当做验证资料, 而剩余的则留下来当做训练资料。 这个步骤一直持续到每个样本都被当做一次验证资料。 事实上,这等同于和K-fold 交叉验证是一样的,其中K为原本样本个数。

    2.4利用R的caret包来实现交叉验证和参数调整

    我们在参数调整前需要明确以下3个问题:

    1。需要使用数据来训练哪种机器学习模型 2。哪些模型的参数是可以调整的,他们能调整的空间有多大 3。使用哪些评价标准来评估模型从而找到最优的参数来建模

    • 在使用caret包之前,我们可以自己试着来写一下,以更好的掌握交叉验证的原理,虽然有轮子,但是也不能做个绝对的调包侠吧
    
    library(glmnet)
    
    #导入数据
    data <- read.csv("C:\\Users\\Administrator\\OneDrive\\study\\data\\pima-indians-diabetes.csv")
    str(data) #发现目标变量为数值型
    data$class <- as.factor(data$class) #将目标变量转化为因子型
    
    #准备好数据X和y,因为glmnet包的要求,数据的数据要是矩阵,所以X是矩阵,y是一个向量
    X <- data[,!names(data)=='class'] #去除目标变量
    X <- as.matrix(X)
    y <- data$class
    
    #将数据随机分成k个子集,且保证每个子集的大小一致(如果样本总数不是 k 的倍数,则要求每个子集的大小尽量接近)
    k <- 10 #设定为10折
    n <- nrow(data)
    set.seed(0) #设定随机数生成器的种子,以便我们重复实验结果
    
    v_random <- sample(n) #产生一个 1 ~n 的随机排列
    v_fold <- rep(1,n) #包含每个样本所对应子集的编号
    sample_size_per_fold <- floor(n/k) #向下取整得到不同子集的个数
    
    for(i in 1:k){
      range_min = (i-1)*sample_size_per_fold+1 #得到不同iteration在数据集中的最小序号
      range_max = i*sample_size_per_fold
      v_fold[(v_random>=range_min) & (v_random<=range_max)] <- i
    }
    
    #针对L2范式lambda 参数,使用交叉检验来训练模型,并计算在测试集上的预测值,最后将结果保存在矩阵 Y_valid 中。
    
    lamdba_list <- c(0,0.001,0.005, 0.01, 0.02, 0.03, 0.05, 0.1, 1)
    param_num <- length(lamdba_list)
    
    Y_valid <- matrix(0,n,param_num)
    
    for(i in 1:param_num){
      lamdba <- lamdba_list[i]
      y_valid <- rep(0,n)
      for(j in 1:k){
        X_train <- X[v_fold!=j,]
        y_train <- y[v_fold!=j]
        X_valid <- X[v_fold==j,]
        #训练模型
        Mj <- glmnet::glmnet(X_train,y_train,family = 'binomial',lambda = lamdba)
        #测试模型
        y_valid_j <- predict(Mj,X_valid,type='class')
        y_valid[v_fold==j] <- y_valid_j
      }
      Y_valid[,i] <- y_valid 
    }
    
    #计算每个 lambda 值对应的准确率,选择最优的 lambda 值并利用整个训练集重新训练一个模型。
    accuracy_list <- rep(0,param_num)
    for(i in 1:param_num){
      accuracy_list[i] <- sum(Y_valid[,i]==1)/n
    }
    lambda_optimal <- lamdba_list[which.max(accuracy_list)]
    
    print(paste0('lambda_optimal = ', lambda_optimal))
    M_optimal <- glmnet(X,y,family = 'binomial',lambda = lambda_optimal)
    
    • 自己写很麻烦,所以我们再利用caret包中的函数来实现

    R虽然很强大,有很多包,但是R中的包很杂乱,没有统一化,比如在 rpart 包中,我们可以直接使用保存为数据框的输入数据,但在 glmnet 中,输入数据只能是矩阵而不能是数据框导致深入的学习曲线上升,这也是很多人转到python的原因之一吧。但是caret包为 R 中超过 200 个包提供了统一的接口,方便了我们的使用

    具体来说,在caret 包中,常用的函数包括train和predict,此外还有一个重要的辅助函数 trainControl 。利用 train 函数,可以直接指定训练数据、挑选模型的方法(如交叉检验)、挑选模型的指标(如回归中的 RMSE 和 R 2 ,分类问题中的准确率和 ROC 等)、调用的包(如 glmnet 或者 rpart )、交叉检验中要测试的参数等。这些参数有一些可以在 train 函数中直接设置,有些需要在 trainControl 中设置。 得到最终选定的参数后, caret 使用所有的数据重新训练一个模型并返回。之后我们可以直接使用 predict 函数(实质是 caret 包中的 predict.train )函数处理新数据。

    train 函数的主要参数如下: train 支持两种模式: train(x, y) 和train(formula, data) 。
    • method :一个字符串,用于指定要调用的模型。使用 names(getModelInfo()) 可以 得到当前 caret 包中所支持的所有模型的列表。
    • metric :字符串,用来指定在模型选择中要优化的指标。在分类中可以是 'Accuracy' 或 'ROC', 'Kappa',在回归中可以是 'RMSE' 或 'Rsquared' 。默认情况下,caret在选择最优模型时,通过这些性能度量指标的最大值来选择。
    • trControl :一个列表,表示进行模型选择时的控制参数。通常,我们先使用 trainControl 得到一个列表,再赋给 trControl 。
    • tuneGrid :一个数据框,表示在模型选择中我们要考虑的参数可能的取值。该数据 框的列名必须与对应模型中的控制参数同名。如果不提供,则 caret 自动选择要遍历 的参数值。caret默认对每个参数最多搜索3个可能值,假设一共有p个参数,而每个参数至少有3个选项时, 这意味着3的P次方个候选模型将会被测试。
    • preProcess :一个字符串,表示对于数据的预处理操作,如 'center' 、 'scale' 等。 在默认状态下,不进行任何预处理 trainControl 函数时,通常要使用的主要参数有以下几个。
    • method :一个字符串,用来设置重抽样的方法。最常用的是 'repeatedcv' ,表示进行多次交叉检验。
    • number :一个整数,表示进行交叉检验时的重数。
    • repeats :一个整数,表示进行 repeatedcv 时的重复次数。
    • classProbs : TRUE 或者 FALSE ,表示在分类模型中是否计算样本属于每个类别的概 率。例如,在计算 ROC 时,我们就需要明确计算概率。
    • selectionFunction:字符串,设定一函数用来在各个参数中选择最优的模型。可以选择3个函数。best函数简单的选择具有最好的metric(train函数中)值的参数,这是默认的;oneSE函数选择最好性能标准差之内的最简单的参数;Tolerance函数选择某个指定比例之内的最简单的参数

    image

    在caret中,主要使用重取样( resampling )的方法来优化模型参数从而进行模型选择。重取样的思想是:从整个训练集中采用某种取样方法得到一个新的数据集,在该数据集上训练模型,并将原始训练集剩余的样本作为检验集来计算模型的性能;重复该过程多次,从而找出最优的模型参数。在交叉检验中,我们把整个训练集分成若干等份,然后在每次重取样时去掉一个子集作为检验集,剩余的数据作为训练集。在 caret 中,还支持除交叉检验以外的其他重取样方法,如每次可以进行 bootstrap 取样。在重取样中,一旦取样数据确定,可以并行地训练多个模型以加快计算速度。在 R 中,可以使用 foreach 包及相关的包来实现。

    比如 下面的代码中,查看M1模型

    image
    
    trControl <- caret::trainControl(method = 'repeatedcv',
                                     number = 10,
                                     repeats = 3)
    #在前面直接使用 glmnet 时,需要将输入数据显式地转换为矩阵,而通过 caret 调用 glmnet 时,不需要显式转换。
    
    #第一个逻辑回归
    M1 <- caret::train(class~.,data=data,method='glmnet',trControl=trControl)
    
    #检查使用多次交叉检验之后模型的选择结果
    M1$bestTune #返回选择的最优模型对应的参数。在 glmnet中为对应的alpha值和lambda值。
    M1$method #返回模型对应的包,这里是 glmnet 。
    M1$metric #返回在交叉检验中优化的性能指标,这里是分类中的准确率
    M1$modelType #返回模型的类型,这里是分类
    M1$results #返回在交叉检验中不同的参数对应的性能指标
    attributes(M1) #查看M模型中所有的结果
    
    #第二个逻辑回归,指定在交叉检验中要比较的不同参数值
    lambda_list <- c(0, 0.001, 0.005, 0.01, 0.02, 0.03, 0.05, 0.1, 1)
    alpha_list <- c(0, 0.1)
    myParamGrid <- expand.grid(lambda=lamdba_list,alpha=alpha_list) #生成一个9*2行的数据框,列名必须要和控制的参数名一样
    trControl2 <- caret::trainControl(method = 'repeatedcv',
                                      number=10,
                                      repeats = 3)
    M2 <- caret::train(class~.,data=data,method='glmnet',trControl=trControl2,tuneGrid=myParamGrid)
    M2$bestTune
    
    #第三个逻辑回归,不是最大化Accuracy(准确率),而是最大化AUC(Area Under ROC Curve)
    #在计算 AUC时,我们需要知道样本属于每个类的概率,所以将classProbs设为TRUE;同时将计算预测结果好坏的函数置
    #为caret包中的twoClassSummary 函数
    library(caret)
    trControl3 <- trainControl(method = 'repeatedcv',
                                      number=10,
                                      repeats = 3,
                                      classProbs = TRUE,
                                      summaryFunction = twoClassSummary)
    
    data <- read.csv("C:\\Users\\Administrator\\OneDrive\\study\\data\\pima-indians-diabetes.csv")
    
    #这里要着重讲一下,因为我们现在是要计算概率,原先的因子值0和1已经不适用了,得利用factor函数中的
    #levels和labels参数重新设置因子值
    data$class <- factor(data$class,levels=c(0,1),labels=c("No","Yes"))
    str(data)
    
    M3 <- train(class~.,data=data,
                       method='glmnet',
                       trControl=trControl3,
                       metric='ROC',tuneGrid=myParamGrid)
    M3$bestTune
    
    #第四个逻辑回归,没有使用指定参数
    M4 <- train(class~.,data=data,
                method='glmnet',
                trControl=trControl3,
                metric='ROC')
    M4$bestTune
    
    #第五个逻辑回归,先进行分层抽样
    trainInd <- createDataPartition(y=data$class,p=0.6,list = FALSE)
    train <- data[trainInd,]
    test <- data[-trainInd,]
    
    trControl5 <- trainControl(method='repeatedcv',
                               number = 10,
                               repeats = 3)
    M5 <- train(class~.,data=train,
                method='glmnet',
                trControl=trControl5)
    
    #这里我们直接用了predict函数来进行预测,这个函数直接用于train()对象有额外的好处,
    #注意不是调用finalModel子对象或者使用最佳模型的参数来训练新模型。
    #首先,train()函数应用到数据中的任何数据预处理方法也会用类似的方式应用到产生预测的数据中。
    #这包括中心化和标准化(使用k近邻)的数据转换、缺失值处理以及一些其他方法。
    #这确保用来建模的数据准备步骤在模型部署后仍然保留。
    #其次,predict()函数提供了标准的接口用来得到预测的类别值和概率
    #预测的类别会默认提供。但是如果想要得到每一类估计的概率,可以添加参数type='prob'
    #即使有些情况下底层的模型会使用不同的字符串来表示预测概率值(比如,朴素贝叶斯模型使用"raw"),
    #但caret自动在后台将type="prob"转成其需要的形式。
    predict(M5,newdata=test)
    M5$bestTune
    
    

    caret中的参数调优是有限制的,比如glmnet只能调整alpha和lambda两个参数,而rpart只能调整cp这个参数,即利用expand.grid生成的数据库只能有cp这一列

    如果要调节更多的参数,有两种选择:

    • 继续使用 caret 包,但需要做更多的定制化工作;

    • 不使用 caret 包,自己直接使用对应的包,并实现交叉检验的对应代码。

    在一般情况下,如果模型所对应的包不是特别生僻,那么通常推荐第二种方法

    大家可以去caret官方网站查看每种方法可以调试的参数http://topepo.github.io/caret/available-models.html

    嗯,关于模型调优的方法就先总结到这吧,后面一篇总结下模型评价的方法,其实在我们这篇中已经用到了两个指标了:Accuracy和AUC
    这篇是以R来写的,python的话可以看看这篇文章:模型介绍、评估和参数调优(附代码)
    赛有拉拉

    相关文章

      网友评论

      本文标题:给模型加个油

      本文链接:https://www.haomeiwen.com/subject/zqnwyftx.html