1. 从零开始:波士顿房价预测,一个经典的机器学习起点

嘿,朋友们,今天我想和大家聊聊一个特别经典的机器学习实战项目——波士顿房价预测。这几乎是每个学机器学习的人都会遇到的“新手村”任务,就像学编程先写“Hello World”一样。但别小看它,这个数据集麻雀虽小,五脏俱全,特别适合用来理解从最基础的线性模型到复杂的集成模型,整个模型优化的完整路径。

我自己刚入门那会儿,也在这个数据集上折腾了好久。当时最大的困惑就是:为什么我的模型预测不准?为什么换了算法分数就上去了?后来踩过不少坑,调过无数参数,才慢慢摸到点门道。所以,我想把我这十年在AI和数据科学领域摸爬滚打的经验,特别是关于模型选择和优化的那些“血泪史”,通过这个案例分享给大家。咱们不搞那些虚头巴脑的理论,就实实在在地看代码、跑结果、做对比,让你亲手感受不同算法的“脾气”。

波士顿房价数据集(Boston Housing)在机器学习圈里名气很大,它包含了上世纪70年代波士顿周边506个街区的房价中位数(MEDV)以及13个可能影响房价的特征,比如犯罪率(CRIM)、房间数(RM)、师生比例(PTRATIO)等等。我们的任务,就是根据这13个特征,训练一个模型,让它能尽可能准确地预测出房价。这本质上是一个回归问题,因为我们要预测的是一个连续的数值(房价),而不是像判断邮件是不是垃圾邮件那样的分类标签。

这个项目特别适合两类朋友:一是刚刚学完机器学习理论,想找个实战项目练手的新手;二是已经有一定基础,但想系统性地了解不同模型性能差异和优化技巧的开发者。跟着我走完这一趟,你不仅能复现一个完整的机器学习流程,更能建立起一套“遇到问题该往哪个方向优化”的直觉,这才是最值钱的。

2. 数据初探与基线模型:线性回归的“第一印象”

万事开头难,但开头也得开。咱们的第一步,永远是先认识数据。我见过太多人拿到数据就直接往模型里塞,结果模型表现稀烂,还不知道问题出在哪。记住,数据科学家80%的时间都在和数据打交道,建模只占一小部分。

2.1 数据加载与“体检”

我们先来把数据请出来看看。这里有个小技巧,虽然原始数据是CSV文件,但scikit-learn库里内置了这个经典数据集,用起来更方便,还能看到官方描述。

# 导入必备工具包,这是我们的工具箱
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_boston

# 设置一下画图风格,让图表好看点
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("husl")

# 加载数据
boston = load_boston()
print(boston.DESCR[:500]) # 先看看数据集的简要描述

# 把数据转换成我们熟悉的Pandas DataFrame,这样操作起来更顺手
df = pd.DataFrame(boston.data, columns=boston.feature_names)
df['MEDV'] = boston.target # 把目标变量(房价)也加进去

print(f"数据集形状: {df.shape}")
print(df.head())

运行这几行代码,你就能看到数据的全貌了:506条样本,13个特征,1个目标变量。关键是要看看有没有缺失值。

print(df.isnull().sum())

幸运的是,波士顿房价数据集非常“干净”,没有缺失值。这给我们省去了数据清洗中非常麻烦的一步。但别高兴太早,没有缺失值不代表数据没问题。我们得看看特征之间的关系,也就是相关性。这里我特别喜欢用热力图(Heatmap),一目了然。

plt.figure(figsize=(14, 10))
# 计算相关系数矩阵
corr_matrix = df.corr().round(2)
# 画热力图,annot=True显示数值,cmap选个好看的色彩映射
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0)
plt.title('特征相关性热力图')
plt.tight_layout()
plt.show()

从热力图上,你能立刻发现哪些特征和房价(MEDV)关系大。比如,RM(房间数)和房价是正相关(约0.7),这很好理解,房间越多房子越大,自然越贵。而LSTAT(低收入人群比例)和房价是强负相关(约-0.74),低收入人群比例高的社区,房价往往较低。这些观察都能帮助我们理解模型后来为什么会做出某些预测。

2.2 建立我们的第一个模型:线性回归

数据看完了,手痒了吧?咱们先来建一个最简单的模型——多元线性回归。把它作为我们的“基线模型”(Baseline Model)。基线模型就像一把尺子,后面所有更复杂的模型都要跟它比一比,看提升有多大。

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score

# 准备特征X和目标y
X = df.drop('MEDV', axis=1)
y = df['MEDV']

# 划分训练集和测试集,random_state固定随机种子,确保结果可复现
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

print(f"训练集样本数: {X_train.shape[0]}")
print(f"测试集样本数: {X_test.shape[0]}")

# 创建线性回归模型并训练
lr_model = LinearRegression()
lr_model.fit(X_train, y_train)

# 用训练好的模型在测试集上进行预测
y_pred = lr_model.predict(X_test)

# 评估模型表现
lr_mse = mean_squared_error(y_test, y_pred)
lr_rmse = np.sqrt(lr_mse) # RMSE和房价单位一致,更好解释
lr_r2 = r2_score(y_test, y_pred)

print(f"【基线模型-线性回归】")
print(f"R²分数 (测试集): {lr_r2:.4f}")
print(f"均方误差 (MSE): {lr_mse:.4f}")
print(f"均方根误差 (RMSE): {lr_rmse:.4f}")

我跑出来的结果,R²分数大概在0.66到0.73之间波动(取决于随机划分)。这意味着我们的模型能解释大约70%的房价波动。RMSE大约在4.5到5.0之间。也就是说,模型预测的房价平均会偏离真实房价大约4.5万到5万美元。

这个结果怎么样?对于第一个模型来说,不算差,但绝对有巨大的提升空间。我们可以可视化一下预测值和真实值的对比,问题会更直观。

# 绘制预测值与真实值的散点图
plt.figure(figsize=(8, 6))
plt.scatter(y_test, y_pred, alpha=0.6, edgecolors='w', linewidth=0.5)
# 画一条理想的对角线(预测值=真实值)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
plt.xlabel('真实房价 (MEDV)')
plt.ylabel('预测房价 (MEDV)')
plt.title('线性回归预测 vs 真实值')
plt.show()

# 再画个残差图看看
residuals = y_test - y_pred
plt.figure(figsize=(8, 6))
plt.scatter(y_pred, residuals, alpha=0.6)
plt.axhline(y=0, color='r', linestyle='--')
plt.xlabel('预测值')
plt.ylabel('残差 (真实值 - 预测值)')
plt.title('线性回归残差图')
plt.show()

从散点图能看到,大部分点围绕在红色对角线附近,但在房价较高(>35)和较低(<15)的区域,点开始明显偏离对角线,说明模型在这些极端值上预测不准。残差图更能说明问题:理想情况下,残差应该随机、均匀地分布在0线上下。但我们的图显示,当预测值在20-25之间时,残差多为负值(预测偏高);而在预测值两端,残差分布变宽,说明误差增大。这提示我们数据可能存在非线性关系,或者存在一些异常值(离群点)干扰了线性模型。

3. 特征工程与模型优化:从“能用”到“好用”

基线模型建好了,也看到了它的不足。接下来就是机器学习中最有意思也最考验经验的部分了——特征工程和模型调优。很多人以为机器学习就是调包调参,其实功夫都在数据里。我经常跟团队说:“垃圾数据进,垃圾模型出。” 特征工程就是给模型“喂好粮”的过程。

3.1 特征选择:给模型“减负”

线性回归模型给了我们每个特征的系数(权重),我们可以看看哪些特征影响大。

# 查看线性回归模型的系数
coeff_df = pd.DataFrame(lr_model.coef_, X.columns, columns=['Coefficient'])
coeff_df = coeff_df.sort_values(by='Coefficient', ascending=False)
print(coeff_df)

# 可视化特征重要性(系数的绝对值)
plt.figure(figsize=(10, 6))
coeff_df['Coefficient'].abs().sort_values().plot(kind='barh')
plt.xlabel('系数绝对值')
plt.title('线性回归特征重要性(基于系数大小)')
plt.tight_layout()
plt.show()

你会发现,RM(房间数)的系数是正的且较大,LSTAT(低收入比例)和PTRATIO(师生比)的系数是负的且绝对值大。这符合我们的直觉。但也有一些特征的系数非常小(比如CHAS查尔斯河虚拟变量),这意味着它对房价的线性影响很微弱。一个自然的想法是:我们只用最重要的几个特征来建模,会不会更好?这既能防止过拟合,又能提升模型可解释性。

我们可以尝试只选用与房价相关性最高的前3个特征(LSTAT, RM, PTRATIO)来重建线性回归模型。

# 选择相关性最高的三个特征
selected_features = ['LSTAT', 'RM', 'PTRATIO']
X_sel = df[selected_features]

# 重新划分数据集
X_train_sel, X_test_sel, y_train, y_test = train_test_split(X_sel, y, test_size=0.2, random_state=42)

# 训练新模型
lr_model_sel = LinearRegression()
lr_model_sel.fit(X_train_sel, y_train)
y_pred_sel = lr_model_sel.predict(X_test_sel)

# 评估
lr_r2_sel = r2_score(y_test, y_pred_sel)
lr_rmse_sel = np.sqrt(mean_squared_error(y_test, y_pred_sel))

print(f"【三特征线性回归】")
print(f"使用的特征: {selected_features}")
print(f"R²分数 (测试集): {lr_r2_sel:.4f}")
print(f"RMSE: {lr_rmse_sel:.4f}")
print(f"与原模型R²对比: {lr_r2:.4f} -> {lr_r2_sel:.4f}")

实测下来,你会发现模型的R²分数下降了(可能从0.73降到0.68左右)。这说明,虽然这三个特征很重要,但其他特征也提供了一些有用的信息,粗暴地扔掉它们会损失一部分预测能力。特征选择不是简单的“Top N”,更高级的方法可以用递归特征消除(RFE)或者基于模型(如Lasso)的特征选择,这个我们后面会谈到。

3.2 数据标准化:为复杂模型铺路

线性回归对数据尺度不敏感,但接下来我们要尝试的很多模型(如SVM、带正则化的回归)都对特征的尺度非常敏感。如果TAX(税率)的取值范围是几百到几千,而CHAS(是否临河)只是0或1,模型就会更“照顾”数值大的特征,这不公平。所以,我们需要做标准化(Standardization),把各个特征都拉到均值为0、标准差为1的同一尺度上。

from sklearn.preprocessing import StandardScaler

# 初始化标准化器
scaler_X = StandardScaler()
scaler_y = StandardScaler()

# 注意:先划分数据,再分别对训练集和测试集进行标准化,避免数据泄露!
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 拟合训练集,并转换训练集和测试集
X_train_scaled = scaler_X.fit_transform(X_train)
X_test_scaled = scaler_X.transform(X_test) # 注意:测试集用训练集的参数转换

# 对于目标变量y,有时也需要标准化,特别是对于某些对输出尺度敏感的模型
y_train_scaled = scaler_y.fit_transform(y_train.values.reshape(-1, 1)).ravel()
y_test_scaled = scaler_y.transform(y_test.values.reshape(-1, 1)).ravel()

print("标准化后的训练集特征示例(前5行,前3列):")
print(X_train_scaled[:5, :3])
print(f"均值(应接近0): {X_train_scaled[:, 0].mean():.2f}")
print(f"标准差(应接近1): {X_train_scaled[:, 0].std():.2f}")

标准化之后,数据就准备好了。这里有个非常重要的坑我踩过:千万不能用整个数据集去fit标准化器,然后再划分训练测试集! 这会导致测试集的信息“泄露”到训练过程中,让模型评估结果虚高。正确的做法一定是先划分,再用训练集的统计量(均值、方差)去转换训练集和测试集。

4. 进阶模型大比拼:寻找更优解

基线模型有了,数据也处理好了,是时候请出更强大的模型了。我们的目标是找到那个在波士顿房价数据上表现最好的“神器”。我会带大家尝试从线性模型的改进版,到树模型,再到集成模型,并对比它们的表现。

4.1 正则化线性模型:防止“过拟合”的利器

线性回归有时候会为了拟合训练数据而把系数搞得特别大,尤其是特征之间存在多重共线性时。这会导致模型在训练集上表现很好,在测试集上就崩了,也就是过拟合。正则化就是给模型加个“紧箍咒”,限制系数的大小。

岭回归(Ridge) 和 Lasso回归 是最常用的两种。岭回归惩罚系数的平方和(L2正则),让所有系数都变小但不为零;Lasso回归惩罚系数的绝对值之和(L1正则),它更狠,能把一些不重要的特征的系数直接压缩到零,从而实现自动特征选择。

from sklearn.linear_model import Ridge, Lasso, ElasticNet
from sklearn.model_selection import cross_val_score

# 准备标准化后的数据(用之前scaler转换后的数据)
models = {
    '线性回归': LinearRegression(),
    '岭回归 (alpha=1.0)': Ridge(alpha=1.0),
    'Lasso回归 (alpha=0.01)': Lasso(alpha=0.01, max_iter=10000), # Lasso需要更多迭代
    '弹性网络 (alpha=0.01, l1_ratio=0.5)': ElasticNet(alpha=0.01, l1_ratio=0.5, max_iter=10000)
}

results = {}
for name, model in models.items():
    model.fit(X_train_scaled, y_train_scaled)
    y_pred = model.predict(X_test_scaled)
    # 将预测值转换回原始房价尺度,以便计算有实际意义的RMSE
    y_pred_original = scaler_y.inverse_transform(y_pred.reshape(-1, 1)).ravel()
    mse = mean_squared_error(y_test, y_pred_original)
    r2 = r2_score(y_test, y_pred_original)
    results[name] = {'RMSE': np.sqrt(mse), 'R2': r2}
    print(f"{name:30} - R²: {r2:.4f}, RMSE: {np.sqrt(mse):.4f}")

# 用交叉验证更稳健地评估岭回归的alpha参数
alphas = [0.001, 0.01, 0.1, 1, 10, 100]
ridge_scores = []
for alpha in alphas:
    ridge = Ridge(alpha=alpha)
    # 5折交叉验证,评估指标为负均方误差(sklearn约定),所以取负号
    cv_scores = -cross_val_score(ridge, X_train_scaled, y_train_scaled, cv=5, scoring='neg_mean_squared_error')
    ridge_scores.append(np.sqrt(cv_scores.mean())) # 存储RMSE

plt.figure(figsize=(8,5))
plt.plot(alphas, ridge_scores, marker='o')
plt.xscale('log') # alpha范围大,用对数坐标
plt.xlabel('正则化强度 alpha')
plt.ylabel('交叉验证 RMSE')
plt.title('岭回归 alpha 参数调优')
plt.grid(True)
plt.show()

跑完这段代码,你会发现一个有趣的现象:在默认参数下,岭回归和Lasso回归的表现可能和普通线性回归差不多,甚至略差一点。这是因为波士顿数据集相对简单,特征也不多,过拟合问题不严重。但当你尝试用交叉验证去调alpha参数时,会发现存在一个最优值能使模型在验证集上表现最好。Lasso回归的另一个妙用是看它把哪些特征的系数压缩为零了,这本身就是一种特征选择。

4.2 支持向量回归(SVR):在高维空间寻找最优边界

支持向量机(SVM)不仅能分类,也能做回归,这就是支持向量回归(SVR)。它的核心思想是:不要求所有点都精确落在回归线上,而是设定一个“容忍带”(epsilon-tube),只要预测值和真实值的偏差在这个带子里,就不算损失。SVR对数据尺度和核函数选择非常敏感,这也是为什么我们之前要做标准化。

from sklearn.svm import SVR

# 尝试不同的核函数
svr_kernels = {
    'SVR-线性核': SVR(kernel='linear', C=1.0, epsilon=0.1),
    'SVR-多项式核': SVR(kernel='poly', C=1.0, degree=2, epsilon=0.1),
    'SVR-径向基核(RBF)': SVR(kernel='rbf', C=1.0, gamma='scale', epsilon=0.1)
}

svr_results = {}
for name, model in svr_kernels.items():
    model.fit(X_train_scaled, y_train_scaled)
    y_pred = model.predict(X_test_scaled)
    y_pred_original = scaler_y.inverse_transform(y_pred.reshape(-1, 1)).ravel()
    mse = mean_squared_error(y_test, y_pred_original)
    r2 = r2_score(y_test, y_pred_original)
    svr_results[name] = {'RMSE': np.sqrt(mse), 'R2': r2}
    print(f"{name:25} - R²: {r2:.4f}, RMSE: {np.sqrt(mse):.4f}")

在我的测试中,SVR-RBF核的表现通常是最好的,R²能到0.85以上,远超线性回归。这是因为房价和特征之间的关系很可能不是线性的,而RBF核能够捕捉这种复杂的非线性关系。但SVR也有缺点:训练速度相对较慢(尤其是大数据集),而且对参数(C, gamma, epsilon)的调优要求比较高,调不好效果可能很差。

4.3 决策树与随机森林:直观的非线性模型

决策树非常直观,它通过一系列“如果...那么...”的规则来做出预测。它天生就能处理非线性关系,而且不需要对数据做标准化。

from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor

tree_models = {
    '决策树 (max_depth=5)': DecisionTreeRegressor(max_depth=5, random_state=42),
    '随机森林 (n_estimators=100)': RandomForestRegressor(n_estimators=100, max_depth=None, random_state=42, n_jobs=-1) # n_jobs=-1使用所有CPU核心
}

for name, model in tree_models.items():
    model.fit(X_train, y_train) # 树模型不需要标准化数据
    y_pred = model.predict(X_test)
    mse = mean_squared_error(y_test, y_pred)
    r2 = r2_score(y_test, y_pred)
    print(f"{name:35} - R²: {r2:.4f}, RMSE: {np.sqrt(mse):.4f}")

# 随机森林还能给出特征重要性,这比线性模型的系数更有解释性
rf_model = RandomForestRegressor(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
importances = rf_model.feature_importances_
feat_imp_df = pd.DataFrame({'feature': X.columns, 'importance': importances}).sort_values('importance', ascending=False)

plt.figure(figsize=(10,6))
sns.barplot(x='importance', y='feature', data=feat_imp_df)
plt.title('随机森林特征重要性')
plt.tight_layout()
plt.show()

决策树如果深度不加限制(max_depth=None),很容易过拟合,在训练集上完美,测试集上糟糕。所以我们需要通过max_depth、min_samples_split等参数来剪枝。而随机森林是决策树的集成,它通过构建多棵树并投票(回归是平均)来降低过拟合风险,效果通常比单棵决策树稳定且好得多。从特征重要性图里,你能再次确认LSTAT和RM是最关键的两个特征。

5. 终极武器:梯度提升决策树(GBDT)及其优化

终于来到我们今天的重头戏——梯度提升决策树(GBDT)。它在很多表格数据竞赛(比如Kaggle)中都是夺冠热门,因为它能非常高效地捕捉复杂的特征交互和非线性关系,而且通常不需要很精细的特征工程就能取得很好的效果。

5.1 GBDT初体验:开箱即用的强大

我们先直接用scikit-learn里的GradientBoostingRegressor试试,用默认参数看看效果。

from sklearn.ensemble import GradientBoostingRegressor

# 使用默认参数的GBDT
gbdt_default = GradientBoostingRegressor(random_state=42, n_estimators=100)
gbdt_default.fit(X_train, y_train) # GBDT也不需要数据标准化
y_pred_gbdt = gbdt_default.predict(X_test)

gbdt_r2 = r2_score(y_test, y_pred_gbdt)
gbdt_rmse = np.sqrt(mean_squared_error(y_test, y_pred_gbdt))

print(f"【GBDT默认参数】")
print(f"R²分数: {gbdt_r2:.4f}")
print(f"RMSE: {gbdt_rmse:.4f}")
print(f"相比线性回归(R²={lr_r2:.4f}),提升巨大!")

不出意外的话,默认参数的GBDT就能轻松达到0.85甚至0.9以上的R²分数,RMSE也能降到3.5以下。这个效果已经比我们之前尝试的所有模型都高出一大截。这就是集成学习的威力:通过串行地训练多棵弱决策树(通常是浅层树),每一棵都试图纠正前一棵树的残差,最终组合成一个强模型。

5.2 GBDT核心参数调优:从“好用”到“顶尖”

默认参数虽然强,但还有优化空间。GBDT有几个关键参数,调好了能让模型更上一层楼:

  • n_estimators: 树的数量。太少会欠拟合,太多会增加计算量且可能过拟合。
  • learning_rate: 学习率,每棵树对最终结果的贡献权重。越小需要越多的树,但模型可能更稳健。
  • max_depth: 每棵决策树的最大深度。控制单棵树的复杂度,防止过拟合。
  • subsample: 子采样比例,比如0.8表示每棵树只用80%的随机样本训练。这是随机性的来源之一,有助于防止过拟合。

我们可以用网格搜索(Grid Search)来系统地寻找最优参数组合。虽然计算量有点大,但对于这种规模的数据集,完全可以在个人电脑上跑。

from sklearn.model_selection import GridSearchCV

# 定义一个参数网格
param_grid = {
    'n_estimators': [100, 200, 300],
    'learning_rate': [0.01, 0.05, 0.1],
    'max_depth': [3, 4, 5],
    'subsample': [0.8, 0.9, 1.0]
}

# 创建基础模型
gbdt = GradientBoostingRegressor(random_state=42)

# 创建网格搜索对象,5折交叉验证,以负均方误差为评分标准
grid_search = GridSearchCV(estimator=gbdt,
                           param_grid=param_grid,
                           cv=5,
                           scoring='neg_mean_squared_error', # 负MSE,sklearn约定
                           verbose=1, # 输出进度
                           n_jobs=-1) # 使用所有CPU核心

# 在训练集上执行网格搜索
grid_search.fit(X_train, y_train)

# 输出最佳参数和最佳分数
print(f"最佳参数组合: {grid_search.best_params_}")
print(f"最佳交叉验证分数 (负MSE): {grid_search.best_score_:.4f}")
print(f"对应的RMSE: {np.sqrt(-grid_search.best_score_):.4f}")

# 用最佳参数模型在测试集上评估
best_gbdt = grid_search.best_estimator_
y_pred_best = best_gbdt.predict(X_test)
best_r2 = r2_score(y_test, y_pred_best)
best_rmse = np.sqrt(mean_squared_error(y_test, y_pred_best))
print(f"\n【调优后GBDT在测试集上】")
print(f"R²分数: {best_r2:.4f}")
print(f"RMSE: {best_rmse:.4f}")

经过一番调优,模型的R²分数很可能突破0.9,RMSE降到3.0甚至更低。这意味着我们的模型已经能解释超过90%的房价波动,平均预测误差在3万美元以内。对于这个经典数据集来说,这已经是非常出色的成绩了。

5.3 模型对比与学习曲线

我们把所有尝试过的模型放在一起做个对比,这样谁好谁坏就一目了然了。

# 收集所有模型结果
all_results = {
    '线性回归': {'R2': lr_r2, 'RMSE': lr_rmse},
    '三特征线性回归': {'R2': lr_r2_sel, 'RMSE': lr_rmse_sel},
    '岭回归': {'R2': results.get('岭回归 (alpha=1.0)', {}).get('R2', 0), 'RMSE': results.get('岭回归 (alpha=1.0)', {}).get('RMSE', 0)},
    'SVR-RBF': {'R2': svr_results.get('SVR-径向基核(RBF)', {}).get('R2', 0), 'RMSE': svr_results.get('SVR-径向基核(RBF)', {}).get('RMSE', 0)},
    '随机森林': {'R2': r2_score(y_test, RandomForestRegressor(n_estimators=100, random_state=42).fit(X_train, y_train).predict(X_test)), 'RMSE': np.sqrt(mean_squared_error(y_test, RandomForestRegressor(n_estimators=100, random_state=42).fit(X_train, y_train).predict(X_test)))},
    'GBDT默认': {'R2': gbdt_r2, 'RMSE': gbdt_rmse},
    'GBDT调优': {'R2': best_r2, 'RMSE': best_rmse}
}

results_df = pd.DataFrame(all_results).T.sort_values('R2', ascending=False)
print(results_df)

# 可视化对比
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
results_df['R2'].plot(kind='bar', ax=axes[0], color='skyblue')
axes[0].set_title('不同模型 R² 分数对比')
axes[0].set_ylabel('R²')
axes[0].tick_params(axis='x', rotation=45)

results_df['RMSE'].plot(kind='bar', ax=axes[1], color='lightcoral')
axes[1].set_title('不同模型 RMSE 对比')
axes[1].set_ylabel('RMSE')
axes[1].tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.show()

从对比图中,你可以清晰地看到一条模型性能提升的路径:从简单的线性模型(R² ~0.7),到正则化线性模型和SVR(R² ~0.8),再到树模型集成方法(R² >0.9)。GBDT(尤其是调优后)稳稳地位居榜首。

最后,我们还可以画一下GBDT的学习曲线,看看增加训练数据量或者增加树的数量,模型性能是如何变化的。这能帮助我们判断模型是欠拟合还是过拟合,以及增加数据是否有用。

from sklearn.model_selection import learning_curve

def plot_learning_curve(estimator, title, X, y, cv=5):
    """绘制学习曲线"""
    train_sizes, train_scores, test_scores = learning_curve(
        estimator, X, y, cv=cv, scoring='neg_mean_squared_error',
        train_sizes=np.linspace(0.1, 1.0, 10), n_jobs=-1)

    train_scores_mean = np.sqrt(-train_scores.mean(axis=1))
    train_scores_std = np.sqrt(train_scores.std(axis=1))
    test_scores_mean = np.sqrt(-test_scores.mean(axis=1))
    test_scores_std = np.sqrt(test_scores.std(axis=1))

    plt.figure(figsize=(10, 6))
    plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
                     train_scores_mean + train_scores_std, alpha=0.1, color="r")
    plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
                     test_scores_mean + test_scores_std, alpha=0.1, color="g")
    plt.plot(train_sizes, train_scores_mean, 'o-', color="r", label="训练集RMSE")
    plt.plot(train_sizes, test_scores_mean, 'o-', color="g", label="交叉验证RMSE")
    plt.xlabel("训练样本数")
    plt.ylabel("RMSE")
    plt.title(title)
    plt.legend(loc="best")
    plt.grid(True)
    plt.show()

# 绘制GBDT的学习曲线
plot_learning_curve(best_gbdt, "GBDT学习曲线", X_train, y_train)

如果学习曲线中训练误差和验证误差随着数据量增加逐渐接近且都处于较低水平,说明模型拟合得很好。如果两条曲线差距很大,说明可能过拟合了。通过这个实战,我希望你感受到的不仅仅是几个API的调用,而是一个完整的、迭代的机器学习问题解决思路:从简单的基线模型开始,理解数据,进行必要的特征工程,然后尝试更复杂的模型,并通过交叉验证和网格搜索进行系统性的调优,最后客观地评估和对比模型性能。波士顿房价预测这个项目虽小,但贯穿了机器学习工作流的核心环节。下次当你面对一个新的预测问题时,不妨也沿着这条“从线性回归到GBDT”的优化路径走一遍,相信你会有更深的体会。

更多推荐