R语言机器学习框架mlr3学习笔记

mlr3verse基础

mlr3verse其实是个框架,整合了各种机器学习算法。其底层的使用R6语法和data.table数据流;支持并行,可以搭建”图”流学习器。

几点技术说明:

  • 使用帮助:?+对象名字,或者对象$help,如learner$help().
  • 某些对象不是数据框,as.data.table()转化成数据框再进行查看。
  • 超参数调参需要找到原始函数对应的包的函数帮助。

任务类型

  • 连续的目标变量:回归
  • 离散的目标变量:分类
  • 没有目标:聚类和降维等

查看自带的任务:

1
2
tsks()

1
2
3
4
5
Show in New Window
<DictionaryTask> with 28 stored values
Keys: actg, bike_sharing, boston_housing, breast_cancer, faithful, gbcs, german_credit, grace, ilpd, iris, kc_housing, lung,
moneyball, mtcars, optdigits, penguins, penguins_simple, pima, precip, rats, sonar, spam, titanic, unemployment, usarrests, whas,
wine, zoo

创建任务

使用包仔带的数据并创建任务。

1
2
3
4
5
6
7
8
9
10
# 提取包中的数据
dat = mlr3::tsk("german_credit")$data()

dat

# 创建任务
task = mlr3::as_task_classif(dat, target = "credit_risk")

# 查看任务
task
1
2
3
4
5
6
7
8
<TaskClassif:dat> (1000 x 21)
* Target: credit_risk
* Properties: twoclass
* Features (20):
- fct (14): credit_history, employment_duration, foreign_worker, housing, job, other_debtors, other_installment_plans,
people_liable, personal_status_sex, property, purpose, savings, status, telephone
- int (3): age, amount, duration
- ord (3): installment_rate, number_credits, present_residence

剔除某列

1
2
3
4
5
6
7
# 剔除某列

task$select(
cols = dplyr::setdiff(task$feature_names, "telephone")
)

task
1
2
3
4
5
6
7
8
<TaskClassif:dat> (1000 x 20)
* Target: credit_risk
* Properties: twoclass
* Features (19):
- fct (13): credit_history, employment_duration, foreign_worker, housing, job, other_debtors, other_installment_plans,
people_liable, personal_status_sex, property, purpose, savings, status
- int (3): age, amount, duration
- ord (3): installment_rate, number_credits, present_residence

划分数据

划分数据得到的是行号,并不是直接将数据分成两份。

1
2
3
4
5
6
7
set.seed(1)

split = mlr3::partition(task, ratio = 0.7, stratify = TRUE) # 70%作为训练集,默认分层抽样

split$train

split$test

学习器

学习器就是来自不同包里面的算法。

1
mlr3::lrns()
1
2
3
4
5
6
7
<DictionaryLearner> with 53 stored values
Keys: classif.cv_glmnet, classif.debug, classif.featureless, classif.glmnet, classif.kknn, classif.lda, classif.log_reg,
classif.multinom, classif.naive_bayes, classif.nnet, classif.qda, classif.ranger, classif.rpart, classif.svm, classif.xgboost,
clust.agnes, clust.ap, clust.cmeans, clust.cobweb, clust.dbscan, clust.diana, clust.em, clust.fanny, clust.featureless, clust.ff,
clust.hclust, clust.kkmeans, clust.kmeans, clust.MBatchKMeans, clust.meanshift, clust.pam, clust.SimpleKMeans, clust.xmeans,
dens.hist, dens.kde, regr.cv_glmnet, regr.debug, regr.featureless, regr.glmnet, regr.kknn, regr.km, regr.lm, regr.ranger,
regr.rpart, regr.svm, regr.xgboost, surv.coxph, surv.cv_glmnet, surv.glmnet, surv.kaplan, surv.ranger, surv.rpart, surv.xgboost

设定学习器的方法如下:

1
2
learner = mlr3::lrn("classif.ranger", num.trees = 100, predict_type = "prob") # 参数应该要超参数调参才可以的,通常。
learner
1
2
3
4
5
6
7
<LearnerClassifRanger:classif.ranger>
* Model: -
* Parameters: num.threads=1, num.trees=100
* Packages: mlr3, mlr3learners, ranger
* Predict Type: prob
* Feature types: logical, integer, numeric, character, factor, ordered
* Properties: hotstart_backward, importance, multiclass, oob_error, twoclass, weights

开始训练模型

1
2
3
learner$train(task, row_ids = split$train)

learner$model # 这个模型就和普通的R包训练得到的结果是一样的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Ranger result

Call:
ranger::ranger(dependent.variable.name = task$target_names, data = task$data(), probability = self$predict_type == "prob", case.weights = task$weights$weight, num.threads = 1L, num.trees = 100L)

Type: Probability estimation
Number of trees: 100
Sample size: 700
Number of independent variables: 19
Mtry: 4
Target node size: 10
Variable importance mode: none
Splitrule: gini
OOB prediction error (Brier s.): 0.1615879

进行预测

1
2
prediction = learner$predict(task, row_ids = split$test)
prediction

模型性能评估

查看性能度量指标:

1
mlr3::msrs()
1
2
3
4
5
6
7
8
9
10
11
12
<DictionaryMeasure> with 87 stored values
Keys: aic, bic, classif.acc, classif.auc, classif.bacc, classif.bbrier, classif.ce, classif.costs, classif.dor,
classif.fbeta, classif.fdr, classif.fn, classif.fnr, classif.fomr, classif.fp, classif.fpr, classif.logloss,
classif.mbrier, classif.mcc, classif.npv, classif.ppv, classif.prauc, classif.precision, classif.recall,
classif.sensitivity, classif.specificity, classif.tn, classif.tnr, classif.tp, classif.tpr, clust.ch, clust.db,
clust.dunn, clust.silhouette, clust.wss, debug, dens.logloss, oob_error, regr.bias, regr.ktau, regr.mae, regr.mape,
regr.maxae, regr.medae, regr.medse, regr.mse, regr.msle, regr.pbias, regr.rae, regr.rmse, regr.rmsle, regr.rrse,
regr.rse, regr.rsq, regr.sae, regr.smape, regr.srho, regr.sse, selected_features, sim.jaccard, sim.phi, surv.brier,
surv.calib_alpha, surv.calib_beta, surv.chambless_auc, surv.cindex, surv.dcalib, surv.graf, surv.hung_auc,
surv.intlogloss, surv.logloss, surv.mae, surv.mse, surv.nagelk_r2, surv.oquigley_r2, surv.rmse, surv.schmid,
surv.song_auc, surv.song_tnr, surv.song_tpr, surv.uno_auc, surv.uno_tnr, surv.uno_tpr, surv.xu_r2, time_both,
time_predict, time_train
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看准确率
prediction$score(msr("classif.acc"))

# 混淆矩阵

prediction$confusion

# 查准率
prediction$score(msr("classif.precision"))

# 召回率
prediction$score(msr("classif.recall"))

# 绘制ROC曲线
mlr3verse::autoplot(prediction, type = "roc")

# AUC曲线
prediction$score(msr("classif.auc"))

000012

重抽样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 支持的重抽样方法
mlr3::rsmps()

# 10倍交叉验证
cv10 = mlr3::rsmp("cv", folds = 10) # 抽样策略

# 实例化
cv10$instantiate(task)

# 使用重抽样
# 需要提供任务、学习器和重抽样方法
rr = mlr3::resample(task, learner, cv10, store_models = TRUE) # 保存每次的模型

# 查看平均准确率
rr$aggregate(msr("classif.acc"))

# 查看每次的准确率
rr$score(msr("classif.acc"))

rr$prediction()

基准测试

基准测试指的是比较不同的算法、在多个任务或者是不同抽样策略上的平均表现。
测试时要保证测试的公平性,也就是喂给每个算法的数据必须是一样的,也就是重抽样得到的数据应该同时喂给每个算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 构建任务
tasks = mlr3::tsk("sonar")

# 选择学习器
learners = mlr3::lrns(c("classif.rpart", # 决策树
"classif.kknn", # KNN
"classif.ranger", # 随机森林
"classif.svm"), # 支持向量机
predict_type = "prob")

# 构建基准测试
design = mlr3::benchmark_grid(tasks, learners, mlr3::rsmps("cv", folds = 10))
design

# 执行基准测试
bmr = mlr3::benchmark(design = design)

# 查看平均的结果
bmr$aggregate(list(msr("classif.acc"),
msr("classif.auc")))

# ROC曲线
mlr3verse::autoplot(bmr, type = "roc")

# AUC图
mlr3verse::autoplot(bmr, measure = msr("classif.auc"))

图学习器

图学习器的主要用途:

  • 特征工程:缺失值插补、特征提取、特征选择、不均衡数据处理……
  • 集成学习:装袋法、堆叠法
  • 分支训练和分块训练
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 简单的线性图
graph = mlr3verse::po("scale") %>>% # 数据归一化
mlr3verse::po("encode") %>>% # 数据重编码
mlr3verse::po("imputemedian") %>>% # 中位数插补
mlr3::lrn("classif.rpart") # 接上学习器

# 图可视化
graph$plot()

# 转换为学习器
gl = mlr3::as_learner(graph)

# 训练任务
task = tsk("iris")

gl$train(task)

# 调试图

# 设置学习器超参数
graph$pipeops$scale$param_set$values$center = FALSE

# 取单独的PipeOp的$state
graph$keep_results = TRUE
graph$train(task)
graph$pipeops$scale$state$scale

# 查看中间结果
graph$pipeops$scale$.result[[1]]$head()

特征工程

加载需要的包

1
2
3
4
5
library(tidyverse)
library(mlr3verse)
library(mlr3pipelines)
library(ggthemes)
library(ggplot2)

什么是特征工程

简单来说就是将自变量转换成能够更好的表达问题本质的新变量的过程,也就是对自变量进行处理。
数据清洗和特征工程属于机器学习中的数据预处理过程,使用mlr3pipelines这个包来实现。

选择器函数

  • selector_all(): 选择所有特征
  • selector_none():不选择任何特征
  • selector_type():根据类型选择特征
  • selector_name():根据特征名字选择特征
  • selector_grep():利用正则表达式选择特征
  • selector_invert():反选某个选择器的特征
  • selector_intersect/union/setdiff():两个选择器特征的交集、并集和差集
  • selector_missing():选择含有缺失值的特征
  • selector_cardinality_greater_than():根据分类特征个数阈值进行筛选

affcet_columns参数

这个参数组要用于说明作用于哪些列。

查看所有的PipeOp

1
mlr3pipelines::po()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
Show in New Window
<DictionaryPipeOp> with 74 stored values
Keys: boxcox, branch, chunk, classbalancing, classifavg, classweights, colapply, collapsefactors, colroles, compose_crank, compose_distr,
compose_probregr, copy, crankcompose, datefeatures, distrcompose, encode, encodeimpact, encodelmer, featureunion, filter, fixfactors,
histbin, ica, imputeconstant, imputehist, imputelearner, imputemean, imputemedian, imputemode, imputeoor, imputesample, kernelpca, learner,
learner_cv, missind, modelmatrix, multiplicityexply, multiplicityimply, mutate, nmf, nop, ovrsplit, ovrunite, pca, proxy, quantilebin,
randomprojection, randomresponse, regravg, removeconstants, renamecolumns, replicate, scale, scalemaxabs, scalerange, select, smote,
spatialsign, subsample, survavg, targetinvert, targetmutate, targettrafoscalerange, textvectorizer, threshold, trafopred_regrsurv,
trafopred_survregr, trafotask_regrsurv, trafotask_survregr, tunethreshold, unbranch, vtreat, yeojohnson
Show in New Window
<ParamSetCollection>
R Console
Description:data.table [18 × 7]
id
<chr>
class
<chr>
scale.center ParamLgl
scale.scale ParamLgl
scale.robust ParamLgl
scale.affect_columns ParamUty
pca.center ParamLgl
pca.scale. ParamLgl
pca.rank. ParamInt
pca.affect_columns ParamUty
classif.rpart.cp ParamDbl
classif.rpart.keep_model ParamLgl
1-10 of 18 rows | 1-2 of 7 columns
data.table
18 x 7
Description:data.table [18 × 7]
id
<chr>
class
<chr>
lower
<dbl>
upper
<dbl>
nlevels
<dbl>
default
<list>
value
<list>
scale.center ParamLgl NA NA 2 <lgl [1]> <NULL>
scale.scale ParamLgl NA NA 2 <lgl [1]> <NULL>
scale.robust ParamLgl NA NA 2 <S3: NoDefault> <lgl [1]>
scale.affect_columns ParamUty NA NA Inf <S3: Selector> <NULL>
pca.center ParamLgl NA NA 2 <lgl [1]> <NULL>
pca.scale. ParamLgl NA NA 2 <lgl [1]> <NULL>
pca.rank. ParamInt 1 Inf Inf <NULL> <int [1]>
pca.affect_columns ParamUty NA NA Inf <S3: Selector> <NULL>
classif.rpart.cp ParamDbl 0 1 Inf <dbl [1]> <NULL>
classif.rpart.keep_model ParamLgl NA NA 2 <lgl [1]> <NULL>
1-10 of 18 rows
Show in New Window
Description:data.table [8 × 1]
key
<chr>
imputeconstant
imputehist
imputelearner
imputemean
imputemedian
imputemode
imputeoor
imputesample
8 rows
Show in New Window
diabetes age pedigree pregnant glucose insulin mass pressure triceps
0 0 0 0 0 0 0 0 0

如何理解

以“标准化”这个管道为例,其中$train()接收训练数据,对输入数据进行标准化处理,在这个过程中训练好的参数(平均值和标准差)将作为state保存下来。在predict()的时候是直接调用state中保存好的参数,而不是重新计算标准差和平均值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
gr = mlr3pipelines::po("scale") %>>% mlr3pipelines::po("pca", rank. = 2) # 保留的主成分数量
gr$plot()

# 调试
task = mlr3::tsk("iris")

gr$train(task)[[1]]$data()

# 训练
gr$predict(task$filter(1:5))[[1]]$data()

# 原始任务经过特征工程后再用于机器学习流程
new.task = gr$train(task)[[1]]
new.task

# 后面可以再接一个机器学习算法
gr = gr %>>% mlr3::lrn("classif.rpart")
gr$plot()

# 转换为学习器
gr_learner= mlr3::as_learner(gr)
gr_learner

# 查看所有调参参数
gr_learner$param_set

缺失值插补

1
2
3
4
5
6
# 查看目前支持的方法
#as.data.table(mlr_pipeops)[tags %in% "missings", "key"]
mlr3pipelines::mlr_pipeops %>%
mlr3verse::as.data.table() %>%
dplyr::filter(tags %in% c("missings", "key")) %>%
dplyr::select("key")# tidyverse语法
1
2
3
4
5
6
7
8
imputeconstant				
imputehist
imputelearner
imputemean
imputemedian
imputemode
imputeoor
imputesample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 简单插补
task = tsk("pima")

task$missings() # 查看缺失值

# 特定列进行插补
po = mlr3pipelines::po("imputeconstant",
param_vals = list(
constant = -999,
affect_columns = selector_name("glucose")
))
new.task = po$train(list(task = task))[[1]] # 这个地方的训练相当于对原始数据进行缺失值的常数插补
new.task$missings()

# 均值插补
po = mlr3pipelines::po("imputemean")
new.tak = po$train(list(task = task))[[1]]
new.tak$missings()

# 中位数插补
po = mlr3pipelines::po("imputemedian")
new.tak = po$train(list(task = task))[[1]]
new.tak$missings()

# 众数插补
po = mlr3pipelines::po("imputemode")
new.tak = po$train(list(task = task))[[1]]
new.tak$missings()

# 随机抽样插补
# 从非缺失的训练数据中随机抽样进行插补
po = mlr3pipelines::po("imputesample")
new.tak = po$train(list(task = task))[[1]]
new.tak$missings()

# 直方图法插补
# 相当于把数据进行划分,然后进行插补
po = mlr3pipelines::po("imputehist")
new.tak = po$train(list(task = task))[[1]]
new.tak$missings()

学习器插补

为每个特征变量拟合一个学习器来插补特征。
学习器支持的特征才可以被插补,也就是说回归类型的学习器只能插补整数和数值特征,分类类型的学习器可以插补因子、有序因子和逻辑特征值等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 决策树插补
po = mlr3pipelines::po("imputelearner", lrn("regr.rpart"))
new.task = po$train(list(task = task))[[1]]
new.tak$missings()

# KNN插补
# 先用直方图法插补再用KNN学习器插补
po = mlr3pipelines::po("imputelearner", mlr3pipelines::po("imputehist") %>>% lrn("regr.kknn"))
new.task = po$train(list(task = task))[[1]]
new.tak$missings()

# 超出范围插补
# 适用于基于树的模型
# 增加一个新的水平".MISSING"来插补因子特征。
po = mlr3pipelines::po("imputeoor")
new.task = po$train(list(task = task))[[1]]
new.task$missings()

特征缩放

不同特征之间的差异可能会很大,对模型的影响可能会很大,因此需要做归一化处理,也就是对特征进行缩放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 标准化
# 均值为0,标准差为1,如果是正态分布的数据则最佳

pos = mlr3pipelines::po("scale") # scale = FALSE表示只做中心化
pos$train(list(task = task))[[1]]$data()

# 归一化
# 将数据放缩到[0,1]区间

pop = mlr3pipelines::po("scalerange", param_vals = list(lower = 0, upper = 1))
pop$train(list(task = task))[[1]]$data()

# 行规范化
# 简单理解就是对行进行处理
pop = mlr3pipelines::po("spatialsign")
pop$train(list(task))[[1]]$data()

特征变换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 计算新特征
# 比如可以根据病害等级计算病情指数
task = mlr3::tsk("iris")
po = mlr3pipelines::po("mutate",mutation = list(
x1_p = ~Sepal.Length + 1,
area1 = ~Sepal.Length * Sepal.Width,
area2 = ~Petal.Length * Petal.Width,
area = ~area1 + area2
))

po$train(list(task = task))[[1]]$data()

# 正态性变换
# Box-Cox变换可以将非负的非正态数据转换成正态分布数据,算法会自动计算参数

po = mlr3pipelines::po("boxcox")
pop$train(list(task))[[1]]$data()

# 如果数据中包含了0和负数,则需要用Yeo-Johnson变换
# 使用bestNormalize可以将变换后的数据转为原始数据
pop = mlr3pipelines::po("yeojohnson")
pop$train(list(task))[[1]]$data()

特征降维

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# PCA
# 不用选择所有主成分,一般85%以上就可以
po = mlr3pipelines::po("pca", rank. = 3)
po$train(list(tsk("iris")))[[1]]$data()

# 核PCA
# PCA用于线性降维,KPCA用于非线性降维,处理线性不可分的数据集
# 原理是将线性不可分的映射到高维空间,再进行PCA
po = mlr3pipelines::po("kernelpca", features = 3)
po$train(list(tsk("iris")))[[1]]$data() # iris数据集是线性可分的,此处只是举例

# ICA独立成分分析
# 提取统计意义上的独立成分
po = mlr3pipelines::po("ica", n.comp = 3)
po$train(list(tsk("iris")))[[1]]$data()

# NMF:非负矩阵分解
# 保证矩阵非负的同时能够减少数据量,相当于把n维的数据降到r维
po = mlr3pipelines::po("nmf", rank = 3)
po$train(list(tsk("iris")))[[1]]$data()

# 剔除常量特征
#
po = mlr3pipelines::po("removeconstants")
po$train(list(tsk("iris")))[[1]]$data()

# 因子合并
# 将数量少的因子合并到数量较多的因子中去
# 训练集中没有的因子水平在测试集中出现了也不会被使用
po = mlr3pipelines::po("collapsefactors", target_level_count = 2)
po$train(list(tsk("iris")))[[1]]$data()

# 因子编码
# 独热编码:分类型的转换成二值型,如一列有三种颜色,则需要转化成三列,如是否红色,是标为1,否标为0
# 虚拟编码相当于独热编码删除一列
task = mlr3::tsk("penguins")
poe = mlr3pipelines::po("encode", method = "one-hot")
poe$train(list(task))[[1]]$data()

不均衡数据处理

  • 欠采样:多数类只保留了一部分
  • 过采样:少数类超量采样

超参数调参

什么是超参数调参

简单来说揪是让模型自动找到最佳参数。
使用mlr3tuning实现。

工作内容

搜索空间、学习器、任务、重新抽样策略、模型性能度量指标及终止条件。

查看学习器超参数

需要先知道学习器包含哪些参数。

1
2
3
learner = mlr3::lrn("classif.svm")
learner$param_set$ids()
learner$param_set

独立参数

tune()进行超参数调参,需要设定以下参数:

  • method:调参方法,支持以下这些
    • grid_search:网络搜索
    • random_search:随机搜索
    • gensa:广义模拟退火
    • nloptr:非线性优化
  • task:任务
  • learner:带调参的学习器或普通学习器
  • resampling:重抽样策略
  • measures:模型性能评估指标,可以是多个
  • search_space:如果是普通的学习器则需要指定搜索空间
  • term_evals/term_time/terminator:终止条件,允许评估次数/允许调参时间多少秒/终止器对象
  • store_models:是否保存每次的模型
  • allow_hotstart:是否允许热启动预拟合模型
  • resolution:网格分辨率,网格搜索的配套参数
  • batch_size:批量大小,每批次放几组参数配置,以加快运算速度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 将测试集索引设置为留出
task = mlr3::tsk("iris")
split = mlr3::partition(task, ratio = 0.8)
task$set_row_roles(split$test, "holdout")

# 选择学习器,需要调参的用to_tune()设定搜索空间
learner = mlr3::lrn("classif.svm",
type = "C-classification",
kernel = "radial",
cost = to_tune(0.1, 10),
gamma = to_tune(0,5))

# 5折交叉验证
instance = mlr3tuning::tune(method = "grid_search",
task = task,
learner = learner,
resampling = rsmp("cv", folds = 5),
measures = msr("classif.acc"),
resolution = 5)

# 提取调参结果
instance$result

# 调参档案
instance$archive

# 调参结果可视化
mlr3viz::autoplot(instance,
type = "surface",
cols_x = c("cost", "gamma"))
# 最优参数更新学习器参数,在训练集上训练模型,然后在测试集上做预测
learner$param_set$values = instance$result_learner_param_vals
learner$train(task) # 之所以这样直接写是因为前面写了holdout

prediction = learner$predict(task, row_ids = split$test)
prediction

000002

自动调参器

自动调参器

独立调参的缺点:

  • 最优参数需要手动更新
  • 不方便实现嵌套重抽样

自动调参将超参数和学习器封装到一起,实现自动调参过程,并可以像其他的学习器一样使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 创建任务并选择学习器
task = mlr3::tsk("iris")
learner = mlr3::lrn(
"classif.svm",
type = "C-classification",
kernel = "radial"
)

# ps()生成自动搜索空间
search.space = mlr3verse::ps(cost = p_dbl(0.1, 10),
gamma = p_int(0, 5))

# auto_tuner创建自动调参器
# 不用提供任务

at = mlr3tuning::auto_tuner(
method = "grid_search",
learner = learner,
search_space = search.space,
resampling = rsmp("cv", folds = 5),
measure = msr("classif.acc"),
resolution = 5
)

# 在全部数据上自动调参,预测新数据
at$train(task)

dat = task$data()[1:5, -1]
at$predict_newdata(dat)

# 外层采样4折交叉验证重抽样并保存模型
# 执行嵌套重抽样,外层循环5次,内层的超参数调参循环5次
rr = mlr3::resample(task, at, rsmp("cv", folds = 5))


# 查看模型的平均性能
rr$aggregate() # 默认classif.ce
rr$aggregate(msr("classif.acc"))

# 将上述步骤改为嵌套调参
rr = mlr3tuning::tune_nested(
method = "grid_search",
task = task,
learner = learner,
search_space = search.space,
inner_resampling = rsmp("cv", folds = 5),
outer_resampling = rsmp("cv", folds = 4),
measure = msr("classif.acc"),
resolution = 5
)

# 查看模型的平均性能
rr$aggregate() # 默认classif.ce
rr$aggregate(msr("classif.acc"))

# 查看每次的
rr$score(msr("classif.acc"))

# 部分参数间具有依赖关系,需要进行声明
search.space = mlr3verse::ps(
cost = p_dbl(log(0.1), log(10), trafo = function(x) exp(x)),
kernel = p_fct(c("polynomial", "radial")),
degree = p_int(1,3, depends = kernel == "polynomial")
)

正则化回归

加载需要的包

1
2
3
4
5
library(tidyverse)
library(mlr3verse)
library(mlr3pipelines)
library(ggthemes)
library(ggplot2)

为什么使用正则化回归

多元线性回归时,部分变量之间存在多重共线性,剔除某个变量后回归系数会变化很大,得到的回归模型是“伪回归”,这时候就需要岭回归、Lasso回归或者是弹性网回归,这些方法都是基于一种正则化技术,可以减少过拟合。

岭回归

岭回归是一种改良后的最小二乘法,通过放弃最小二乘法的无偏性,损失部分信息、降低部分精度为代价,得到更符合实际、更可靠的回归方法。

需要注意的是岭回归中自变量的量纲对模型的影响非常大,因此需要对自变量进行标准化,保证所有自变量都是一个量纲上。

岭回归的最佳系数是通过梯度下降法优化得到的。

Lasso回归

Lasso回归和岭回归的差异在于惩罚项不同。

弹性网回归

弹性网回归是岭回归和Lasso回归的整合。

mlr3案例

mlr3调用glmnet包实现正则化回归。有两个学习器:

  • regr.glmnet:调用glmnet::glmnet()
  • regr.cv_glmnet:调用glmnet::cv.glmnet

两个函数的基本用法为:

1
2
glmnet::glmnet(x,y,family,alpha,lambda = NULL)
glmnet::cv.glmnet(x,y,family,alpha,lambda = NULL)

其中:

  • x:自变量
  • y:因变量
  • family:因变量的类型,默认是gaussian,还可以是binomial、poisson、multinomial、cox、mgaussian
  • alpha:弹性网混合系数,0为岭回归,1为Lasso回归,介于0和1之间的是弹性网回归
  • lambda:惩罚度的超参数

二者的区别在于cv.glmnet已经的带有交叉验证对lambda调参,自动选择最优的lambda.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 使用自带的任务
boston = mlr3::tsk("boston_housing")

glimpse(boston$data())

# glmnet不能直接处理因子型变量
# town水平过于分散,采用效应编码
# chas是两因子水平,处理成虚拟变量

prep = mlr3pipelines::po("encode",
method = "treatment",
affect_columns = selector_name("chas")) %>>%
mlr3pipelines::po("encodeimpact",
affect_columns = selector_name("town"))

boston = prep$train(boston)[[1]] # 这一步的训练相当于对数据进行处理

# 分层抽样划分训练集和测试集
set.seed(123)
split = mlr3::partition(boston, ratio = 0.8)

# 选择学习器
learner = mlr3::lrn("regr.glmnet", alpha = 0)
learner

# 查看学习器的所有超参数及其默认值
learner$param_set

# 对lambda进行自动调参

# 设置搜索空间
search.space = mlr3verse::ps(s = p_dbl(lower = 0.001, upper = 2))

# 自动调参器设置
at = mlr3tuning::auto_tuner(
method = "random_search",
learner = learner,
resampling = rsmp("cv", folds = 3),
measure = msr("regr.rmse"),
search_space = search.space,
term_evals = 10 # 计算十次后终止计算
)

# 启动自动调参过程
set.seed(123)
at$train(boston, row_ids = split$train)

# 查看最优参数
at$tuning_result

# 查看超参数变化对模型性能的影响

at$archive %>%
as.data.table() %>%
ggplot(aes(s, regr.rmse)) +
geom_line(color = "steelblue", size = 1) +
geom_point(color = "red", size = 4) +
scale_x_continuous(breaks = seq(0,2,0.2)) +
pac4xiang::mytheme()

# 最优参数训练模型
learner$param_set$values = at$tuning_result$learner_param_vals[[1]]

# 或者用最优参数重新构建学习器
learner = mlr3::lrn("regr.glmnet", alpha = 0, s = at$tuning_result$s)

# 在训练集上训练模型
learner$train(boston, row_ids = split$train)

# 绘图
mlr3viz::autoplot(learner) + pac4xiang::mytheme()

# 提取岭回归系数

model = learner$model
coef(model, s = at$tuning_result$s)

# 模型绘图
# 可以看出随着lambda增大,越来越多的指标系数逼近于0
plot(model, xvar = "lambda", label = TRUE)

# 模型预测
predict = learner$predict(boston, row_ids = split$test)

predict

# 模型评估
predict$score(msr("regr.rmse")) # 均方根误差
predict$score(msr("regr.rsq")) # R2

# 预测新数据
new.data = boston$data()[1:5, -1]
learner$predict_newdata(new.data)

其余两种正则化回归的调参过程类似。

弹性网回归时,超参数调参需要加一个参数:

1
2
search.space = mlr3verse::ps(s = p_dbl(lower = 0.001, upper = 2),
alpha = p_dbl(lower = 0, upper = 1))

Logistic回归

加载需要的包

1
2
3
4
5
library(tidyverse)
library(mlr3verse)
library(mlr3pipelines)
library(ggthemes)
library(ggplot2)

广义线性模型

线性回归要求因变量服从正态分布,但是实际情况下因变量可能是类别型、计数型等,因此需要将线性回归推广到广义线性模型。

广义线性模型相当于一个复合函数,先做线性回归,再接一个变换,变换后得到的就是非正态的因变量数据。

image-20230211195950010

这个过程应该是反过来理解,完成这个变换的函数叫做链接函数。

image-20230211200548869

mlr3案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# 创建损失矩阵
costs = matrix(c(-0.35,0,1,0), nrow = 2)
dimnames(costs) = list(response = c("good", "bad"),
truth = c("good", "bad"))
costs

# 选择自带任务
task = mlr3::tsk("german_credit")

# 选择学习器
learner = mlr3::lrn("classif.log_reg")

# 查看学习器参数
# 该处没有参数需要超参数调参
learner$param_set$ids()

# 划分数据
set.seed(123)
split = mlr3::partition(task, ratio = 0.8)

# 训练模型
learner$train(task = task, row_ids = split$train)

# 提取回归系数
coef(learner$model)

# 预测
prediction = learner$predict(task = task, row_ids = split$test)
prediction

# 提取混淆矩阵
prediction$confusion

# 查看准确率
prediction$score(msr("classif.acc"))

# 召回率
prediction$score(msr("classif.recall"))

# 计算银行的代价
# 负值表示银行会赚钱
sum(prediction$confusion * costs)/length(split$test) -> ave.cost
ave.cost
# 假设每人贷款20000,贷给10000个人,银行的收益为:
ave.cost * 20000 * 10000 * -1 # -1将收益表示为银行赚的钱

# 上述方式是使用0.5作为阈值,但是不一定是最佳值
# 使用“代价-敏感”方式对模型进行评估
cost.msr = msr("classif.costs", costs = costs)
prediction$score(cost.msr, task = task)

# 将学习器的预测方式变为概率
learner$predict_type = "prob"
learner$train(task, row_ids = split$train)
prediction = learner$predict(task = task, row_ids = split$test)

prediction

# 绘图
mlr3viz::autoplot(prediction, type = "roc")

mlr3viz::autoplot(prediction, type = "prc")

# 绘制二分类校准曲线
prediction %>%
as.data.table() %>%
dplyr::mutate(bins = cut(prob.good, breaks = seq(0,1,0.1))) %>%
dplyr::group_by(bins) %>%
dplyr::summarise(p.pred = mean(prob.good),
p.true = mean(truth == "good")) %>%
ggplot(aes(p.pred, p.true)) +
geom_point(size = 5) +
geom_point(aes(x = 1, y = 1), color = "white") +
geom_point(aes(x = 0, y = 0), color = "white") +
geom_segment(aes(x = 0, xend = 1, y = 0, yend = 1), linetype = "dashed", color = "red") +
geom_line(size = 2) +
pac4xiang::mytheme()

# 手动调节阈值参数
# 自定义函数
with.threshod = function(th){
prediction$set_threshold(th)
prediction$score(measures = cost.msr, task = task)
}

# 测试
with.threshod(0.5)
with.threshod(0.75)

# 将不同阈值的代价函数传递给optimize()函数,在[0.5,1]之间找到最小值
best = optimize(with.threshod, c(0.5,1))
best

KNN

加载需要的包

1
2
3
4
5
library(tidyverse)
library(mlr3verse)
library(mlr3pipelines)
library(ggthemes)
library(ggplot2)

如何理解KNN

简单理解为近朱者赤近墨者黑

KNN和聚类分析的区别在于KNN是有因变量的,聚类分析没有因变量,属于无监督学习。

KNN最重要的参数是K. 距离方法也可以进行超参数调参选择最优的距离计算方法。

常用的距离算法有:

  • 欧氏距离:两点之间直线距离的推广,相当于直角三角形的斜边长度。
  • 曼哈顿距离:相当于两个直角边的和。
  • 余弦相似度:两个向量的余弦夹角。
  • 杰卡德相似系数:两个集合交集与并集的元素个数之比。

后面两种方法常用于文本挖掘和推荐算法。

philentropy这个包可以实现46种距离的计算。

如何确定最佳K值

  • 交叉验证法:不同的K值进行m折交叉验证。
  • 近邻样本加权:如果已知样本距离未知样本距离较远,则需要进行加权处理。

mlr3实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# 读取数据
dat = readxl::read_excel("../data/Knowledge.xlsx") %>%
dplyr::mutate(UNS = as.factor(UNS))

table(dat$UNS)

# 创建任务
task = mlr3::as_task_classif(dat, target = "UNS")
task

# 绘图
mlr3viz::autoplot(task, type = "pairs")

# 划分数据
set.seed(123)
split = mlr3::partition(task, ratio = 0.7)

# 选择学习器
learner = mlr3::lrn("classif.kknn",
predict_type = "prob",
kernel = "rectangular",
scale = FALSE)

learner
# 查看学习器超参数
learner$param_set

# 正对K和距离进行调参
search.space = mlr3verse::ps(
k = p_int(lower = 2, upper = 15),
distance = p_int(lower = 1, upper = 2) # 1是欧式距离,2是曼哈顿距离
)

at = mlr3verse::auto_tuner(
method = "grid_search", # 随机搜索
learner = learner,
resampling = rsmp("cv", folds = 10),
measure = msr("classif.acc"),
search_space = search.space,
term_evals = 10
)

# 开始训练
at$train(task, row_ids = split$train)

# 查看最优参数
at$tuning_result

# 自动绘图
mlr3viz::autoplot(at$tuning_instance, type = "performance") +
pac4xiang::mytheme()

mlr3viz::autoplot(at$tuning_instance, type = "points") +
pac4xiang::mytheme()

# 手动展示调参结果
at$archive %>%
as.data.table() %>%
dplyr::select(k, distance, classif.acc) %>%
dplyr::arrange(-classif.acc) %>%
dplyr::slice(1:5)

# 对k进行不均匀调参,加入对数-指数变换,同时再对kernel进行调参
search.space = mlr3verse::ps(
# 必须保证k是整数
k = p_dbl(lower = log(3), upper = log(30), trafo = function(x)round(exp(x))),
distance = p_int(lower = 1, upper = 2), # 1是欧式距离,2是曼哈顿距离
kernel = p_fct(c("rectangular", "triangular", "epanechnikov",
"biweight", "triweight", "cos", "inv", "gaussian",
"rank", "optimal")) # 全选所有的kernel
)

at = mlr3verse::auto_tuner(
method = "grid_search", # 随机搜索
learner = learner,
resampling = rsmp("cv", folds = 10),
measure = msr("classif.acc"),
search_space = search.space,
term_evals = 20
)

# 开始训练
at$train(task, row_ids = split$train)

# 查看最优参数
at$tuning_result

# 手动展示调参结果
# 注意该处的K值
at$archive %>%
as.data.table() %>%
dplyr::select(k = x_domain_k, distance, kernel, classif.acc) %>%
dplyr::arrange(-classif.acc) %>%
dplyr::slice(1:10)

# 选择最优参数更新学习器参数然后进行训练
learner$param_set$values = at$tuning_result$learner_param_vals[[1]]
learner$train(task, row_ids = split$train)

# 进行预测
learner$predict(task, row_ids = split$test) -> predi

# 提取混淆矩阵
predi$confusion

# 查看准确率
predi$score(msr("classif.acc"))

回归的过程类似。

随机森林

加载需要的包

1
2
3
4
5
library(tidyverse)
library(mlr3verse)
library(mlr3pipelines)
library(ggthemes)
library(ggplot2)

关于集成学习

集成学习就是通过构建多个基学习器,按一定的策略结合成强学习器完成学习任务。

装袋法

装袋法采用的是并行机制,基学习器之间没有先后顺序,可以同时进行。装袋法采用的是有放回的抽样方法。

装袋法的代表算法是随机森林。

提升法

提升法采用的是串行机制,基学习器之间存在依赖关系,按顺序进行训练。该算法主要关注降低偏差,每次迭代都关注训练过程中错误的样本,将弱学习器提升为强学习器。

代表算法有AdaBoost、GBDT、XGboost、LightGBM和catBoost等。

堆叠法

堆叠法采用的是分阶段机制,将若干基模型的输出作为输入,再接一层主学习器,得到最终的预测。

随机森林原理

决策树是很弱的分类器,效果一般,但是将多个若学习器通过集成学习技术组合到一起,就可以实现强分类器。

优点

  • 不容易过拟合,无需建制
  • 可以并行计算,单个决策树可以同时训练
  • 可以做分类或者是回归,无需调参,就可以获得很高的分类精度
  • 对缺失值和异常值不敏感
  • 可以处理很多变量,无需对变量进行约减

缺点

  • 树越多越稳定越准确,但是也越慢
  • 更擅长分类,回归差一些,回归预测的范围只能在训练数据的范围内
  • 结果不容易解释,属于“黑箱”模型

mlr3案例

mlr3实现随机森林调用的是ranger包(要求数据不能有NA)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 使用自带的数据集创建任务
task = mlr3::tsk("pima")

# 查看数据缺失情况
task$missings()

# 直方图插补法进行缺失值插补
po = mlr3pipelines::po("imputehist")

task.new = po$train(list(task))[[1]]

task.new$missings()

# 划分数据
set.seed(123)
split = mlr3::partition(task = task.new, ratio = 0.8)

# 选择学习器
learner = mlr3::lrn("classif.ranger",
importance = "impurity",
predict_type = "prob")

# 查看学习器超参数
learner$param_set

# 超参数调参
# 对树的数量和最小节点数进行调参
search.space = mlr3verse::ps(
num.trees = p_int(lower = 1, upper = 30, trafo = function(x) 20*x), # 步长为20
min.node.size = p_int(lower = 3, upper = 30)
)

at = mlr3tuning::auto_tuner(
method = "random_search",
learner = learner,
resampling = rsmp("cv", folds = 10),
measure = msr("classif.acc"),
search_space = search.space,
term_evals = 10,
store_models = TRUE
)

# 选练模型
at$train(task.new, row_ids = split$train)

# 提取最佳参数
at$tuning_result

# 最佳参数放回到学习器
learner$param_set$values = at$tuning_result$learner_param_vals[[1]]

# 更新后的学习器进行训练
learner$train(task.new, row_ids = split$train)

# 进行预测
learner$predict(task.new, row_ids = split$test) -> predict.res

# 提取混淆矩阵
predict.res$confusion

# 查看准确率
predict.res$score(msr("classif.acc"))

# 绘制ROC曲线
mlr3viz::autoplot(predict.res, type = "roc")

R语言机器学习框架mlr3学习笔记
https://lixiang117423.github.io/article/learningmlr3/
作者
小蓝哥
发布于
2023年2月10日
许可协议