Lasso-ElasticNet调参报错及数据展示问题求助
问题背景
用R的caret+glmnet对比Ridge、Lasso、ElasticNet预测美国实际GDP增长率(Real.GDP.Growth),数据集为125行×37列(1个因变量+36个自变量),以RMSE为核心评估指标。Ridge模型运行正常,但Lasso和ElasticNet训练时触发报错:
In nominalTrainWorkflow(x = x, y = y, wts = weights, info = trainInfo, : There were missing values in resampled performance measures.
目前虽能得到结果,但存在异常,变量重要性图无法正常显示。测试发现lambda取值为10^seq(-5,-1,length=100)时无报错,但seq(-2,5,length=100)(更适配数据)会触发问题,计划使用alpha=seq(0,1,0.01)和lambda=seq(0,1,0.01)的调参网格。
报错原因
你的推测完全准确:当lambda值过大时,Lasso/ElasticNet会将所有自变量系数压缩为0,模型对所有样本的预测值变为因变量的均值(常数)。train()函数默认计算R²指标,而R²的计算公式要求预测值方差不为0,常数预测值的方差为0,导致R²计算产生NA,进而触发缺失值性能指标警告。
解决方案
1. 自定义性能评估函数,仅关注RMSE
既然核心评估指标是RMSE,可以跳过R²计算,避免NA值产生。修改trainControl指定专属汇总函数:
# 自定义仅计算RMSE的汇总函数 rmse_summary <- function(data, lev = NULL, model = NULL) { rmse_val <- RMSE(data$pred, data$obs) out <- c(RMSE = rmse_val) out } # 更新训练控制参数 custom <- trainControl(method = "repeatedcv", number = 10, repeats = 5, verboseIter = F, summaryFunction = rmse_summary)
这样train()只会计算RMSE,不会因R²的NA值报错。
2. 自动生成适配数据的lambda范围
不要手动指定lambda范围,让glmnet自动生成符合数据特性的序列。train()的glmnet方法默认会生成合理的lambda范围,也可以先预跑模型获取序列再调整:
# 预跑glmnet获取适配的lambda序列 pre_fit <- glmnet(x = as.matrix(US.train[, -1]), y = US.train$Real.GDP.Growth, alpha = 1) # 截取序列中间部分生成调参网格(避免最大值导致常数预测) lambda_grid <- seq(min(pre_fit$lambda), max(pre_fit$lambda)*0.95, length.out = 100) # 使用该网格训练Lasso lasso <- train(Real.GDP.Growth~. , US.train , method="glmnet", trControl=custom, tuneGrid = expand.grid(alpha = 1, lambda = lambda_grid))
glmnet生成的序列会从能压缩所有系数为0的最大值开始逐步减小,避免手动设置的lambda过大引发问题。
3. 过滤调参网格中会导致常数预测的lambda
如果坚持手动指定lambda范围,可以先找出不会让系数全为0的lambda上限:
# 找到能将所有系数压缩为0的最小lambda(Lasso为例) max_safe_lambda <- glmnet(x = as.matrix(US.train[, -1]), y = US.train$Real.GDP.Growth, alpha = 1)$lambda[1] # 设置略小于该值的lambda上限 lambda_grid <- seq(0.01, max_safe_lambda * 0.95, length.out = 100)
这样就能避免lambda过大导致的常数预测问题。
4. 确认数据标准化
glmnet默认会对自变量做标准化,但如果数据存在异常值或未正确标准化,可能影响lambda效果。可以手动标准化后再训练:
# 标准化自变量 US.train_scaled <- US.train US.train_scaled[, -1] <- scale(US.train_scaled[, -1]) US.test_scaled <- US.test US.test_scaled[, -1] <- scale(US.test_scaled[, -1]) # 使用标准化数据训练模型 lasso <- train(Real.GDP.Growth~. , US.train_scaled , method="glmnet", trControl=custom, tuneGrid = expand.grid(alpha = 1, lambda = seq(0.01,1,by = 0.01)))
数据展示替代方法
若dput(dataUS)超出字符限制,可通过以下方式展示数据信息:
- 展示数据结构与前几行:
str(dataUS) - 展示变量统计量:
summary(dataUS) - 展示前20行数据:
head(dataUS, 20) - 提供关键统计信息:因变量的均值、方差,自变量的类型(数值/分类)、缺失值比例等
修正后完整代码示例
#TRAIN-TEST set.seed(240884) train_rows <- sample(1:nrow(dataUS), 0.805*nrow(dataUS)) US.train <- as.data.frame(dataUS[train_rows,]) US.test <- as.data.frame(dataUS[-train_rows,]) library(glmnet) library(Metrics) library(caret) # 自定义仅计算RMSE的汇总函数 rmse_summary <- function(data, lev = NULL, model = NULL) { rmse_val <- RMSE(data$pred, data$obs) out <- c(RMSE = rmse_val) out } #Custom CV rule custom <- trainControl(method = "repeatedcv", number = 10, repeats = 5, verboseIter = F, summaryFunction = rmse_summary) # Ridge Regression set.seed(1234) ridge <- train(Real.GDP.Growth~. ,US.train, method="glmnet",trControl=custom, tuneGrid = expand.grid(alpha = 0,lambda = seq(0.001,20,by = 0.1))) ridge.predicted <- predict(ridge, s=ridge$lambda.1se, newdata=US.test) rmse(US.test[,1],ridge.predicted) # Plot Results plot(ridge) plot(ridge$finalModel, xvar = "lambda", label = T) plot(ridge$finalModel, xvar = 'dev', label=T) plot(varImp(ridge, scale=T)) # Lasso Regression - 使用glmnet自动生成的lambda范围 set.seed(1234) pre_lasso <- glmnet(x = as.matrix(US.train[, -1]), y = US.train$Real.GDP.Growth, alpha = 1) lambda_lasso <- seq(min(pre_lasso$lambda), max(pre_lasso$lambda)*0.95, length.out = 100) lasso <- train(Real.GDP.Growth~. , US.train , method="glmnet",trControl=custom, tuneGrid = expand.grid(alpha = 1,lambda = lambda_lasso)) lasso.predicted <- predict(lasso, s=lasso$lambda.1se, newdata=US.test) rmse(US.test[,1],lasso.predicted) # Plot Results plot(lasso) plot(lasso$finalModel, xvar = 'lambda', label=T) plot(varImp(lasso, scale=T)) # Elastic Net Regression - 自动生成lambda范围 set.seed(1234) pre_en <- glmnet(x = as.matrix(US.train[, -1]), y = US.train$Real.GDP.Growth, alpha = 0.5) lambda_en <- seq(min(pre_en$lambda), max(pre_en$lambda)*0.95, length.out = 50) en <- train(Real.GDP.Growth~. , US.train , method="glmnet",trControl=custom, tuneGrid = expand.grid(alpha =seq(0,1,0.05),lambda = lambda_en)) Enet.predicted <- predict(en, s=en$lambda.1se, newdata=US.test) rmse(US.test[,1],Enet.predicted) # Plot Results plot(en) plot(en$finalModel, xvar = 'lambda', label=T) plot(en$finalModel, xvar = 'dev', label=T) plot(varImp(en)) # 对比RMSE c(Ridge=rmse(US.test[,1],ridge.predicted), Lasso=rmse(US.test[,1],lasso.predicted), ElasticNet=rmse(US.test[,1],Enet.predicted)) # Compare Models model_list <- list(Ridge=ridge,Lasso=lasso,ElasticNet=en) res <- resamples(model_list) summary(res) # Best Model ridge$bestTune lasso$bestTune en$bestTune best <- en$finalModel coef(best, s = en$bestTune$lambda)
内容的提问来源于stack exchange,提问作者siiiq

