ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

R语言机器学习算法实现全流程:数据清洗、建模评估与避坑

R语言机器学习算法实现全流程:数据清洗、建模评估与避坑

简介:这份面向R语言机器学习入门与进阶学习者的资料包,围绕监督学习与非监督学习常见算法的代码实现展开,适合希望用R完成数据预处理、回归建模、分类与聚类实践的数据分析人员。压缩包共20个文件,约16KB,以8个R脚本和6个CSV数据集为主,前者承载算法与建模流程,后者提供可运行的示例数据,另含5个系统元数据文件与1个许可文件,便于直接对照练习。已有245人学习下载,说明其在R语言机器学习入门场景中具有一定参考价值。内容覆盖简单线性回归、多元线性回归、多项式回归、决策树回归、支持向量回归及数据预处理模板等模块,读者可据此了解不同算法的R实现方式、数据组织结构和建模脚本的编写思路,并在此基础上扩展随机森林、梯度提升、聚类等模型实践。

1. 先别急着调包:这套 R 语言机器学习算法实现适合谁

R语言做机器学习,真正的门槛往往不在算法公式,而在把表格数据、因子水平、缺失值和重采样顺序串成一条能复现的链路。这套“R语言+机器学习+各种ML算法的实现”资源,解决的就是从原始 CSV 到模型评估的整段落地问题:回归、分类、树模型、聚类和降维都有对应实现,适合已经会一点 R、但一调包就翻车的从业者,也适合想把课程设计跑通的学生。它不是只给公式的讲义,而是能照着改参数、换数据、看输出的工程样本。拿到手先别急着追高分,先确认数据读入、类型转换、训练测试划分这三步能不能稳定跑通,后面再谈模型选择。

2. 拆包先看数据流:目录、依赖与算法地图怎么读

这套资源最值得先看的不是某个算法脚本,而是数据流。R 语言机器学习项目常见的问题是脚本各自为战:清洗脚本改一列,建模脚本还在用旧列名;训练时因子有五个水平,预测时只剩三个。资源按数据层、脚本层、输出层组织,读的时候先找入口脚本和依赖声明,再顺着“读入—清洗—划分—建模—评估”走一遍。算法实现只是中间一段,前后两端的接口稳定性决定了这套代码能不能复用到自己的数据上。

2.1 目录与依赖:先跑通最小闭环

我一般先不看模型脚本,先看数据读入和依赖加载。依赖装不齐,后面所有报错都会伪装成算法问题。资源里的依赖通常集中在几个常见包:统一建模接口、正则化回归、树模型、随机森林和基础统计。先把这些包检查一遍,再跑一个空模型,确认 R 环境、工作目录和包路径没有冲突。

# 先检查资源脚本常见的依赖,已安装的包不重复安装 pkgs <- c("caret", "glmnet", "rpart", "randomForest", "e1071") to_install <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)] if (length(to_install) > 0) install.packages(to_install) # 载入关键包,并用一个最小数据框确认建模接口可用 library(caret) library(glmnet) library(rpart) library(randomForest) mini <- data.frame( y = c(1.2, 2.1, 1.8, 3.2, 2.9), x1 = c(0.5, 0.8, 1.1, 1.6, 2.0), x2 = factor(c("A", "A", "B", "B", "B")) ) fit_mini <- lm(y ~ ., data = mini) summary(fit_mini) sessionInfo() # 记录运行环境,排查包差异时先看这里

这段代码先做依赖检查,再做最小闭环。pkgs是资源脚本常用包列表,requireNamespace静默检查包是否可用,避免重复安装。mini数据框只用来验证公式接口、因子处理和lm能否正常返回,不涉及真实业务数据。sessionInfo()会输出当前 R 环境和已加载包的信息,后面出现“同一份代码两台机器结果不一样”时,先对比这里。注意不要一上来就装一堆不相关的包,依赖越多,版本冲突越难排查。

目录阅读也有顺序。先看数据层里有没有训练集、测试集和字段说明;再看脚本层里有没有统一的预处理函数;最后看输出层保存的是模型对象、预测结果还是评估表。R 的saveRDS和readRDS很常用,模型对象里可能带着公式、因子水平和预处理参数,丢一个就会在预测时翻车。资源如果提供了字段说明,优先读字段说明,不要靠str()猜列含义。

2.2 算法地图与选型:从线性模型到集成学习

“各种 ML 算法”不等于每个算法都要在同一份数据上跑一遍。选型要看任务类型、样本量、特征维度和解释成本。资源里常见的算法可以按下面这张表理解,表里的接口是 R 里常见的函数名,实际脚本可能封装成函数,但参数逻辑相近。

任务类型常见算法R 接口示例关键参数适用场景
回归线性回归lm()公式、因子处理基线模型、解释性强
回归正则化回归glmnet()alpha、lambda特征多、共线性明显
分类逻辑回归glm(family = binomial)阈值、type = "response"二分类基线、概率输出
分类决策树rpart()cp、maxdepth规则清晰、可解释
分类随机森林randomForest()ntree、mtry表格数据、非线性关系
分类梯度提升对应提升包学习率、树数、深度追求精度、可调参
聚类K 均值kmeans()centers、nstart无标签分群
降维主成分分析prcomp()center、scale.高维可视化、去相关

选型时先问三个问题:目标是预测还是解释;样本量够不够支撑复杂模型;预测结果要不要给人看。线性回归和逻辑回归不是“低级算法”,它们是最好的基线,跑通后再上树模型和集成模型,才能判断复杂模型带来的提升是不是真实。随机森林对因子水平和缺失值相对宽容,但不代表可以跳过清洗;mtry控制每次分裂随机抽取的变量数,ntree控制树的数量,树太少结果波动,树太多训练时间上升但收益递减。梯度提升精度常常更好,调参空间也更大,新手先把学习率、树数和深度三个参数管住,不要一上来铺大网格。

资源里如果同时放了分类和回归脚本,注意目标变量类型。回归目标写成因子,lm()会报错或给出无意义结果;分类目标写成数值,glm()的家庭函数要跟着改。R 的公式接口很方便,但公式里的.会把所有列都带进去,包括 ID、时间和标签泄漏列。读脚本时看到y ~ .,先检查数据框里有没有不该进模型的列。

2.3 统一评估协议:为什么先定指标再调参

算法实现能不能信,取决于评估协议是否统一。同一份数据,A 脚本用准确率,B 脚本用 AUC,C 脚本只看混淆矩阵,最后没法比较。资源如果提供统一评估函数,先读它的正类定义、阈值和重采样方式。回归任务常用 RMSE、MAE 和 R²;分类任务常用准确率、召回率、F1 和 AUC;聚类任务看轮廓系数和业务可解释性。指标不是越多越好,先定一个主指标,再配一个辅助指标防止过拟合。

# 统一重采样方案:5 折交叉验证,保存折外预测便于比较 set.seed(2024) ctrl <- trainControl( method = "cv", number = 5, classProbs = TRUE, summaryFunction = defaultSummary, savePredictions = "final" ) # 回归任务用 5 折,分类任务把 classProbs 打开 set.seed(2024) ctrl_reg <- trainControl(method = "cv", number = 5, savePredictions = "final")

method = "cv"指定交叉验证,number = 5是折数,classProbs = TRUE让分类模型输出类别概率,后面算 AUC 或调阈值会用到。savePredictions = "final"保存最终模型的折外预测,方便做模型对比。set.seed()放在重采样之前,保证每次划分一致。注意折数不是越大越好,样本量小的时候 5 折比 10 折稳定,样本量很大时 5 折也够用。评估协议定好后,所有算法共用同一组折,不要每个模型重新随机划分,否则比较结果没有意义。

3. 从原始表到建模矩阵:R 里的清洗、特征与重采样

R 语言机器学习算法实现里,最容易被低估的是数据准备。原始表里常见空字符串、前后空格、数值列被读成字符、分类列水平不一致,这些问题在lm()里可能只是报错,在树模型里可能悄悄变成新水平。清洗阶段的目标不是把数据变漂亮,而是让训练集和测试集共享同一套列类型、因子水平和缺失值处理参数。下面按读入、清洗变换、划分重采样三步走,每一步都保留可复用的参数。

3.1 数据读入与类型校正

读入时先把空值和缺失值统一。read.csv()默认会把空字符串读成NA以外的形式,字符列里的空格也会保留。建模前用trimws()清理分类列,用as.factor()显式转换,不要依赖自动推断。数值列如果被读成字符,先查有没有千分位、货币符号或中文单位,再决定是清洗还是放弃该列。

# 读入时统一缺失值标记,避免空字符串和 "NA" 混在一起 dat <- read.csv( "data/train.csv", stringsAsFactors = FALSE, na.strings = c("", "NA", "NULL") ) str(dat) # 显式指定数值列和分类列,实际使用时按字段说明替换列名 num_cols <- c("age", "income", "score") cat_cols <- c("city", "segment") dat[cat_cols] <- lapply(dat[cat_cols], function(x) as.factor(trimws(x))) dat$target <- as.numeric(dat$target) # 检查因子水平,训练前先记录一份 schema lapply(dat[cat_cols], levels)

stringsAsFactors = FALSE防止字符列被自动转成因子,后面可以按需控制水平顺序。na.strings把空字符串、"NA"和"NULL"统一成缺失值,减少后续判断。trimws()清理分类列前后的空格,避免"A"和"A "被当成两个水平。as.numeric()强制目标变量为数值,如果目标本来是分类,应该改成as.factor()。最后用lapply(dat[cat_cols], levels)记录因子水平,这个信息在预测时要复用。

类型校正后做一次列检查:ID 列、时间列、标签衍生列是否应该进模型。R 的公式y ~ .会把所有列都带进去,ID 列进模型通常没有预测价值,还会让树模型记住样本。时间列如果直接当数值,可能引入未来信息。资源脚本里如果有select()或列筛选步骤,先看它排除了哪些列,再决定是否照搬。

3.2 缺失值、异常值与特征变换

缺失值处理要先看比例和机制。缺失比例很高的列,先标记再决定是否删除;缺失比例低的数值列可以用中位数填充,分类列用众数填充。填充参数必须从训练集计算,测试集复用,否则就是数据泄漏。异常值不要一上来就删,先看业务含义和分布,用缩尾或对数变换处理长尾特征。

# 统计缺失比例,超过阈值的列先标记 na_rate <- sort(colMeans(is.na(dat)), decreasing = TRUE) na_rate[na_rate > 0] # 数值列中位数填充,因子列众数填充 median_fill <- function(x) { med <- median(x, na.rm = TRUE) x[is.na(x)] <- med x } mode_fill <- function(x) { tab <- table(x) x[is.na(x)] <- names(tab)[which.max(tab)] x } dat[num_cols] <- lapply(dat[num_cols], median_fill) dat[cat_cols] <- lapply(dat[cat_cols], mode_fill)

colMeans(is.na(dat))给出每列缺失比例,先看哪些列值得保留。median_fill()用中位数填充数值缺失,中位数比均值抗异常值。mode_fill()用众数填充分类缺失,which.max(tab)取出现次数最多的水平。实际项目里要把med和众数保存下来,预测时用同一组参数,不能在新数据上重新计算。

特征变换要看模型需求。线性模型和正则化模型对量纲敏感,做标准化;树模型对量纲不敏感,但缺失值和因子水平仍然要处理。标准化参数同样只在训练集上计算。

# 只在训练集上计算均值和标准差,测试集复用 train_mean <- colMeans(dat[num_cols], na.rm = TRUE) train_sd <- apply(dat[num_cols], 2, sd, na.rm = TRUE) dat_scaled <- as.data.frame( scale(dat[num_cols], center = train_mean, scale = train_sd) )

center和scale分别指定中心化和缩放参数。scale()默认会对列做标准化,这里显式传入训练集参数,避免测试集信息进入训练。注意scale()返回矩阵,用as.data.frame()转回数据框,方便后续合并。标准差为 0 的列要提前剔除,否则会产生NaN。

3.3 训练测试划分与交叉验证

划分要固定随机种子,分类任务尽量分层。训练集和测试集的比例没有绝对标准,样本量小的时候 7:3 或 8:2 都常见,样本量很大时测试集可以小一些。交叉验证的折要在训练集内部生成,测试集只在最终评估时用一次,不要反复看测试集调参。

set.seed(42) # 简单随机划分,70% 训练,30% 测试 idx <- sample(seq_len(nrow(dat)), size = floor(0.7 * nrow(dat))) train <- dat[idx, ] test <- dat[-idx, ] # 分类任务用分层划分,保证训练集和测试集类别比例接近 set.seed(42) idx2 <- createDataPartition(dat$target_class, p = 0.7, list = FALSE) train2 <- dat[idx2, ] test2 <- dat[-idx2, ] # 在训练集内部生成 5 折索引,后续所有模型共用 set.seed(42) folds <- createFolds(train$target_class, k = 5, list = TRUE, returnTrain = FALSE) for (i in seq_along(folds)) { valid_idx <- folds[[i]] fold_train <- train[-valid_idx, ] fold_valid <- train[valid_idx, ] }

sample()是基础随机抽样,size控制训练集大小。createDataPartition()的p = 0.7是训练比例,list = FALSE返回索引向量,分类任务会自动分层。createFolds()生成折外索引,returnTrain = FALSE表示返回验证集索引。折索引生成一次后保存下来,后面所有模型共用,不要在每个模型里重新set.seed()再划分,否则模型比较会失真。

4. 算法逐个落地:回归、分类、树模型与无监督实现

算法脚本要按任务拆开看。回归看残差和 RMSE,分类看混淆矩阵和概率校准,树模型看变量重要性和过拟合,无监督看稳定性和业务解释。R 的公式接口统一,但不同函数对数据的要求不一样:lm()对因子会自动生成哑变量,glmnet()需要自己构造矩阵,randomForest()对因子水平数量有限制。下面按算法类型给出可复现的最小代码,参数按常见场景设置,实际使用时按数据规模调整。

4.1 线性回归与正则化

线性回归是最好的基线。先跑lm(),看系数方向、显著性和残差分布,再决定要不要上正则化。特征多、共线性强的时候,普通最小二乘估计不稳定,正则化回归通过惩罚项收缩系数。glmnet()的alpha控制惩罚类型:alpha = 1是 Lasso,alpha = 0是 Ridge,中间值是弹性网。

# 线性回归基线 fit_lm <- lm(y ~ ., data = train) summary(fit_lm) pred_lm <- predict(fit_lm, newdata = test) rmse_lm <- sqrt(mean((test$y - pred_lm)^2, na.rm = TRUE)) mae_lm <- mean(abs(test$y - pred_lm), na.rm = TRUE) # 正则化回归:先构造模型矩阵,保证训练和测试列对齐 library(glmnet) x_train <- model.matrix(y ~ . - 1, data = train) y_train <- train$y x_test <- model.matrix(y ~ . - 1, data = test) set.seed(42) cv_fit <- cv.glmnet(x_train, y_train, alpha = 1, nfolds = 5) pred_lasso <- predict(cv_fit, newx = x_test, s = "lambda.min")

lm()的公式y ~ .会把所有列带入,实际使用时先排除 ID 和泄漏列。predict()对新数据要求列名和类型一致。model.matrix()把因子展开成哑变量,- 1去掉截距列,避免和glmnet()默认截距重复。alpha = 1是 Lasso,nfolds = 5是内部交叉验证折数,s = "lambda.min"选择交叉验证误差最小的 lambda。正则化前要标准化,否则量纲大的特征惩罚不均衡。

4.2 逻辑回归与分类阈值

二分类先跑逻辑回归,输出概率再调阈值。默认阈值 0.5 不一定适合业务,正类召回重要时可以把阈值调低,误报成本高时调高。评估不要只看准确率,类别不平衡时准确率会被多数类主导。

# 逻辑回归二分类 fit_logit <- glm(target_class ~ ., data = train, family = binomial) prob <- predict(fit_logit, newdata = test, type = "response") # 阈值可调,先看混淆矩阵再决定 threshold <- 0.5 pred_class <- ifelse(prob >= threshold, "yes", "no") conf <- table(Predicted = pred_class, Actual = test$target_class) conf acc <- sum(diag(conf)) / sum(conf) recall <- conf["yes", "yes"] / sum(conf[, "yes"]) precision <- conf["yes", "yes"] / sum(conf["yes", ])

family = binomial指定逻辑回归,type = "response"输出概率而不是线性预测值。threshold是分类阈值,改这个值会改变混淆矩阵。diag(conf)取对角线上的正确预测数,recall和precision的分母分别对应真实正类和预测正类。注意table()的维度顺序要和预测、实际标签顺序一致,否则指标会算反。

4.3 树模型与集成学习

决策树适合看规则,随机森林适合表格数据。rpart()的cp控制复杂度修剪,maxdepth控制树深,树太深容易记住训练集。随机森林的ntree是树的数量,mtry是每次分裂随机抽取的变量数,importance = TRUE打开变量重要性。

# 决策树:cp 越大树越简单,maxdepth 限制深度 library(rpart) fit_tree <- rpart( target_class ~ ., data = train, method = "class", control = rpart.control(cp = 0.01, minsplit = 20, maxdepth = 5) ) pred_tree <- predict(fit_tree, newdata = test, type = "class") # 随机森林:只放数值列示例,实际项目先处理因子水平 library(randomForest) set.seed(42) fit_rf <- randomForest( x = train[, num_cols], y = train$target_class, ntree = 300, mtry = 3, importance = TRUE ) pred_rf <- predict(fit_rf, newdata = test[, num_cols]) importance(fit_rf)

cp = 0.01是复杂度参数,值越大树越容易剪枝。minsplit = 20是节点分裂的最小样本数,maxdepth = 5限制树深。ntree = 300是森林中树的数量,mtry = 3是每次分裂抽取的变量数,分类任务常用sqrt(特征数)附近的值。importance()输出变量重要性,但重要性高不代表因果,别过度解读。

4.4 聚类与降维

无监督任务没有标签,评估更依赖稳定性和业务解释。K 均值先标准化,再选中心数,nstart多跑几次初始中心,避免局部最优。主成分分析用于降维和可视化,prcomp()的center和scale.控制中心化和缩放。

# K 均值:先标准化,再聚类 set.seed(42) x_clu <- scale(dat[num_cols]) km <- kmeans(x_clu, centers = 3, nstart = 25, iter.max = 100) km$cluster km$size # 主成分分析:前面已经标准化,这里不再重复 pca <- prcomp(x_clu, center = FALSE, scale. = FALSE) summary(pca) pca$x[, 1:2]

centers = 3是聚类数,nstart = 25表示用 25 组初始中心,取最优结果,iter.max = 100是最大迭代次数。km$size看每类样本数,类别严重不均衡时考虑换算法或调整中心数。prcomp()的center = FALSE和scale. = FALSE是因为前面已经标准化,避免重复处理。主成分解释方差比例看summary(pca),前两个主成分用于画图时注意坐标轴含义。

4.5 统一评估与模型比较

模型比较要在同一测试集、同一指标下做。回归用 RMSE、MAE、R²,分类用准确率、召回率、F1。把结果整理成表,不要只看单个数字。

# 回归指标 rmse <- sqrt(mean((test$y - pred_lm)^2, na.rm = TRUE)) mae <- mean(abs(test$y - pred_lm), na.rm = TRUE) r2 <- 1 - sum((test$y - pred_lm)^2, na.rm = TRUE) / sum((test$y - mean(test$y))^2, na.rm = TRUE) # 分类指标 conf <- table(Predicted = pred_class, Actual = test$target_class) acc <- sum(diag(conf)) / sum(conf) recall <- conf["yes", "yes"] / sum(conf[, "yes"]) precision <- conf["yes", "yes"] / sum(conf["yes", ]) f1 <- 2 * precision * recall / (precision + recall) # 整理模型对比表 data.frame( model = c("linear", "tree", "random_forest"), metric = c(rmse_lm, NA, NA) )

rmse对大误差更敏感,mae更直观,r2看解释比例。分类指标里precision和recall的分母容易写反,先用小样本手算一遍再信代码。模型对比表只放真正跑过的模型,空值不要硬填。

5. 避坑与排查:R 机器学习实现里最容易翻车的四件事

R 语言机器学习算法实现的报错经常指向奇怪的位置:predict()说列名不对,glm()说找到 NA,随机森林结果每次不一样。下面四条是血泪经验里最常见的情况,按“现象—原因—解决”拆开。遇到问题先缩小到最小可复现样本,再回来看资源和脚本。

5.1 因子水平与预测列错位

现象:训练集能跑,测试集predict()报错“因子有新的水平”,或者模型矩阵列数对不上。原因:训练集和测试集的因子水平不一致,测试集缺少某个水平,或者多了训练集没见过的水平。R 的公式接口会自动生成哑变量,水平不一致时列名和列数都会变。解决:训练前统一因子水平,保存 schema,预测前检查列名和类型。

# 训练集水平为准,测试集补齐缺失水平 train$city <- factor(train$city, levels = c("A", "B", "C")) test$city <- factor(test$city, levels = levels(train$city)) # 检查模型矩阵列名是否一致 x_train_mm <- model.matrix(target_class ~ . - 1, data = train) x_test_mm <- model.matrix(target_class ~ . - 1, data = test) stopifnot(identical(colnames(x_train_mm), colnames(x_test_mm)))

factor(levels = ...)显式固定水平,测试集出现新水平时会变成NA,比悄悄多一列安全。model.matrix()生成哑变量后用identical()检查列名,不一致就停下。实际项目里把因子水平存成列表或 RDS,预测脚本先读 schema 再转换,不要靠read.csv()自动推断。

5.2 数据泄漏与重采样顺序

现象:交叉验证分数很高,上线后掉得厉害。原因:先对全量数据做标准化、缺失值填充或特征选择,再划分训练测试,测试集信息进入训练。另一种常见错误是先在全量数据上选特征,再做交叉验证。解决:所有预处理放进重采样内部,训练折算参数,验证折复用。

# 错误做法:先全量标准化,再划分 # dat_scaled <- scale(dat[num_cols]) # idx <- sample(...) # 正确做法:在训练折内计算参数,验证折复用 prep_fold <- function(train_fold, valid_fold, cols) { med <- apply(train_fold[cols], 2, median, na.rm = TRUE) for (j in cols) { train_fold[[j]][is.na(train_fold[[j]])] <- med[j] valid_fold[[j]][is.na(valid_fold[[j]])] <- med[j] } list(train = train_fold, valid = valid_fold, med = med) }

prep_fold()只在训练折上算中位数,再把同一组中位数应用到验证折。med要跟着模型一起保存,预测新样本时复用。数据泄漏不会报错,只会让评估虚高,所以划分顺序要写死在脚本里,别图省事。

5.3 缺失值处理与模型接口不一致

现象:随机森林能跑,逻辑回归报“NA/NaN/Inf”,或者训练时填了缺失,预测时新样本又有缺失。原因:不同算法对缺失值容忍度不同,预处理只做在训练集,预测脚本没复用填充参数。解决:统一插补流程,训练和预测共用同一套参数。

# 用插补模型处理缺失,并保存插补器 library(rpart) impute_fit <- rpart(income ~ age + score + segment, data = train, method = "anova") train$income_filled <- ifelse( is.na(train$income), predict(impute_fit, train), train$income ) test$income_filled <- ifelse( is.na(test$income), predict(impute_fit, test), test$income )

rpart()的method = "anova"用于连续变量插补,predict()对新数据给出预测值。ifelse()只替换缺失位置,非缺失保持原值。插补器要保存,预测脚本先加载再处理。注意插补特征不能包含目标变量,否则会泄漏。

5.4 随机种子、并行与结果不可复现

现象:同样参数跑两次,随机森林结果不一样;开了并行之后交叉验证分数波动。原因:随机种子没固定,并行计算的随机数流不同,或者每次重新生成折索引。解决:固定种子,保存折索引,调试阶段先串行。

set.seed(123) fold_ids <- createFolds(train$target_class, k = 5, list = FALSE) # 后续所有模型共用同一组 fold_ids,不要每次重新 createFolds set.seed(123) fit_rf_1 <- randomForest( x = train[, num_cols], y = train$target_class, ntree = 300, mtry = 3 )

set.seed(123)固定随机数生成器,createFolds()生成折索引后保存。randomForest()内部也有随机性,set.seed()要放在建模前。并行计算时每个 worker 的随机数流可能不同,调试阶段先foreach串行跑通,再开并行。结果不可复现时先对比sessionInfo()和折索引,不要一上来怀疑算法。

6. 进阶用法:把单次建模变成可复用的调参与验证流程

单次跑通只是开始,真正省时间的是把调参、验证和保存串成流程。随机森林的mtry可以用网格搜索,回归的正则化参数用交叉验证选 lambda。调参不要直接看测试集,测试集只在最后评估一次。嵌套交叉验证的外层评估泛化,内层调参,能减少调参带来的乐观偏差。

# 随机森林调参:内层交叉验证选 mtry set.seed(42) ctrl_tune <- trainControl(method = "cv", number = 5, savePredictions = "final") grid_rf <- expand.grid(mtry = c(2, 4, 6)) fit_grid <- train( target_class ~ ., data = train, method = "rf", trControl = ctrl_tune, tuneGrid = grid_rf, metric = "Accuracy", ntree = 300 ) fit_grid$bestTune # 外层评估:用固定折索引,避免调参泄漏 outer_folds <- createFolds(train$target_class, k = 5, list = TRUE) for (i in seq_along(outer_folds)) { outer_train <- train[-outer_folds[[i]], ] outer_valid <- train[outer_folds[[i]], ] inner_ctrl <- trainControl(method = "cv", number = 3) inner_fit <- train( target_class ~ ., data = outer_train, method = "rf", trControl = inner_ctrl, tuneLength = 3, ntree = 300 ) pred <- predict(inner_fit, outer_valid) # 在这里记录外层指标 }

tuneGrid里的mtry是每次分裂抽取的变量数,ntree固定在train()参数里。metric = "Accuracy"指定主指标,分类任务也可以换Kappa。嵌套交叉验证的外层折只用于评估,内层折用于选参,折索引要提前保存。调参表不要太大,先粗网格再细网格,否则训练时间会失控。

模型保存要把预处理参数、因子水平和模型对象一起存。只存模型不存 schema,预测脚本换个环境就可能列错位。

# 保存模型和预处理参数 saveRDS( list(model = fit_grid, num_cols = num_cols, cat_levels = lapply(train[cat_cols], levels)), "output/model_bundle.rds" ) # 预测时先读 bundle,再按同一套列和水平处理新数据 bundle <- readRDS("output/model_bundle.rds") new_dat <- test new_dat[cat_cols] <- lapply(cat_cols, function(x) { factor(new_dat[[x]], levels = bundle$cat_levels[[x]]) }) pred_final <- predict(bundle$model, newdata = new_dat)

saveRDS()保存的是 R 对象,readRDS()读回后结构和类型保持不变。model_bundle里放模型、数值列名和因子水平,预测前按同一套 schema 转换。predict()的newdata列名要和训练时一致,缺列或多列都会报错。每次改完预处理,重新保存 bundle,不要只更新模型文件。

从那以后我每次跑 R 机器学习脚本,都强制先固定重采样索引、保存预处理参数,再谈调参;测试集只看一次,预测脚本必须从 bundle 读 schema。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进