在这篇短文中,我们想要探讨机器学习分析中可能导致过度乐观的性能估计的两个陷阱。
在建立交叉验证工作流时,主要目标通常是估计经过训练的模型在外部数据上的表现如何,这在考虑生物标志物发现时特别重要。然而,涉及特征选择或时间过程数据的更复杂的工作流可能难以正确设置。在不正确的工作流程中,从测试到训练数据的信息泄漏可能导致对外部数据集的过度拟合和较差的泛化。
在这里,我们专注于监督特征选择和依赖数据的朴素分割。
首先,我们加载执行分析所需的包。
库(tidyverse)库(“SIAMCAT”)
监督特征选择意味着在交叉验证分割之前考虑标签信息。在这个过程中,如果特征与标签相关联(例如在差异丰度测试之后),则选择特征,使用完整的数据集计算特征关联,并且不留下任何数据用于无偏模型评估。
执行特征选择的正确方法是将选择步骤嵌套到交叉验证过程中。这意味着对每个训练折叠分别进行特征关联的计算。
例如,我们将使用两个结直肠癌(CRC)数据集,这些数据集可通过curatedMetagenomicData
包中。
由于模型训练过程需要很长时间,因此在构建包时不会评估这个小插图,但是如果您自己执行代码块,应该会得到类似的结果。
库(“curatedMetagenomicData”)
首先,我们将从托马斯等人作为训练数据集。
- 'ThomasAM_2018a.metaphlan_bugs_list. x <- 'ThomasAM_2018a.metaphlan_bugs_list. x <- '凳子的壮举。t <- curatedMetagenomicData(x=x, dryrun=FALSE)壮举。T <-壮举。t[[x]]@assayData$exprs #清除元平面配置文件,只包含物种级丰度壮举。t < - feat.t [grep (x = rownames (feat.t)模式=“s__”),]壮举。T <-壮举。t[grep(x=rownames(feat.t),pattern='t__', invert = TRUE),] stopifnot(all(colSums(feat.t) != 0)) feat.t <- t(t(feat.t)/100)
作为外部数据集,我们将使用来自泽勒等人。.
x <- 'ZellerG_2014.metaphlan_bugs_list. x。凳子的壮举。z <- curatedMetagenomicData(x=x, dryrun=FALSE)壮举。Z <-壮举。z[[x]]@assayData$exprs #清除元平面配置文件,只包含物种级丰度专长。z < - feat.z [grep (x = rownames (feat.z)模式=“s__”),]壮举。Z <-壮举。z[grep(x=rownames(feat.z),pattern='t__', invert = TRUE),] stopifnot(all(colSums(feat.z) != 0)) feat.z <- t(t(feat.z)/100)
类中提取相应的元数据combined_metadata
对象的一部分curatedMetagenomicData
包中。
元。t <- combined_metadata %>% filter(dataset_name == 'ThomasAM_2018a') %>% filter(study_condition %in% c('control', 'CRC')) rownames(meta.t) <- meta.t。sampleID元新台币。z <- combined_metadata %>% filter(dataset_name == 'ZellerG_2014') %>% filter(study_condition %in% c('control', 'CRC')) rownames(meta.z) <- meta.z$sampleID .z
用于概要文件的MetaPhlAn2分析器只输出数据集中存在的物种。因此,我们可以有这样的情况,在矩阵中存在物种ThomasAM_2018
哪些不在矩阵中ZellerG_2014
反之亦然。以便把它们作为训练和外部测试的集合SIAMCAT
,我们必须首先确保两个数据集的特征集完全重叠(另请参阅用SIAMCAT进行拒接测试装饰图案)。
物种。union <- union(rownames(feat.t), rownames(feat.z)) #添加Zeller_2014-only物种到Thomas_2018矩阵add.species <- setdiff(species. z)Union, rownames(feat.t))。T <- rbind(专长。T,矩阵(0,nrow=length(add.species), ncol=ncol(feature . T), dimnames = list(add. species)。add.species <- setdiff(species. t)))) # add Thomas_2018-only species到Zeller_2014矩阵Union, rownames(feat.z))。Z <- rbind(专长。Z,矩阵(0,nrow=length(add.species), ncol=ncol(feature . Z), dimnames = list(add. species)。物种,colnames (feat.z))))
现在,我们准备开始模型训练过程。为此,我们选择了三种不同的特征选择截断,并准备了一个tibble来保存结果:
fs。截止<- c(20,100,250) auroc。all <- tibble(cutoff=character(0), type=character(0), study.test=character(0), AUC=double(0))
首先,我们将使用所有可用的特征,在没有任何特征选择的情况下训练一个模型。我们将其添加到结果矩阵两次(都与正确的
而且不正确的
),以便稍后绘图。
Sc.obj.t <- siamcat(壮举=壮举。t,元=元。c.obj.t <- filter.features(sc.obj. t, label='study_condition', case='CRC')t,过滤器。方法= '患病率',截止值= 0.01)sc.obj.t <- normalize.features(sc.obj. t)t,规范。方法= 'log。性病,norm.param =(日志列表。N0 =1e-05, sd.min.q=0)) sc.obj.t <- create.data.split(sc.obj. q=0)T, num.folds = 10, num.resample = 10) sc.obj. T <- train.model(sc.obj. model)T, method='lasso') sc.obj.t <- make. forecasts (sc.obj.t) sc.obj.t <- evaluate. forecasts (sc.obj.t) auroc。所有<- auroc。所有%>% add_row(cutoff='full', type='correct', study. txt)test='Thomas_2018', AUC=as.numeric(sc.obj.t@eval_data$auroc)) %>% add_row(cutoff='full', type='incorrect', study. txt) %>%测试= ' Thomas_2018 ', AUC = as.numeric (sc.obj.t@eval_data auroc美元))
然后我们还将模型应用于外部数据集,并记录到另一个数据集的泛化:
Sc.obj.z <- siamcat(壮举=壮举。z,元=元。c.obj.z <- make.predictions(c.obj.z, label='study_condition', case='CRC')T, sc.obj.z) sc.obj.z <- evaluate. forecasts (sc.obj.z) auroc。所有<- auroc。所有%>% add_row(cutoff='full', type='correct', study. txt)test='Zeller_2014', AUC=as.numeric(sc.obj.z@eval_data$auroc)) %>% add_row(cutoff='满',type='不正确',研究。测试= ' Zeller_2014 ', AUC = as.numeric (sc.obj.z@eval_data auroc美元))
对于不正确的特征选择过程,我们可以使用完整的数据集测试特征的差异丰度,然后选择顶部相关的特征。
sc.obj.t <- check.associations(sc.obj. t。t,检测。Lim = 1e-05, fn。Plot = 'assoc_plot.pdf') mat.assoc <- associations(sc.obj.t) mat.assoc$species <- rownames(mat.assoc) # sort by p-value mat.assoc <- mat.assoc %>% as_tibble() %>% arrange(p.val)
基于P值check.association
函数,我们现在选择X
用于训练模型的特征数量。
对于(x in fs.cutoff){#选择x个特征基于p值排名feature .train.red <- feature .t[mat. off]Assoc %>% slice(seq_len(x)) %>% pull(species),] sc.obj.t.fs <- siamcat(feat=feat.train。红色,元=元。<- normalize.features(sc.obj.t, label='study_condition', case='CRC') #规范化不过滤sc.obj.t.fsfs,规范。方法= 'log。性病,norm.param =列表(log.n0 = 1 e-05 sd.min.q = 0),功能。Type = 'original') #采用与之前相同的交叉验证分割data_split(sc.obj.t.fs) <- data_split(sc.obj.t.fs) # train sc.obj.t.fs <- train.model(sc.obj.t.fs)Fs, method = 'lasso') #做出预测sc.obj.t.fs <- make. forecasts (sc.obj.t.fs) #评估预测并记录结果sc.obj.t.fs <- evaluate. forecasts (sc.obj.t.fs) auroc。所有<- auroc。所有%>% add_row(cutoff=as.character(x), type='不正确',研究。test='Thomas_2018', AUC=as.numeric(sc.obj.t.fs@eval_data$auroc)) #应用到外部数据集并记录结果sc.obj.z <- siamcat(feat=feat. z)。z,元=元。z, label='study_condition', case='CRC') sc.obj.z <- make.predictions(sc.obj.t. z)Fs, sc.obj.z) sc.obj.z <- evaluate. forecasts (sc.obj.z) auroc。所有<- auroc。所有%>% add_row(cutoff=as.character(x), type='不正确',研究。test='Zeller_2014', AUC=as.numeric(sc.obj.z@eval_data$auroc))}
如果特征选择嵌套在交叉验证过程中,则可以正确地执行。我们可以用SIAMCAT
通过指定perform.fs
参数中的train.model
函数。
对于(x in fs.cutoff){# train使用原始SIAMCAT对象#与正确版本的特征选择sc.obj.t.fs <- train.model(sc.obj. cutoff)T, method = 'lasso',执行。fs = TRUE,参数。Fs = list(thres。Fs = x,方法。fs = "AUC",方向='absolute')) #做出预测sc.obj.t.fs <- make. forecasts (sc.obj.t.fs) #评估预测并记录结果sc.obj.t.fs <- evaluate. forecasts (sc.obj.t.fs) auroc. txt所有<- auroc。所有%>% add_row(cutoff=as.character(x), type='正确',研究。test='Thomas_2018', AUC=as.numeric(sc.obj.t.fs@eval_data$auroc)) #应用到外部数据集并记录结果sc.obj.z <- siamcat(feat=feat. z)。z,元=元。z, label='study_condition', case='CRC') sc.obj.z <- make.predictions(sc.obj.t. z)Fs, sc.obj.z) sc.obj.z <- evaluate. forecasts (sc.obj.z) auroc。所有<- auroc。所有%>% add_row(cutoff=as.character(x), type='正确',研究。测试= ' Zeller_2014 ', AUC = as.numeric (sc.obj.z@eval_data auroc美元))}
现在,我们可以绘制交叉验证和外部验证的性能评估结果:
auroc。所有%>% # facetting用于绘图突变(split=case_when(study。test=="Thomas_2018"~ '交叉验证(Thomas 2018)', TRUE~"外部验证(Zeller 2014)")) %>% #转换为因子强制排序突变(cutoff=因子(cutoff,水平= c(fs. fs.)cutoff, 'full')) %>% ggplot(aes(x=cutoff, y=AUC, col=type)) + geom_point() + geom_line(aes(group=type)) + facet_grid(~split) + scale_y_continuous(limits =c (0.5, 1), expand =c (0,0)) + xlab('已选特征')+ ylab('AUROC') + theme_bw() + scale_color_manual (values =c('正确'='蓝色','不正确'='红色'),name='特征选择程序')+ theme(panel.grid. type)。Minor = element_blank(),图例。Position = 'bottom')
正如你所看到的,不正确的特征选择过程导致AUROC值膨胀,但对真正的外部数据集的泛化较低,特别是在选择很少的特征时。相比之下,正确的程序给出了较低的交叉验证结果,但对模型在外部数据上的表现有更好的估计。
机器学习工作流程中的另一个问题可能发生在样本不独立的时候。例如,在不同时间点从同一个人身上采集的微生物组样品通常比从其他个人身上采集的样品更相似。如果这些样本在简单的交叉验证过程中被随机分割,则可能会出现来自同一个人的样本最终会出现在训练和测试折叠中。在这种情况下,该模型将学习跨时间点对同一个体进行泛化,而期望的模型应该学习在不同个体之间区分标签。
为了避免这个问题,在交叉验证期间,相关测量需要被阻塞,以确保同一块内的样本将保持在相同的折叠中(用于训练和测试)。
作为一个例子,我们将使用几个克罗恩病(CD)的数据集,这些数据集通过EMBL集群可用。数据已经过滤和清理。
由于模型训练将再次花费相当长的时间,因此在构建包时不会评估小插图的这一部分,但是您应该能够自己执行它。
数据。Location <- 'https://www.embl.de/download/zeller/' #元数据元数据。所有<- read_tsv(paste0(数据。位置,CD_meta / meta_all.tsv)) # #行:1597列:6 # #──列规范────────────────────────────────────────────────────────# #分隔符:" \ t " # #杆(4):Sample_ID,集团Individual_ID,研究# #双(2):Library_Size,计算# # # #ℹ使用的规范()来检索此数据的完整列规范。##ℹ指定列类型或设置' show_col_types = FALSE '来关闭此消息。#特性壮举。Motus <- read.table(paste0(数据。location, 'CD_meta/feat_rel_filt.tsv'), sep='\t', stringsAsFactors = FALSE, check.names = FALSE)
当我们看样本数量和个体数量时,我们看到每个个体都有几个样本例如在HMP2
研究。
X <- meta。所有%>% group_by(Study, Group) %>% summary (n.all=n(), .groups='drop') y <- meta。所有% > %选择(研究、集团Individual_ID) % > %明显的()% > % group_by(研究,组)% > %总结(n.indi = n (), .groups =“下降”)full_join (x, y) # #加入,通过= c(“研究”,“集团”)# # #一个宠物猫:10×4 # #研究小组n.all n.indi # # <空空的> <空空的> < int > < int > # # 1 Franzosa_2019 CD 88 88 # # 2 Franzosa_2019 CTR 56 56 # # 3 HMP2 CD 583 50 # # 4 HMP2 CTR 357 26 # # 5 He_2017 CD 49 49 # # 6 He_2017 CTR 53 53 # # 7 Lewis_2015 CD 294 85 # # 8 Lewis_2015 CTR 25 25 # # 9 metaHIT CD 21 13 # # 10 metaHIT CTR 71 59
因此,我们要训练一个模型HMP2
研究。然而,每个个体的样本数量在样本之间有很大的差异,因此我们希望每个个体随机选择5个样本:
元。所有% > %过滤器(研究= = HMP2) % > % group_by (Individual_ID) % > %总结(n = n (), .groups =“下降”)% > %拉(n) % > %嘘(20)
每个元5个样本。火车<- meta。all %>% filter(Study=='HMP2') %>% group_by(Individual_ID) %>% sample_n(5, replace = TRUE) %>% distinct() %>% as.data.frame() rownames(meta.train) <-元数据
为了评估,我们只需要每个个体一个样本,因此我们可以创建一个新的矩阵,删除其他研究的重复样本:
元。Ind <- meta。all %>% group_by(personal_id) %>% filter(Timepoint==min(Timepoint)) %>% ungroup()
最后,我们已经可以创建一个tibble来保存结果的AUROC值:
auroc。all <- tibble(type=character(0), study.test=character(0), AUC=double(0))
分割样本进行交叉验证的简单方法没有考虑样本之间的依赖关系。因此,管道看起来就像这样:
Sc.obj <- siamcat(壮举=壮举。运动,元=元。train, label='Group', case='CD') sc.obj <- normalize.features(sc. obj)obj,规范。方法= 'log。性病,norm.param =列表(log.n0 = 1 e-05, sd.min.q = 1),功能。Type = 'original') sc.obj.naive <- create.data.split(sc. data.split)Obj, num.folds = 10, num.resample = 10) sc.obj.naive <- train.model(sc.obj. models)Naive, method='lasso') sc.obj.naive <- make. forecasts (sc.obj.naive) sc.obj.naive <- evaluate. forecasts (sc.obj.naive) auroc。所有<- auroc。所有%>% add_row(type='naive', study。测试= HMP2, AUC = as.numeric (eval_data (sc.obj.naive) auroc美元))
考虑重复样本的正确方法是阻止个体的交叉验证程序。这样,来自同一个人的样本总是会出现在相同的褶皱中。这可以在SIAMCAT
通过指定分不开的
参数中的create.data.split
功能:
sc.obj.block <- create.data.split(sc. obj.block)obj, num.folds = 10, num.resample = 10, = 'Individual_ID') sc.obj.block <- train.model(sc.obj. obj.block)Block, method='lasso') sc.obj.block <- make. forecasts (sc.obj.block) sc.obj.block <- evaluate. forecasts (sc.obj.block) auroc。所有<- auroc。所有%>% add_row(type='blocked', study。测试= HMP2, AUC = as.numeric (eval_data (sc.obj.block) auroc美元))
现在,我们可以将这两个模型应用于外部数据集,并记录结果的准确性:
for (i in setdiff(unique(meta.all$Study), 'HMP2')){元。测试<- meta。ind %>% filter(Study==i) %>% as.data.frame() rownames(meta.test) <-元。test$Sample_ID #应用朴素模型sc.obj.test <- siamcat(壮举=壮举。运动,元=元。test, label='Group', case='CD') c.obj.test <- make.predictions(sc.obj. test)Naive, sc.obj.test) sc.obj.test <- evaluate. forecasts (sc.obj.test) auroc。所有<- auroc。所有%>% add_row(type='naive', study。test=i, AUC=as.numeric(eval_data(sc.obj.test)$auroc)) #应用阻塞模型sc.obj.test <- siamcat(壮举=壮举。运动,元=元。test, label='Group', case='CD') c.obj.test <- make.predictions(sc.obj. test)Block, sc.obj.test) sc.obj.test <- evaluate. forecasts (sc.obj.test) auroc。所有<- auroc。所有%>% add_row(type='blocked', study。test=i, AUC=as.numeric(eval_data(sc.obj.test)$auroc)}
现在,我们可以比较两种不同方法得到的AUROC值:
auroc。所有%>% #转换为因子强制排序突变(type=因子(type, levels =c ('naive', 'blocked'))) %>% # facetting用于绘制突变(CV=case_when(study。test=='HMP2'~'CV', TRUE~'外部验证'))%>% ggplot(aes(x=研究。test, y=AUC, fill=type)) + geom_bar(stat='identity', position = position_dodge(), col='black') + theme_bw() + coord_cartesian(ylim=c(0.5, 1)) + scale_fill_manual(values=c('red', 'blue'), name= ") + facet_grid(~CV, space =' free', scales =' free') + xlab(") + ylab('AUROC') + theme(图例。位置= c(0.8, 0.8))
正如您所看到的,与阻塞的交叉验证相比,简单的交叉验证过程会导致夸大的性能估计。然而,当评估到真正外部数据集的泛化时,阻塞过程会产生更好的性能。
sessionInfo() ## R version 4.2.0 RC (2022-04-19 r82224) ##平台:x86_64-pc-linux-gnu(64位)##运行在:Ubuntu 20.04.4 LTS ## ##矩阵产品:默认## BLAS: /home/biocbuild/bbs-3.15-bioc/R/lib/libRblas。/home/biocbuild/bbs-3.15-bioc/R/lib/libRlapack。所以## ## locale: ## [1] LC_CTYPE=en_US。UTF-8 LC_NUMERIC= c# # [3] LC_TIME=en_GB LC_COLLATE= c# # [5] LC_MONETARY=en_US。utf - 8 LC_MESSAGES = en_US。UTF-8 ## [7] LC_PAPER=en_US。UTF-8 LC_NAME= c# # [9] LC_ADDRESS=C lc_phone = c# # [11] LC_MEASUREMENT=en_US。UTF-8 LC_IDENTIFICATION=C ## ##附加的基础包:## [1]stats graphics grDevices utils datasets methods base ## ##其他附加包:## [1]ggpubr_0.4.0 SIAMCAT_2.0.0 phyloseq_1.40.0 mlr3_0.13.3 ## [5] forcats_0.5.1 string_1 .4.0 dplyr_1.0.8 purrr_0.3.4 ## [9] readr_2.1.2 tidyr_1.2.0 tibble_3.1.6 ggplot2_3.3.5 ## [13] tidyverse_1.3.1 BiocStyle_2.24.0 ## ##通过命名空间加载(且未附加):[13] htmltools_0.5.2 magick_2.7.3 lmertest3.1 -3 # [19] cluster_2.1.3 tzdb_0.3.0 globals_0.14.0 # [22] Biostrings_2.64.0 modelr_0.1.8 mlr3tuning_0.13.0 ## [28] colorspace_2.0-3 rvest_1. 3.2 ## [7] splines_4.2.0 listenv_0.8.0 GenomeInfoDb_1.32.0 ## [10] gridBase_0.4-7 digest_0.6.29 foreach_1.5.2 ## [13] htmltools_0.5.2 magick_2.7.3 lmertest_1 .3 ## [19] cluster_2.1.3 tzdb_0.3.0 globals_0.14.0 ## [28] Biostrings_2.64.0 modelr_0.1.8 mlr3tuning_0.13.0 ## [28] matrixStats_0.62.0 vroom_1.5.7 prettyunits_1.1.1 ## [28] colorspace_2.0-3 rvest_1.0.2 ##[40] mlr3measures_0.4.1 gtable_0.3.0 Rhdf5lib_1.18.0 ## [46] shape_1.4.6 BiocGenerics_0.42.0 abind_1. 1.4-5 ## [49] scales_1.2.0 infotheo_1.2.0.1 DBI_1.1.2 ## [52] rstatix_0.7.0 Rcpp_1.0.8.3 progress_1.2.2 ## [55] palmerpenguins_0.1.0 bit_4.0.4 stats4_4.2.0 ## [58] glmnet_4.1-4 httr_1.4.2RColorBrewer_1.1-3 ## [61] ellipsis_0.3.2 farver_2.1.0 pkgconfig_2.0.3 ## [64] sass_0.4.1 dbplyr_2.1.1 utf8_1.2.2 ## [67] labeling_0.4.2 tidyselect_1.1.2 rlang_1.0.2 ## [73] cellranger_1.1.0 tools_4.2.0 cli_3.3.0 ## [76] generics_0.1.2 ade4_1.7-19 broom_0.8.0 ## [79] evaluate_0.15 biomformat_1.24.0 fastmap_1.1.0 ## [82] yaml_2.3.5 bit64_4.0.5 knitr_1.38 ## [88] bbotk_0.5.2 future_1.25.0 nlme_1 .1-157 ##[91] paradox_0.9.0 xml2_1.3.3 compiler_4.2.0 ## [94] rstudioapi_0.13 curl_4.3.2 ggsignif_0.6.3 ## [97] reprex_2.0.1 bslib_0.3.1 stringi_1.7.6 ## [100] highr_0.9 lattice_0.20-45 Matrix_1.4-1 ## [103] nloptr_2.0.0 vegan_2.6-2 permute_0.9-7 ## [106] multtest_2.52.0 vctrs_0.4.1 pillar_1.7.0 ## [109] lifecycle_1.0.1 rhdf5filters_1.8.0 BiocManager_1.30.17 ## [112] jquerylib_0.1.4 LiblineaR_2.10-12 data.table_1.14.2 ## [115] bitops_1.0-7 R6_2.5.1 bookdown_0.26 ## [118] gridExtra_2.3 mlr3misc_0.10.0IRanges_2.30.0 ## [121] parallelly_1.31.1 codetools_0.2-18 boot_1.3-28 ## [124] MASS_7.3-57 assertthat_0.2.1 rhdf5_2.40.0 ## [127] mlr3learners_0.5.2 withr_2.5.0 S4Vectors_0.34.0 ## [130] GenomeInfoDbData_1.2.8 mgcv_1.8-40 parallel_4.2.0 ## [133] hms_1.1.1 grid_4.2.0 minqa_1.2.4 ## [136] rmarkdown_2.14 carData_3.0-5 pROC_1.18.0 ## [139] num德里_2016.8-1.1 Biobase_2.56.0 lubridate_1.8.0