ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

随机森林回归实战:基于UCI数据的葡萄酒质量预测

随机森林回归实战:基于UCI数据的葡萄酒质量预测 不绕弯子直接聊这次做的葡萄酒质量预测项目。市面上的教程大多拿鸢尾花、波士顿房价练手但那类数据集干净得不像实战。我这次选的是UCI上的公开葡萄酒质量数据集用随机森林回归模型解决一个非常接地气的问题给一堆理化指标能不能把酒的质量分数预测出来。这篇博文把完整流程拆开揉碎写清楚包含数据探索、特征处理、模型调参、评估分析和完整代码不讲虚的全部可直接复现。1. 项目背景与整体设计思路1.1 为什么选葡萄酒质量预测这个问题做回归预测的数据集其实很多但葡萄酒质量预测有个天然优势特征维度适中、特征之间相关性复杂、目标变量是真实的评分而非人造标签。数据集记录了红葡萄酒的各种理化指标比如固定酸度、挥发性酸度、柠檬酸、残糖、氯化物、游离二氧化硫、总二氧化硫、密度、pH值、硫酸盐、酒精浓度以及一个品酒师给出的质量评分0到10的整数。这个评分背后是真实世界的感官评价而我们要做的是用化学指标去拟合人的味觉本质上是在做一个“理化指标到感官体验”的映射模型业务解释非常强。做这类项目最关键的一点是分清楚任务类型。有人把评分当作分类问题用随机森林分类器去做这其实是错误的路径。质量评分虽然是整数但它代表的是连续的质量高低排序类别之间是有序的、有间隔的。用回归模型可以得到更细粒度的预测结果还能输出连续值用于排序这一点在建模之前必须想清楚。所以这个项目从一开始就锁定为回归任务核心模型选择随机森林回归RandomForestRegressor。1.2 数据集构成与任务边界选用UCI Wine Quality数据集中的红葡萄酒部分共1599条样本11个输入特征1个目标变量quality。数据量规模不算大但足够训练一个稳定的随机森林模型。数据没有缺失值这也是UCI老牌数据集的特点——干净不需要做太多清洗工作。但干净不等于可以直接开跑布局探索仍然要做后面的相关性分析会给出很多有价值的信息。任务目标定义为基于11个理化特征预测葡萄酒的质量评分。评估标准采用回归问题的三件套决定系数R²、均方根误差RMSE、平均绝对误差MAE。R²回答“模型能解释多少变异”RMSE回答“平均偏差有多大”MAE辅助判断误差分布是否受极端值影响。1.3 随机森林回归的选型理由随机森林是Bagging集成算法的一种核心思想是训练多个决策树每棵树在随机抽样的子集上生长最终输出所有树的均值回归或投票结果分类。它有三大优势正好踩在这个项目的需求点上第一非线性拟合能力强。葡萄酒理化指标与质量评分之间的关系显然不是线性的比如酒精浓度在某个阈值以下影响较弱超过某个阈值后质量评分显著提升这种阈值效应用线性回归很难表达。第二对特征尺度的鲁棒性。随机森林不需要对特征做标准化因为树模型的分裂逻辑是基于特征排序而不是距离度量。这省去了一大批预处理工作。第三自带特征重要性评估。这个特性对业务理解非常有用。训练完后可以直接看到哪些理化指标对质量影响最大而这一点在神经网络或线性模型中要么难解释、要么不够直观。2. 数据探索与相关性分析2.1 数据加载与初始状态检查数据来源是UCI的winequality-red.csv文件注意这个文件的分隔符是分号而不是逗号。我第一次处理时直接用pd.read_csv()读入结果整个DataFrame只有一列原因就是没注意分隔符。这一点写出来提醒大家UCI老数据集的格式经常带点“历史遗留问题”。import pandas as pd import numpy as np df pd.read_csv(winequality-red.csv, sep;) print(df.shape) print(df.info()) print(df.describe().T)输出显示数据集是[1599 rows x 12 columns]各列都无缺失值。describe()可以快速看出字段的量纲差异很大比如固定酸度均值在8.3左右总二氧化硫均值在46而密度集中在0.99到1.00。随机森林不会因为量纲差异而失衡但如果后面做特征工程或换模型这一步的观察就有价值了。还要检查重复样本。葡萄酒样本中确实存在重复记录它们是不同批次但理化指标完全一致的酒。对于这个问题我没有做去重因为重复样本不影响树的随机抽样而且保留它们可以维持原始数据的分布特征。统计下来重复样本大概有几十条占比不大不影响模型稳定性。2.2 特征相关性矩阵与关键信号用相关系数热力图观察特征之间的关系这步看似简单却能提供大量建模线索。import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize(12, 10)) sns.heatmap(df.corr(), annotTrue, fmt.2f, cmapcoolwarm, linewidths0.5) plt.title(Feature Correlation Matrix) plt.tight_layout() plt.show()几个关键信号需要提炼。alcohol与quality的相关系数约为0.48是单特征中与目标变量相关性最高的。volatile acidity与quality的相关系数为-0.39表明挥发性酸度越高质量评分越低这符合酿酒常识——挥发酸过高会产生不愉悦的酸味。sulphates与quality呈正相关大约0.25。density与其他特征之间有多重相关性尤其是与酒精浓度强负相关酒精密度低于水酒精越高整体密度越低这提示特征之间存在共线性但树模型对共线性不敏感所以不需要像线性回归那样做VIF筛选或PCA降维。这里有个值得新手关注的点特征之间的相关性强并不影响随机森林的精度但会影响特征重要性的解读。如果两个特征相关性很高树在分裂时可能随机选择其中一个导致重要性分散。理解这个原理有助于客观解读后续的特征重要性排序不要看到某个重要特征被低估就认为模型出错了。2.3 目标变量的分布检查查看质量评分的值分布情况这一步很关键因为它直接影响模型的评估方式。sns.countplot(xquality, datadf) plt.show()质量评分集中在5到6分3分和8分是少数没有4分以下和9分以上的样本。这种分布是典型的偏斜分布意味着模型更容易学会预测中间分数而极端分数的预测误差会比较大。后面评估阶段我对评分在5分以下的样本单独做了误差统计发现预测误差确实集中在低分样本上这与品酒师本身对低分酒的评判标准差更大也有关系。目标变量的取值范围从3跳到8跨度5个等级。预测一个6分的酒实际是5分和一个预测8分的酒实际是7分在绝对误差上都是1分但业务意义不同6分与5分之间的认可度差距没有那么大8分和7分的价格可能会差一截。这个信息在业务层面上值得注意不过就项目本身而言回归模型不做分类不涉及阈值设定所以误差评估还是用MAE和RMSE来量化。3. 基线模型构建与超参数调优3.1 数据分割方案设计将数据划分为训练集和测试集比例采用常见的train_test_split默认的四分法即训练集占75%测试集占25%。设置random_state42保证结果可复现。这里有一个较容易被忽视的细节回归模型应当采用分层采样吗对于分类问题stratify参数保持类别比例很有必要但在回归问题中目标值连续没有现成的分层依据所以在分割时需要额外考虑数据分布倾斜的问题。我的做法是先用简单的随机分割跑通基线流程后续在优化阶段再引入基于评分的分组交叉验证从而验证模型在不同分数段上的稳定性。在实际业务中如果样本量够大可以按目标变量的分位数分层切分让训练集尽可能覆盖全范围的质量分数。否则模型对低分样本和极高分样本的学习可能不充分。from sklearn.model_selection import train_test_split X df.drop(quality, axis1) y df[quality] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.25, random_state42 )3.2 最简随机森林回归基线先用默认超参数构建一个基线模型目的是拿到一个“傻跑”出来的结果让我们了解模型在不做任何调整的情况下能达到什么水平。这也是一种调试习惯先确认流程没有bug再谈优化。from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error rf_base RandomForestRegressor(random_state42) rf_base.fit(X_train, y_train) y_pred_base rf_base.predict(X_test) print(R2:, r2_score(y_test, y_pred_base)) print(RMSE:, mean_squared_error(y_test, y_pred_base, squaredFalse)) print(MAE:, mean_absolute_error(y_test, y_pred_base))以默认参数100棵树、不限最大深度、叶子节点最少1个样本运行时R²大约在0.45左右RMSE约0.64。这个成绩怎么说呢对于葡萄酒评分这个任务模型已经能解释约45%的质量变异但还远远不够好。默认深度的随机森林容易过拟合训练数据泛化能力受到限制这就是需要调参的主要信号。读者可以自行跑一下R²在0.4到0.5之间波动属于正常现象毕竟随机种子不同会导致抽样不同。随机森林不是确定性的算法主要有两处随机性bagging随机抽样和特征子集随机抽样。3.3 网格搜索调参实践直接进入网格搜索对n_estimators、max_depth、min_samples_split、max_features四个超参进行寻优。这里我不推荐直接上大范围网格多个参数组合相乘计算量会爆炸。比如4个参数每个给4个候选值就是256次五折交叉验证1600次训练时间不可控。我的策略是分两轮调参第一轮粗调定位最优区间第二轮精调微调。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200, 300], max_depth: [None, 10, 20], min_samples_split: [2, 5, 10], max_features: [sqrt, 0.5, 0.7] } rf_search RandomForestRegressor(random_state42) grid_search GridSearchCV( estimatorrf_search, param_gridparam_grid, cv5, scoringneg_root_mean_squared_error, n_jobs-1, verbose1 ) grid_search.fit(X_train, y_train) print(Best params:, grid_search.best_params_) print(Best CV RMSE:, -grid_search.best_score_)这里有几个细节值得解释。scoringneg_root_mean_squared_error是因为GridSearchCV总是最大化评分所以用负RMSE让“越小越好”变成“越大越好”。n_jobs-1使用所有CPU核心网格搜索训练成本主要是多棵树的并行构建所以并行效率很高。一轮跑下来最优参数大致在n_estimators200、max_depth1020、min_samples_split510、max_featuressqrt附近。针对点数比较大的参数空间我建议将n_estimators从[100, 200, 300]继续细化到[150, 200, 250]综合精度和训练时间选200。值得提醒的是max_depthNone并非在所有数据集上都好。随机森林每棵树都是完全生长的决策树单棵树的方差很大但bagging通过平均降低了方差。当树的数量够多时max_depthNone的精度往往不错只是会占用更多内存。在数据量不大时可以直接尝试在数据规模上到几十万以上时限制深度是更好的取舍。3.4 调参后模型效果差异用调参后的模型重新评估测试集。最终R²提升到0.54左右RMSE降到0.57左右MAE大约0.43相比基线的确改善了一截。RMSE比MAE大说明存在一部分误差绝对值较大的样本这也呼应了低分酒和某些独特风土条件下样本预测容易失准的问题。E值提醒在实际项目中调参的收益通常不会特别巨大更重要的是特征理解。随机森林这模型样本特征强相关时调参所能带来的增益相对有限从0.45到0.55的R²提升已经算显著。后面还能进一步提高的办法是特征工程和处理目标偏斜这两条路我放在后续部分展开。4. 特征重要性分析与误差诊断4.1 随机森林特征重要性解读随机森林有两种特征重要性计算方式基于杂质的平均减少量MDIMean Decrease Impurity和基于排列的准确率减少量MDAMean Decrease Accuracy。sklearn的feature_importances_返回的是MDI方式。MDI会偏好取值多的连续特征因为取值多会让纯度下降看起来更明显。使用时要有这个认知必要时可以手动实现MDA做交叉验证。import numpy as np feature_importance rf_best.feature_importances_ feature_names X_train.columns indices np.argsort(feature_importance)[::-1] for i in indices: print(f{feature_names[i]:30s} {feature_importance[i]:.4f})在本数据集中排在前四的特征是alcohol、volatile acidity、sulphates、total sulfur dioxide不同随机种子下顺序略有变化但酒精和挥发性酸度基本锁死在前二。这与相关性分析的结果高度一致也与酿酒工艺的基本常识吻合酒精带来酒体和甜感挥发酸过高代表微生物污染或氧化硫酸盐有抗氧化和抑菌作用。特征重要性分析得到的不是“黑盒”而是一条可以用来检查模型是否学习到真实信号的路径。需要注意的是fixed acidity和citric acid的重要性在MDI中都偏低但这并不能断言这两个指标不影响质量。它们的特点是与其他指标如pH、volatile acidity高度相关模型在分裂时随机竞争两个特征的重要性被分散了。要判断真实影响需要结合排列重要性、SHAP值等多维度验证。4.2 残差分布与预测偏差分析残差分析是回归项目里不可跳过的一步但很多新手只看R²就结束了。诊断残差能发现很多模型层面的问题比如是否存在系统性偏差、是否对某些取值区间预测有偏。residuals y_test - y_pred_final plt.scatter(y_pred_final, residuals, alpha0.6) plt.axhline(y0, colorred, linestyle--) plt.xlabel(Predicted Quality) plt.ylabel(Residuals (Actual - Predicted)) plt.show()从残差图看预测值为6分附近时残差集中而且对称模型在这个区间表现稳定。但当预测值低于5分或高于6.5分时残差分布明显发散说明模型对极端质量的酒样本学习不足这与目标变量严重偏斜直接相关。从业务角度解释也很自然好酒和差酒在品鉴时争议大理化指标对它们的解释力相对有限。针对这种偏斜分布可以考虑的目标编码、加权采样等方案。例如在RandomForestRegressor中没有直接的sample_weight参数其实fit()函数支持sample_weight可以通过传入权重来放大低分样本的训练权重代价是会减少高分样本的准确率优点是模型整体误差分布更均衡。我在实验中对质量评分最低的30%样本设置了2倍权重MAE下降了约0.03但R²略微下降方向不同取舍不同。实际项目中要不要做加权核心依据还是业务目标更关心哪些样本的预测精度。4.3 OOB分数与其他评估视角随机森林一个独有的优势是OOBOut-of-Bag分数它是在训练过程中利用每棵树未抽样到的样本做内部验证相当于免费的交叉验证。sklearn的RandomForestRegressor在oob_scoreTrue时直接给出R²。提取OOB分数可以快速判断模型泛化能力而不用额外划分验证集。rf_final RandomForestRegressor( n_estimators200, max_depth15, min_samples_split5, max_featuressqrt, random_state42, oob_scoreTrue ) rf_final.fit(X_train, y_train) print(OOB R2:, rf_final.oob_score_)OOB分数与测试集R²比较接近说明模型没有严重的过拟合问题泛化能力稳健。这里有一个微妙但重要的细节OOB是在训练集内部估计的它不能替代测试集评估但可以充当一个良好的中期指标。当连续多轮调参OOB分数不再上升时说明继续调参的边际收益已经很低此时应该把精力转移到特征工程或样本质量上而不是继续无脑细化网格。5. 完整项目代码与工程化细节5.1 项目结构与运行环境这个项目整体结构清晰代码文件不多但按照工程规范组织起来。无论是学习还是扩展到实际业务都可以往下沉淀。wine-quality-project/ ├── data/ │ └── winequality-red.csv ├── notebooks/ │ └── EDA.ipynb ├── src/ │ ├── preprocess.py │ ├── train.py │ └── evaluate.py ├── models/ │ └── rf_model.pkl ├── requirements.txt └── README.md运行环境建议Python 3.8、scikit-learn 1.0、pandas 1.3、numpy 1.21、matplotlib 3.5、seaborn 0.11。安装依赖时需要注意scikit-learn版本差异对超参名称和默认行为的影响例如老版本中的n_estimators默认是10而新版本是100这个默认值差异会直接影响基线效果让人误以为“自己的代码效果比别人差”。5.2 完整训练评估脚本下面贴出完整的训练评估脚本这是一个“可直接抄作业”的版本包含数据加载、分割、训练、调参、评估、特征重要性和模型保存全流程。代码风格遵循机器学习项目的惯例函数化封装主入口逻辑清晰。 葡萄酒质量预测随机森林回归完整训练评估 import joblib import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error def load_data(path: str) - pd.DataFrame: df pd.read_csv(path, sep;) assert not df.isnull().any().any(), 存在缺失值请检查数据 return df def split_features_target(df: pd.DataFrame): X df.drop(quality, axis1) y df[quality] return X, y def train_best_model(X_train, y_train): param_grid { n_estimators: [150, 200, 250], max_depth: [10, 15, 20], min_samples_split: [5, 10], max_features: [sqrt, 0.7] } rf RandomForestRegressor(random_state42, oob_scoreTrue) grid_search GridSearchCV( rf, param_grid, cv5, scoringneg_root_mean_squared_error, n_jobs-1, verbose1 ) grid_search.fit(X_train, y_train) return grid_search.best_estimator_, grid_search.best_params_ def evaluate_model(model, X_test, y_test): y_pred model.predict(X_test) r2 r2_score(y_test, y_pred) rmse mean_squared_error(y_test, y_pred, squaredFalse) mae mean_absolute_error(y_test, y_pred) return {R2: r2, RMSE: rmse, MAE: mae}, y_pred if __name__ __main__: df load_data(data/winequality-red.csv) X, y split_features_target(df) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.25, random_state42 ) best_model, best_params train_best_model(X_train, y_train) print(最优参数:, best_params) metrics, y_pred evaluate_model(best_model, X_test, y_test) print(模型评估:, metrics) print(OOB 分数:, best_model.oob_score_) feature_importance best_model.feature_importances_ feature_names X.columns for name, imp in sorted(zip(feature_names, feature_importance), keylambda x: x[1], reverseTrue): print(f{name:30s} {imp:.4f}) joblib.dump(best_model, models/rf_model.pkl)脚本中存在一个较隐蔽的坑数据分割时直接用原始df传入但在完整项目中有些数据源支持的列较多直接drop(quality, axis1)可能报KeyError更稳妥的写法是定义一列特征列表feature_cols比盲目drop要严谨。5.3 常见问题与排查实录问题1R²为负数说明什么R²为负说明模型的预测效果比直接取均值还要差通常发生在数据分割极端或模型设置严重错误时。排查路径依次是检查训练集与测试集分割后是否发生数据泄漏、查看数据是否含有高频噪声、确认模型没有使用random_state导致的结果波动。用随机森林做回归出现负数R²时还要检查是否误分类数据全填了缺失值、有没有把目标变量当作特征一起灌进训练。问题2测试集RMSE远高于训练集RMSE怎么办典型过拟合。首选限制max_depth和增大min_samples_split让单棵树不要生长得过深过密。树模型的优点在于可以通过限制单棵树复杂度来平衡偏差与方差只要树之间差异够大bagging平均后仍然能有不错的精度。另一个大招是增大n_estimators树的数量增加虽然不能直接解决过拟合但能让集成平均过程更稳定一定程度上缩小训练和测试评估的差距。问题3为什么两次运行结果不一样因为随机森林既对训练数据做自助抽样又对特征子集做随机选择。random_state控制了这两处随机性但不设置时就由系统时间决定。要想让项目可复现所有模型、数据分割和网格搜索都应该设置固定的random_state。这一点在工程化和团队协作时尤其重要。问题4全特征训练和只保留top特征训练谁效果更好随机森林在数据集特征个数中等时全特征训练往往更稳。删除弱特征可能会削弱少量信号而且树的特征子集选择机制本身会降低噪声特征的干扰。我做了一组对比实验只保留特征重要性前6个特征进行训练R²大约比全特征版本低0.020.03。结论很明确如果特征是领域相关且维度不爆炸的情况下全特征优于特征子集。6. 项目扩展方向与个人实测心得6.1 从回归到分类与排序的改进空间当前项目做的是回归预测但在真实业务场景中很多需求是把酒分成几个质量等级。这里可以有两种改法一种是直接用加权的随机森林分类器把目标变量映射为低、中、高三档另一种是保持回归分数再根据分数阈值划分等级。前者的精确度更高后者更灵活可以适应不同业务阈值。还可以考虑引入梯度提升树XGBoost、LightGBM它们处理偏斜分布的能力更强、样本并行上也有优势尤其是在增加更多特征列之后GBDT系模型通常比随机森林有更好的精度上限。不过随机森林的调参难度低、训练速度快、对异常值不敏感在中小规模数据集上仍然很实用。6.2 与Shapley值结合的深层解释feature_importances_给出的是全局重要性如果想具体回答“某一瓶酒的预测结果为什么是6分而不是5分”就需要引入SHAPSHapley Additive exPlanations值。SHAP可以绘制每个样本的解释力方向比如某种酒虽然含硫量高但酒精含量高带来的正面贡献更大所以整体质量得分被推高。这个分析对业务的价值非常大等于给黑盒模型装上了一个可与人沟通的翻译器。在项目进阶阶段强烈建议把SHAP分析补上。6.3 个人实操的经验和体会做完整轮项目有几点体会想与大家分享。第一写代码前的数据探索不是“跑流程”而是给模型定调子。相关性分析提前告诉我哪些特征重要、哪些特征冗余目标分布提前告诉我误差一定会在低分样本上偏大这些认知在模型训练前就等于已经预警了所以后续看到结果时不会意外也不会被局部噪声误导。第二调参要有边界感。网格搜索跑完不代表项目结束真正好的流程是把调参时间控制在总项目时间的20%以内。随机森林在数据量不够大的情况下调参收益有限但我还是建议手动看一遍各种参数的效果不是为了追求极致精度是为了建立“参数如何影响模型复杂度”的直觉。第三模型评价眼光要放宽。单一R²不能说清问题RMSE与MAE也不能表达误差分布形态。拿一个真实的品酒业务来类比某瓶质量为7分的酒被预测成6分和某瓶质量为4分的酒被预测成6分业务影响完全不同。回归模型本质上是在拟合一个平均值随机森林在这一任务上的表现已经足够稳定。如果你在做一个正经的葡萄酒质量评分系统下一步应该做的不是换个算法刷分而是去思考怎么用特征重要性指导酿酒工艺优化那才是让模型产生业务价值的地方。
返回列表