
简介这份资源面向机器学习初学者与需要完成课程大作业的学生围绕经典波士顿房价数据集展开回归预测实战。项目覆盖完整流程数据预处理、模型训练、结果评估与可视化分析并实现批处理梯度下降、随机梯度下降、岭回归与LASSO回归四种算法便于横向对比不同线性回归策略的收敛表现与正则化效果。压缩包共12个文件约944KB包含3个csv数据与结果文件、3个py源码脚本、3个png可视化图表、2个txt说明及1个md文档结构清晰可直接运行复现。数据集含506个样本、13个特征目标变量为房屋中位数价值。已有120人学习适合希望掌握回归建模、梯度下降调参与特征系数分析的读者参考也可作为课程设计或期末项目的模板帮助快速理解从数据到评估的完整链路。1. 波士顿房价预测这个作业到底在练什么很多人拿到「波士顿房价预测」这个题目第一反应是找一份现成代码跑通交差。但如果你真去翻那些所谓的高分项目会发现真正拉开差距的不是模型多花哨而是对数据本身的理解和处理。这个数据集只有 506 条样本、13 个特征目标是用犯罪率、房间数、到市中心距离这些变量去预测房价中位数。它小到可以在几分钟内跑完但也正因为它小每一个特征的处理方式、每一次交叉验证的切分都会直接影响最终分数。这份作业真正练的是完整流程数据加载、探索性分析、特征工程、模型选择、调参、评估、结果解释。适合刚入门机器学习、需要一份能跑通且能讲清楚的项目来建立手感的人也适合已经会调库但说不清每一步为什么这么做的人。下面我按实际做一遍的顺序把每个环节拆开讲。2. 数据加载与探索先看清 506 条样本长什么样2.1 加载数据并确认没有缺失值波士顿房价数据集在 scikit-learn 里可以直接加载但要注意版本问题。从 1.2 版本开始load_boston因为伦理争议被移除了。如果你用的是新版本要么降级要么用fetch_openml或者直接读本地 CSV。我一般会先把数据转成 DataFrame方便后续操作。import pandas as pd import numpy as np from sklearn.datasets import fetch_openml # 方式一用 fetch_openml 获取注意 name 和 version boston fetch_openml(nameboston, version1, as_frameTrue) df boston.frame # 方式二如果本地有 CSV直接读 # df pd.read_csv(boston.csv) print(df.shape) print(df.isnull().sum()) print(df.describe())这段代码先确认数据规模是 506 行、14 列13 个特征加 1 个目标 MEDV。isnull().sum()用来检查缺失值这个数据集本身没有缺失但养成检查习惯很重要。describe()看每个特征的均值、标准差、最小最大值能快速发现异常量纲。比如 RM 均值在 6.3 左右而 CRIM 最大值能到 88.97量纲差异巨大后面必须做标准化。2.2 用相关性矩阵筛掉冗余特征13 个特征不是每个都有用。有些特征之间高度相关比如 RAD 和 TAX 相关系数能到 0.9 以上同时放进线性模型会导致系数不稳定。我一般会画相关性热力图把和目标 MEDV 相关性低于 0.2 的特征先标记出来再结合业务含义决定去留。import matplotlib.pyplot as plt import seaborn as sns corr df.corr() plt.figure(figsize(12, 10)) sns.heatmap(corr, annotTrue, fmt.2f, cmapcoolwarm) plt.show() # 只看和目标的相关性 print(corr[MEDV].sort_values(ascendingFalse))从结果看RM房间数和 MEDV 正相关最强约 0.7LSTAT低收入人群比例负相关最强约 -0.74。这两个是核心特征。而 CHAS是否临河相关性只有 0.17 左右但它是 0/1 变量不能直接扔掉后面可以单独看分组分布。相关性矩阵的作用是帮你判断哪些特征值得保留、哪些可以尝试剔除而不是机械地按阈值一刀切。2.3 处理异常值房价被截断在 50 这件事波士顿房价数据有个经典问题MEDV 最大值是 50而且有 16 个样本正好等于 50。这不是真实房价而是当时数据收集时做了截断。这些样本会让模型在高端预测上产生偏差。常见做法有两种一是直接删除这些样本二是保留但用树模型来降低影响。我一般会先删掉看看效果。# 查看 MEDV 等于 50 的样本数量 print((df[MEDV] 50).sum()) # 删除这些样本 df_clean df[df[MEDV] 50].copy() print(df_clean.shape)删除后样本量从 506 降到 490。这一步不是必须的但如果你用线性回归删掉后 R² 通常会略有提升。注意删除后要重新划分训练集和测试集不能先划分再删否则测试集里混入截断值会影响评估。3. 特征工程与模型选择从线性回归到随机森林3.1 标准化和多项式特征线性模型的必备操作线性回归对量纲敏感CRIM 范围 0 到 88而 NOX 范围 0 到 0.87不标准化的话梯度下降会震荡。我一般用 StandardScaler 做 z-score 标准化把均值变 0、方差变 1。另外RM 和 LSTAT 与房价的关系不是纯线性的加二次项能提升拟合能力。from sklearn.preprocessing import StandardScaler, PolynomialFeatures from sklearn.pipeline import Pipeline from sklearn.linear_model import LinearRegression, Ridge from sklearn.model_selection import train_test_split X df_clean.drop(MEDV, axis1) y df_clean[MEDV] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) # 构建管道标准化 - 多项式 - 岭回归 pipe Pipeline([ (scaler, StandardScaler()), (poly, PolynomialFeatures(degree2, include_biasFalse)), (ridge, Ridge(alpha1.0)) ]) pipe.fit(X_train, y_train) print(Train R2:, pipe.score(X_train, y_train)) print(Test R2:, pipe.score(X_test, y_test))这里用 Pipeline 把三步串起来避免数据泄露。PolynomialFeatures 的 degree2 表示生成所有二次项和交互项特征数从 13 膨胀到 104。Ridge 回归的 alpha 是正则化强度alpha 越大惩罚越重。我试过 alpha 从 0.1 到 10在 1.0 附近测试集 R² 比较稳定。注意多项式特征必须在标准化之后做否则量纲差异会被平方放大。3.2 随机森林不用标准化但要看特征重要性树模型对量纲不敏感所以可以跳过标准化。随机森林在这个数据集上通常比线性回归表现好因为它能捕捉非线性和交互效应。我一般会先跑一个默认参数的随机森林作为 baseline再看特征重要性。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators200, max_depthNone, min_samples_split2, min_samples_leaf1, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) print(Train R2:, rf.score(X_train, y_train)) print(Test R2:, rf.score(X_test, y_test)) # 特征重要性 importances pd.Series(rf.feature_importances_, indexX.columns) print(importances.sort_values(ascendingFalse))n_estimators200 表示 200 棵树再多提升有限但训练变慢。max_depth 不限制会让树长得太深训练集 R² 能到 0.98但测试集可能只有 0.85 左右这是过拟合的信号。我一般会把 max_depth 设在 10 到 15 之间或者用 min_samples_leaf 控制叶子节点最小样本数。特征重要性排出来RM、LSTAT、DIS 通常在前三和相关性分析一致。3.3 用网格搜索找最优参数手动调参靠感觉网格搜索能系统性地遍历参数组合。但要注意参数组合多了计算量会爆炸。我一般先粗调再细调。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200, 300], max_depth: [8, 12, 16, None], min_samples_leaf: [1, 2, 4] } grid GridSearchCV( RandomForestRegressor(random_state42, n_jobs-1), param_grid, cv5, scoringr2, n_jobs-1, verbose1 ) grid.fit(X_train, y_train) print(Best params:, grid.best_params_) print(Best CV R2:, grid.best_score_) print(Test R2:, grid.score(X_test, y_test))cv5 表示 5 折交叉验证比单次划分更可靠。scoringr2 用 R² 作为评估指标。注意网格搜索是在训练集内部做交叉验证测试集只在最后评估一次不能拿测试集去调参。我跑下来最佳参数通常是 n_estimators200、max_depth12 左右测试集 R² 在 0.87 到 0.89 之间。4. 避坑与排查这五个地方最容易翻车4.1 现象测试集 R² 远低于训练集差距超过 0.1原因模型过拟合。随机森林不限制深度时每棵树都能把训练样本背下来但泛化能力差。线性回归加多项式特征后104 个特征对 490 个样本也容易过拟合。解决树模型限制 max_depth 或增大 min_samples_leaf线性模型增大 Ridge 的 alpha 或减少多项式 degree。另外交叉验证的分数比单次划分更可信如果 CV 分数和训练分数差距大说明过拟合。4.2 现象特征重要性里 CHAS 排最后但业务上临河应该值钱原因CHAS 是 0/1 变量且只有约 7% 的样本为 1。样本极度不平衡时树模型很难从这么少的正例里学到有效分裂。解决不要只看全局重要性可以单独对 CHAS1 和 CHAS0 分组看房价均值。如果差异明显可以尝试对 CHAS 做目标编码或者用线性模型单独看它的系数。但不要为了提升重要性而强行过采样这个数据集太小过采样容易引入偏差。4.3 现象用 load_boston 报错提示函数已被移除原因scikit-learn 1.2 版本移除了 load_boston因为数据集涉及伦理争议。解决降级到 1.1 版本或者用 fetch_openml(nameboston, version1)。但 fetch_openml 需要联网下载如果网络不稳定建议提前把数据存成本地 CSV。我一般会在项目里附一份 boston.csv避免环境问题导致跑不通。4.4 现象标准化后再做多项式测试集 R² 反而下降原因PolynomialFeatures 默认 include_biasTrue会生成一列全 1 的特征。如果标准化之后再加偏置列和 Ridge 的截距项冲突导致系数估计异常。解决设置 include_biasFalse。另外多项式 degree 不要超过 2degree3 会让特征数膨胀到 560样本才 490必然过拟合。如果一定要用高次项先做特征选择。4.5 现象交叉验证分数波动很大每次运行结果不一样原因随机森林本身有随机性如果不固定 random_state每次建的树不同。另外train_test_split 不固定 random_state划分不同也会导致分数波动。解决所有涉及随机的步骤都设 random_state。GridSearchCV 的 cv 参数如果用 KFold 而不是默认的也要设 shuffleTrue 和 random_state。我一般会在代码开头统一设 np.random.seed(42) 和 random_state42保证结果可复现。5. 进阶技巧用残差图和学习曲线判断模型状态5.1 残差图看模型在哪个价格区间预测不准R² 只给一个总体分数残差图能告诉你模型在哪里犯错。我一般会画预测值 vs 残差的散点图如果残差随机分布在 0 附近说明模型没有系统性偏差如果残差随预测值增大而增大说明存在异方差。import matplotlib.pyplot as plt y_pred grid.predict(X_test) residuals y_test - y_pred plt.figure(figsize(8, 6)) plt.scatter(y_pred, residuals, alpha0.6) plt.axhline(y0, colorr, linestyle--) plt.xlabel(Predicted MEDV) plt.ylabel(Residuals) plt.title(Residual Plot) plt.show()从图上看如果低价房预测值 10 到 20的残差偏正说明模型低估了低价房高价房预测值 40 以上残差偏负说明模型高估了高价房。这通常是因为 MEDV50 的截断样本被删掉后高端样本太少模型没学好。解决办法是保留截断样本但用稳健回归或者对目标做对数变换。5.2 学习曲线判断加数据还是加特征学习曲线横轴是训练样本数纵轴是分数。如果训练分数和验证分数都低且接近说明欠拟合需要加特征或换更复杂的模型如果训练分数高、验证分数低且差距大说明过拟合需要加数据或正则化。from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores learning_curve( grid.best_estimator_, X_train, y_train, train_sizesnp.linspace(0.1, 1.0, 10), cv5, scoringr2, n_jobs-1 ) train_mean train_scores.mean(axis1) val_mean val_scores.mean(axis1) plt.figure(figsize(8, 6)) plt.plot(train_sizes, train_mean, labelTrain R2) plt.plot(train_sizes, val_mean, labelValidation R2) plt.xlabel(Training samples) plt.ylabel(R2) plt.legend() plt.show()这个数据集只有 490 个样本学习曲线通常在 300 个样本后趋于平缓。如果验证分数还在上升说明加数据有用如果已经平了加数据没用得从特征工程入手。我试过把 LSTAT 做分箱、把 DIS 取对数验证分数能提升 0.01 到 0.02但不会质变。这个作业的分数上限就在 0.90 左右不要为了刷分去用深度学习506 条样本喂不饱神经网络。5.3 一个实用习惯把每次实验的参数和分数记下来我刚开始做这个作业时改了参数就跑跑完就忘结果一周后完全不记得哪个配置最好。后来养成习惯每次实验用字典记录参数和分数最后转成 DataFrame 排序。results [] for alpha in [0.1, 1.0, 10.0]: for degree in [1, 2]: pipe Pipeline([ (scaler, StandardScaler()), (poly, PolynomialFeatures(degreedegree, include_biasFalse)), (ridge, Ridge(alphaalpha)) ]) pipe.fit(X_train, y_train) results.append({ alpha: alpha, degree: degree, train_r2: pipe.score(X_train, y_train), test_r2: pipe.score(X_test, y_test) }) results_df pd.DataFrame(results) print(results_df.sort_values(test_r2, ascendingFalse))这个习惯看起来笨但能帮你快速定位最佳配置也方便写文档说明。作业要求交文档说明这份记录直接就是素材。我一般会把最终选定的参数、测试集 R²、残差图和学习曲线一起放进文档比只贴代码有说服力得多。希望帮到你。本文还有配套的精品资源点击获取