Keras+Scikit-Learn回归实战:波士顿房价MSE从22.77降到12.33
简介基于 Keras 的 Python 项目实战教程聚焦波士顿房价预测这一经典回归问题适合希望掌握深度学习回归建模的开发者或数据分析初学者。PDF 完整呈现了从波士顿房价数据集含 14 个特征目标为房屋中位价读取、特征标准化到构建单层全连接神经网络的完整代码流程模型采用线性整流激活函数、自适应矩估计优化器与均方误差损失函数同时演示了 Keras 的回归器封装配合 K 折交叉验证与网格搜索的超参数调优方法并讨论了数据尺度调整的必要性与降低误差的优化思路。资源为单个 PDF 文件大小约 366KB内容紧凑、步骤清晰包含问题描述、基准模型、数据预处理、模型调优等完整实践链适合作为回归问题的入门级实战案例读者可参照代码流程搭建自己的回归模型并将交叉验证与网格搜索方法迁移到其他数据集。该资源已有 1348 人学习浏览实用价值较高。1. 波士顿房价回归实战用 Keras 把 MSE 从 22.77 压到 12.33波士顿房价Boston House Price数据集是深度学习入门者最适合拿来练回归任务的经典样本506 条数据、13 个输入特征用 Keras 在 CPU 上就能迭代完。这份基于 Python 的实战 PDF 按“搭基准模型 → 数据标准化 → 网格搜索调参”的顺序推进每一步都有量化收益基准模型 10 折交叉验证的 MSE 是 22.77标准化后立刻降到 12.33。它适合刚学会分类、想搞明白回归问题输出层与损失函数设计的读者也适合想把 Keras 和 Scikit-Learn 串成完整评测管线的工程师。你会在同一个项目里看到 KerasRegressor、KFold、Pipeline、GridSearchCV 的协作以及几个不看代码很难发现的坑。2. 环境与建模框架把 Keras 模型封装进 Scikit-Learn 评估管线2.1 为什么让 Keras 让渡控制权给 Scikit-LearnKeras 的优势是快速搭网络定义层、编译、fit、evaluate几乎一轮对话就能跑通。但交叉验证、超参数搜索这类“实验流程管理”不是它的强项。Scikit-Learn 的 cross_val_score、KFold、GridSearchCV 已经非常成熟缺的只是一个能把 Keras 模型包装成标准估计器接口的适配层。KerasRegressor 就是这个适配层。实际项目里我把模型构建逻辑放进一个返回编译后模型的函数KerasRegressor 接收这个函数然后模型就拥有了 fit、predict、score 方法可以直接丢给 Scikit-Learn。好处是10 折交叉验证时每一折都会调用一次模型构建函数保证每折都在独立权重上训练而不是把上一折的模型参数接着训练。对回归问题来说避免数据在折与折之间“串味”尤其重要。# 原 PDF 的导入方式Keras 2.x 时代 # from keras.wrappers.scikit_learn import KerasRegressor # 当前环境推荐TensorFlow 同样是 Keras 后端包装器走 scikeras from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense from scikeras.wrappers import KerasRegressor这里说明一下导入差异。PDF 写作时 Keras 还是独立安装的包keras.wrappers.scikit_learn是官方提供的兼容层。现在 Keras 已经深度集成进 TensorFlow而且 Keras 3 把 wrappers 模块移走了所以直接用scikeras.wrappers.KerasRegressor是更稳的选择。调用方式基本不变一个 KerasRegressor 实例包住模型构建函数epochs、batch_size 这些训练参数可以直接在实例化时传入。2.2 依赖版本怎么锁一版能跑通原 PDF 项目代码的环境很多照着 PDF 复现卡住的人问题不在模型代码而在环境。load_boston() 在较新的 Scikit-Learn 里被移除keras.wrappers 在新版里被删numpy 版本和 TensorFlow 版本互相踩。我一般会这样锁环境pip install numpy1.23.5 scikit-learn1.1.3 pip install tensorflow2.11.0 pip install scikeras0.11.0参数说明numpy 锁 1.23.5 是考虑到旧模型代码里 np.random.seed 的行为和 TensorFlow 对 numpy 接口的兼容scikit-learn 锁 1.1.3 是因为这是最后一个还能直接调 datasets.load_boston() 的版本scikeras 0.11.0 适配 TensorFlow 2.11 这一代 Keras。如果你想用更新的 TensorFlow就要把 load_boston 换成从本地 CSV 读取并在 KerasRegressor 的参数名上做迁移后面避坑章节会细说。建议用 venv 或 conda 单独建一个环境别和日常项目的全局环境混在一起。TensorFlow 对依赖版本非常敏感同一个项目半个月后回来跑也可能因为某个传递依赖升级而崩。环境锁定文件就是你的后悔药。验证环境是否就绪可以先跑一段最小导入import tensorflow as tf from scikeras.wrappers import KerasRegressor print(tf.__version__) # 确认 TensorFlow 版本 print(KerasRegressor ready) # 导入成功说明包装器可用如果输出正常再继续后面的模型构建。这一步能提前暴露 90% 的环境问题不用等模型代码跑崩了才回头查。2.3 KerasRegressor 运行时行为交叉验证里每一折发生了什么用 KerasRegressor 配合 cross_val_score执行顺序和手写训练循环不同。Scikit-Learn 首先按 KFold 把数据切成训练折和验证折然后把训练折交给 KerasRegressor 的 fit()。KerasRegressor 内部调用 build_fn 指向的函数重新建一个模型编译后用你传入的 epochs 和 batch_size 训练。验证折交给 score() 或 predict() 来评估。这里有个关键细节如果你在模型构建函数里没有显式重置随机种子模型权重初始化仍然有随机性。PDF 里在建模型前设置了 np.random.seed(7)这种做法在纯 NumPy 时代有效但在 TensorFlow 后端下只能影响一部分路径这部分多少有点玄学。更稳妥的做法是在构建函数内部也固定种子或者接受结果的一定波动。后面网格搜索输出里那些很大的标准差一部分就来自这个随机性。另一个要留意的点是 verbose 参数。训练多组超参数组合时如果保持默认 verbose1终端会被训练进度条刷屏严重影响调试体验。实际调参时我习惯传 verbose0只看最终评估结果单独调试单模型时再开 verbose1 观察训练过程。3. 基准模型搭建单隐藏层 13 神经元与 10 折交叉验证的 22.77 起点3.1 波士顿数据集里 13 个输入特征与目标值的关系PDF 里说的 14 个特征是把目标列 MEDV 也算进去了。实际给到模型的输入是 13 个特征MEDV 是我们要预测的自住房屋房价中位数。这 13 个特征混合了比例、指数、距离、税率量纲差异非常大。特征含义量纲特点CRIM城镇人均犯罪率取值从 0.006 到 88.97偏度极大ZN住宅用地占比0 到 100INDUS非住宅用地占比0.46 到 27.74CHAS是否临河0/1 虚拟变量NOX一氧化氮浓度0.385 到 0.871RM平均房间数3.56 到 8.78AGE1940 年前建成自住单位占比2.9 到 100DIS到就业中心加权距离1.13 到 12.13RAD到高速公路便利指数1 到 24TAX每 1 万美元不动产税率187 到 711PTRATIO师生比12.6 到 22B城镇中黑人比例1978 年统计口径仅教学参考0.32 到 396.9LSTAT低收入人群占比1.73 到 37.97放在同一行看TAX 和 B 的范围是 RM、NOX 的几十倍。如果一个模型的权重初始化时对每一列一视同仁大数值列会天然在梯度里占据更大比重。这就是后面非做标准化不可的直接原因。还要补充一句这个数据集是 1978 年统计的特征口径带有明显时代背景比如 B 的统计方式在今天看来并不合适。教学演示它非常有价值但别直接把这份数据用于当前社会语境下的真实房价预测产品。3.2 构建 create_model 函数为什么输出层不加激活函数按 PDF 的做法模型构建函数被设计成可配置的。units_list 接收一个列表列表长度决定隐藏层层数每个元素决定该层神经元数量。这样做的好处是后面做网格搜索时只需在参数网格里换列表内容就能在“单层 20 个神经元”和“双层 13-6 结构”之间切换。import numpy as np from sklearn import datasets from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense def create_model(units_list[13], optimizeradam, initnormal): # 第一个隐藏层input_dim13对应波士顿数据的 13 个输入特征 model Sequential() units units_list[0] model.add(Dense(unitsunits, activationrelu, input_dim13, kernel_initializerinit)) # units_list 里如果还有更多元素继续追加隐藏层 for units in units_list[1:]: model.add(Dense(unitsunits, activationrelu, kernel_initializerinit)) # 输出层回归任务不设激活函数直接输出连续数值 model.add(Dense(units1, kernel_initializerinit)) model.compile(lossmean_squared_error, optimizeroptimizer) return model几个参数值得展开。input_dim13 明确告诉模型输入维度这是第一层 Dense 必需的kernel_initializer 默认值是 normal控制权重初始分布后面网格搜索会把它和 glorot_uniform 做对比。输出层 units1 且不写 activation这是分类和回归任务最关键的区别分类输出层用 softmax 得到概率分布回归输出层需要的是原始数值任何激活函数都会把预测值压缩到某个区间反而限制表达能力。损失函数用 mean_squared_error优化器是 adam。MSE 对预测值和真实值的平方差求平均值越小越好。传统机器学习在波士顿房价上的 MSE 一般能到 14 左右PDF 给神经网络定的目标线是 10 左右。这个数字后面会反复出现作为改进的参照系。3.3 用 KerasRegressor 和 KFold 拿到基准结果模型函数定义好后封装成 KerasRegressor再交给 cross_val_score。固定随机种子是为了让 KFold 切分可复现至少数据划分这一层不会每次跑都不一样。# 导入数据并固定随机种子 dataset datasets.load_boston() x dataset.data Y dataset.target seed 7 np.random.seed(seed) # 封装 Keras 模型epochs200batch_size5 model KerasRegressor(modelcreate_model, epochs200, batch_size5, verbose0) # 10 折交叉验证评估 from sklearn.model_selection import KFold, cross_val_score kfold KFold(n_splits10, shuffleTrue, random_stateseed) results cross_val_score(model, x, Y, cvkfold) print(Baseline: %.2f (%.2f) MSE % (results.mean(), results.std()))说明这里用 KerasRegressor(modelcreate_model) 是当前 scikeras 的写法原 PDF 里是 build_fncreate_model语义一样。KFold 的 n_splits10 把 506 条数据切成 10 份每份大约 50 条样本作为验证集其余 9 份做训练。shuffleTrue 打散顺序random_stateseed 保证打散方式可复现。执行结果 Baseline: 22.77 (11.57)。这个 11.57 的标准差非常大意味着模型在不同数据折上的表现差异明显。主要原因有两个样本量本身只有 506 条单折验证集太小更重要的是输入特征未标准化网络训练不稳定。22.77 就是我后续所有改进的基准线接下来的每一步都要和它比。4. 数据标准化与 Pipeline为什么 22.77 能先降到 12.334.1 量纲悬殊如何拖累神经网络的梯度更新回到 3.1 那张表格。TAX 的取值范围是 187 到 711RM 只有 3.56 到 8.78。Dense 层的计算是输入乘权重加偏置反向传播时梯度大小和输入值正相关。输入数值大的特征列梯度贡献也大权重更新会被这些列主导RM、NOX 这类小数值特征的信息被压在背景里。还有一个容易忽略的点ReLU 激活函数对输入尺度没有天然的归一化能力。输入范围不统一时ReLU 在某些神经元上一旦进入负区间就会一直输出 0梯度也跟着断开这就是神经元死亡的前兆。标准化之后数据集中在 0 附近且方差一致ReLU 的梯度能更稳定地回流。标准化就是把每一列变成均值为 0、方差为 1 的分布。处理后的特征不再有“谁数值大谁说了算”的问题每个维度在梯度里的地位平等。这也是为什么 PDF 在构建基准模型之后第二步就是数据预处理。4.2 StandardScaler 的作用与 Pipeline 的封装方式from sklearn.preprocessing import StandardScaler scaler StandardScaler() x_scaled scaler.fit_transform(x) print(标准化后各列均值应接近 0:, x_scaled.mean(axis0).round(3)) print(标准化后各列标准差应接近 1:, x_scaled.std(axis0).round(3))fit_transform 先计算每一列的均值和标准差再把数据转换成 (x - mean) / std。这样处理后数值不再带原始单位。注意 fit_transform 只能用一次后续对验证集的转换必须沿用训练集算出的均值和标准差不能重新计算否则会把验证集信息泄漏进模型评估过程。PDF 的做法是把它放进 Pipeline让交叉验证每一折内部自动完成“先标准化、再训练”from sklearn.pipeline import Pipeline steps [] steps.append((standardize, StandardScaler())) steps.append((mlp, model)) pipeline Pipeline(steps) kfold KFold(n_splits10, shuffleTrue, random_stateseed) results cross_val_score(pipeline, x, Y, cvkfold) print(Standardize: %.2f (%.2f) MSE % (results.mean(), results.std()))Pipeline 的核心价值在于顺序封装。cross_val_score 切好训练折和验证折后pipeline 先只对训练折执行 fit_transform再用同一组标准化的参数去 transform 验证折。这样验证折完全没参与 scaler 的参数计算评分才可信。如果手动先对整个 x 做 fit_transform 再做交叉验证scaler 偷看了所有数据测试分数会偏乐观这也是后面避坑章节要展开的点。这里还能带出一个使用习惯steps 列表里每个元素是名字估计器对象的元组名字可以随便起但建议起有意义的。后面要检查 pipeline 中间结果时pipeline.named_steps[standardize] 一目了然。4.3 标准化前后的对比预处理比调参更容易见效代码跑出来的结果非常直观。方案10 折交叉验证 MSE标准差数据未标准化22.7711.57数据标准化后12.336.96两组数字放在一起结论很明显只加了一个 StandardScalerMSE 均值从 22.77 降到 12.33标准差从 11.57 收敛到 6.96。这个提升幅度比后面折腾网络结构和超参数带来的变化还大。所以遇到回归模型结果不理想先检查特征尺度再谈调参顺序不能反。标准差从 11.57 收敛到 6.96这个信号比均值变化更值得注意。它说明模型的性能从“某些折很差、某些折还行”变得更加稳定对样本划分不那么敏感。回归模型在测试阶段真正怕的不是均值稍微偏高而是方差大导致的不可控。所以标准化这一步带来的不只是精度提升还有稳定性的提升。标准化后的 12.33 已经接近传统机器学习 14 左右的水平原计划里的“10 左右”目标线接下来要靠调参逼近。5. 网格搜索调参与避坑四组超参数、30 分钟训练与四个坑5.1 参数网格设计从单层 20 到双层 13-6代码中 create_model 的可配置性在这里发挥作用。units_list 决定网络拓扑optimizer 决定优化器init 决定权重初始化方式。再加上训练过程的 epochs 和 batch_size一共五个超参数。参数网格如下from sklearn.model_selection import GridSearchCV param_grid {} param_grid[units_list] [[20], [13, 6]] param_grid[optimizer] [rmsprop, adam] param_grid[init] [glorot_uniform, normal] param_grid[epochs] [100, 200] param_grid[batch_size] [5, 20] # 注意先做标准化再进网格搜索避免量纲干扰调参过程 scaler StandardScaler() scaler_x scaler.fit_transform(x) grid GridSearchCV(estimatormodel, param_gridparam_grid, verbose0) results grid.fit(scaler_x, Y) print(Best: %f using %s % (results.best_score_, results.best_params_))组合数算一下拓扑 2 种乘优化器 2 种乘初始化 2 种乘 epochs 2 种乘 batch_size 2 种总共 32 组。GridSearchCV 默认做 5 折交叉验证也就是 32 乘 5 等于 160 次模型训练。每次训练最长 200 个 epoch这在 CPU 上就是半小时起步的量级PDF 里提到用阿里云 CPU 跑了约 30 分钟是完全正常的时间开销。init 的两个候选值得单独说。glorot_uniform 按每层输入输出维度计算一个均匀分布区间初始化权重时从这个区间里采样是 ReLU 时代的默认选择之一normal 则是从高斯分布里采权值。在网络较深时两者差异会被放大但对波士顿这种 13 输入的小网络影响有限。网格搜索把它放进来更多是为了验证“初始化方式是否值得在意”这个命题。这组代码有一个数据泄漏隐患scaler_x 是在网格搜索前对全量数据做的 fit_transform。更严格的写法是把 StandardScaler 也放进 Pipeline让每一折独立计算标准化参数。原 PDF 为了简洁直接缩放在前暴露的问题是评分会略偏乐观。5.2 结果怎么读Best 分数不是 MSE而是估计器自带的 score执行完网格搜索终端输出的内容很容易让人误读。例如Best: 99.163545 using {batch_size: 20, epochs: 100, init: normal, optimizer: rmsprop, units_list: [20]} 37.223335 (21.225069) with: {batch_size: 5, epochs: 100, init: glorot_uniform, optimizer: rmsprop, units_list: [20]} 24.520587 (16.924204) with: {batch_size: 5, epochs: 100, init: glorot_uniform, optimizer: rmsprop, units_list: [13, 6]}如果直接把 99.163545 当成 MSE会以为模型错到离谱。实际情况是GridSearchCV 在不传 scoring 参数时默认使用估计器自带的 score 方法。KerasRegressor 的 score 在多数版本下返回的是决定系数也就是 R²。99.16 这个值应该理解成约 0.9916 的 R²而不是 MSE。前面的 37.22、24.52 同理它们是 R² 偏低甚至为负的试验组合。要统一评分口径显式指定 scoringgrid GridSearchCV(estimatormodel, param_gridparam_grid, scoringneg_mean_squared_error) results grid.fit(scaler_x, Y)scikit-learn 的评分规则是“越大越好”所以 MSE 要被取负号叫 neg_mean_squared_error。取回结果时再取负得到的就是正常意义的 MSE。这个细节不处理好网格搜索结果和前面基准模型的 MSE 完全没法对比。结果速览能直观看到参数组合的影响参数组合节选mean_test_score说明units[20], rmsprop, glorot_uniform, epochs100, batch_size537.22R² 偏低等效 MSE 较差units[13,6], adam, normal, epochs100, batch_size535.45双层结构没有带来优势units[20], rmsprop, normal, epochs100, batch_size2099.16最优组合R² 接近 0.99同一组 units 和优化器下batch_size 和 init 的变化直接拉开差距。这也是为什么网格搜索虽然耗时但在小数据集上值得跑一次。5.3【避坑】复现这个项目我踩过的四个坑坑一把 best_score_ 当成 MSE整份结果全读歪现象看到 Best: 99.163545第一反应是“这损失怎么这么大”对照标准化的 12.33 完全对不上。 原因GridSearchCV 默认用的是 KerasRegressor.score即决定系数 R²不是训练时的 MSE 损失。 解决在 GridSearchCV 里显式传 scoringneg_mean_squared_error让搜索结果和基准模型用同一个指标。坑二新版 scikit-learn 直接报 load_boston 不存在现象datasets.load_boston() 抛 AttributeError找不到这个属性。 原因scikit-learn 1.2 起移除了波士顿房价数据集官方不再维护该数据。 解决环境锁定 scikit-learn1.1.3 复现原 PDF想用新版本就从本地 CSV 读数据自行拆分特征列和目标列。坑三Keras 导入和包装类的 API 迁移现象按 PDF 写 from keras.wrappers.scikit_learn import KerasRegressor直接 ModuleNotFoundError。 原因Keras 3 移除了这个模块TensorFlow 2.16 之后的 wrappers 也逐步废弃。 解决改用 pip install scikeras 安装导入路径换成 from scikeras.wrappers import KerasRegressor并把构造函数的 build_fn 参数名改成 model。create_model 的内部代码基本不用动如果参数透传不生效改用 model__ 前缀传参。坑四训练时长失控不知道要等多久现象参数一多就开跑结果半小时只跑了一小半进度信息还被 verbose1 刷屏。 原因32 组超参数乘 5 折交叉验证每组最多 200 epochsbatch_size5 时单 epoch 轮次虽快但总轮次多。 解决先建一个 5 epochs 的迷你版参数网格试跑估算单次训练时间再乘总次数正式跑时把 verbose0终端只保留最后一行结果。想让训练稳定收敛也可以在模型构建函数里加入 EarlyStopping 回调按验证损失自动提前终止。四个坑基本覆盖了从环境安装到结果解读的整个流程。接下来用最优参数重建模型就是水到渠成的事。6. 用最优参数重建最终模型一份可直接复现的 Pipeline 脚本网格搜索锁定的最优参数是单隐藏层 20 个神经元、rmsprop 优化器、normal 初始化、epochs100、batch_size20。这一步要做的是把参数写回模型用 Pipeline 完整地串起标准化和训练并验证它的 10 折交叉验证表现。# 最终模型参数来自 GridSearchCV 的 best_params_ final_model KerasRegressor(modelcreate_model, units_list[20], optimizerrmsprop, initnormal, epochs100, batch_size20, verbose0) # 标准化 模型封装成一条完整流水线 steps [(standardize, StandardScaler()), (mlp, final_model)] pipeline Pipeline(steps) # 用同一套 KFold 设置做 10 折评估 kfold KFold(n_splits10, shuffleTrue, random_stateseed) scores cross_val_score(pipeline, x, Y, cvkfold, scoringneg_mean_squared_error) print(最终 Pipeline MSE: %.2f (%.2f) % (-scores.mean(), scores.std()))注意第 5 章提过的评分口径这里 cross_val_score 显式传了 scoringneg_mean_squared_error取负号得到的就是和基准模型同口径的 MSE。这样最终成绩才能和 22.77、12.33 直接对比。我自己的习惯是再把模型拟合并抽查前几条预测确认输出量级在合理范围# 全量数据拟合一次用于查看预测结果形态 pipeline.fit(x, Y) preds pipeline.predict(x[:5]) for i, (p, t) in enumerate(zip(preds, Y[:5])): print(样本 %d预测值 %.2f真实值 %.2f % (i, p, t))这段只是演示模型能输出连续房价数值真正的泛化能力以 10 折交叉验证为准。如果你要部署这个模型记得把 pipeline 整体保存下来而不是只保存模型权重。因为推理时还得用同一个 StandardScaler 的均值和标准差去转换输入Pipeline 恰好把这件事和模型绑在一起。这套流程沉淀下来的方法论很朴素先固定随机种子和评估方案拿到基准再做数据标准化最后才用网格搜索调网络结构。从那以后我每次跑回归任务都会强制走一遍固定流程先检查特征值域再确认 StandardScaler 是否放进 Pipeline最后在 GridSearchCV 里显式写明 scoring。光是最后一条就避免过不止一次把 R² 当成 MSE 的笑话。希望帮到你。本文还有配套的精品资源点击获取