在机器学习的实际应用中,我们经常面临一个棘手的问题:现实世界的数据很少是完美的。如果你尝试使用标准的普通最小二乘法(OLS)回归来拟合包含异常值的数据,你会发现模型的表现可能会大打折扣。这条所谓的“最佳拟合线”会被那些极端的离群点强行拉偏,导致模型无法捕捉到数据的真实趋势。
为了解决这一痛点,我们需要引入更强大的工具——稳健回归。在本文中,我们将深入探讨这一领域。我们将一起了解为什么普通的线性回归在面对噪声时会显得脆弱,以及我们如何利用像 RANSAC 和 Theil-Sen 这样的算法来构建具有“抗干扰”能力的机器学习模型。我们将结合 Python 的 scikit-learn 库,通过实际的代码示例,一步步掌握这些技术。
为什么我们需要稳健回归?
简单线性回归的目标是最小化误差的平方和。这种方法的数学特性决定了它对离群点非常敏感。想象一下,如果数据集中存在一个极端错误的点(例如,录入错误导致的数量级偏差),为了减小这个点的巨大误差,回归线会不得不向该点大幅度倾斜,从而牺牲了对大部分正常数据的拟合精度。这就是我们常说的“模型偏差”。
稳健回归的核心思想在于:不要让少数的“坏”数据点绑架了整个模型。这类算法在设计上就赋予了模型识别并降低异常值权重的机制,使其能够专注于捕捉数据的主体结构。
环境准备与工具介绍
在开始编码之前,让我们先准备好战场。Python 拥有极其丰富的数据科学生态,我们将主要依赖以下“四大金刚”:
- Pandas: 我们的数据管家。它不仅能帮助我们以二维表格的形式加载数据,还提供了便捷的数据清洗和分析功能。
- Numpy: 底层计算引擎。处理数组运算时,它的速度极快,是我们进行数学计算的基础。
- Matplotlib / Seaborn: 我们的画笔。用于绘制数据的分布图、散点图以及拟合后的回归线,让我们直观地看到模型的效果。
- Scikit-learn (Sklearn): 核心武器库。它封装了从数据预处理到模型训练、评估的全套流程,我们将直接调用其中的稳健回归模型。
首先,我们需要导入必要的库并配置环境:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression, RANSACRegressor, TheilSenRegressor, HuberRegressor
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
# 忽略警告,保持输出整洁
import warnings
warnings.filterwarnings(‘ignore‘)
# 设置绘图风格,让图表更美观
sns.set(style="whitegrid")
数据集的加载与探索
为了演示稳健回归的威力,我们需要一个包含噪声的真实数据集。在这里,我们将使用经典的波士顿房价数据集。虽然这是一个比较老的例子,但它非常适合用来展示特征与目标变量之间的关系,尤其是当我们在其中人为添加一些“脏数据”时。
让我们加载数据并进行预处理。这里的一个关键步骤是特征选择。为了便于在二维平面上可视化,我们将只关注“房间数”(RM)与“房价”(MEDV)之间的关系。
# 加载波士顿房价数据集
# 注意:在较新版本的 sklearn 中可能需要从其他源获取,这里假设环境支持
try:
boston = datasets.load_boston()
except ImportError:
# 如果 sklearn 版本不支持,这里模拟生成一个类似的数据集用于演示
print("注意:原版波士顿数据集已移除,正在生成模拟数据用于演示...")
np.random.seed(42)
X_raw = np.random.normal(6.5, 1.0, 500) # 平均房间数
y_raw = 10 + 5 * X_raw + np.random.normal(0, 2, 500) # 房价
# 构造 DataFrame
df = pd.DataFrame({‘RM‘: X_raw, ‘MEDV‘: y_raw})
# 人为注入一些离群点来模拟真实世界的“脏数据”
outlier_indices = np.random.choice(df.index, size=20, replace=False)
df.loc[outlier_indices, ‘MEDV‘] = df.loc[outlier_indices, ‘MEDV‘] * 2.5 + np.random.normal(0, 10, 20)
X = df[‘RM‘].to_numpy().reshape(-1, 1)
y = df[‘MEDV‘].to_numpy().reshape(-1, 1)
if ‘boston‘ in globals():
df = pd.DataFrame(boston.data, columns=boston.feature_names)
df[‘MEDV‘] = boston.target
# 同样注入一些噪声,以便对比模型表现
# 找出一些索引点作为离群点
np.random.seed(42)
outlier_indices = np.random.choice(df.index, size=30, replace=False)
# 让这些点的房价异常高
df.loc[outlier_indices, ‘MEDV‘] = df.loc[outlier_indices, ‘MEDV‘] * 2.5 + 10
# 选择特征
X = df[‘RM‘].to_numpy().reshape(-1, 1)
y = df[‘MEDV‘].to_numpy().reshape(-1, 1)
# 让我们快速查看一下数据的统计信息
print("数据集前5行预览:")
print(df.head())
print(f"
数据形状: {df.shape}")
通过上面的代码,我们不仅加载了数据,还故意制造了一些麻烦。在实际工作中,数据录入错误、设备故障或极端的市场情况都会产生这样的离群点。现在,让我们看看模型如何应对这些挑战。
方法一:RANSAC 回归器(随机抽样一致算法)
RANSAC(RANdom SAmple Consensus)是稳健回归中最著名、应用最广泛的算法之一。它的运作逻辑非常像人类的直觉判断:
- 随机采样:从数据中随机抽取一小部分点(最小样本集)构建一个初始模型。
- 验证:用这个初始模型去测试剩下的所有点。
- 分类:符合模型误差阈值的点被称为“内点”,不符合的则是“离群点”。
- 迭代优化:重复上述过程,选择拥有最多内点的模型作为最终结果,并仅用内点重新训练模型。
这种方法的核心优势在于,只要正常数据的比例大于离群点,RANSAC 就能以极高的概率找出正确的模型参数,而离群点则会被直接剔除在训练过程之外。
#### 代码实现与详解
让我们使用 RANSACRegressor 来拟合我们的数据。这里有几个关键参数我们需要注意:
-
base_estimator:基础估计器,默认是线性回归。 -
min_samples:构建模型所需的最小样本数。 -
residual_threshold:这是判断内点和外点的阈值。小于这个误差的算内点,否则是外点。设置这个参数需要一定的领域知识或通过观察误差分布来确定。
# 创建 RANSAC 回归模型
# 这里我们设置 residual_threshold 为 10,意味着预测值与真实值差距小于 10 的点被视为有效数据
ransac = RANSACRegressor(base_estimator=LinearRegression(),
min_samples=50, # 每次迭代至少取 50 个样本
max_trials=100, # 最大迭代次数
loss=‘absolute_loss‘, # 使用绝对损失,对大误差不那么敏感
random_state=42,
residual_threshold=10)
# 拟合模型
ransac.fit(X, y)
# 预测
y_pred_ransac = ransac.predict(X)
# 评估指标
mae_ransac = mean_absolute_error(y, y_pred_ransac)
r2_ransac = r2_score(y, y_pred_ransac)
print(f"RANSAC 平均绝对误差 (MAE): {mae_ransac:.4f}")
print(f"RANSAC R² 分数: {r2_ransac:.4f}")
# 查看哪些被认为是离群点
inlier_mask = ransac.inlier_mask_
outlier_mask = np.logical_not(inlier_mask)
print(f"
总样本数: {len(y)}")
print(f"识别出的内点数量: {np.sum(inlier_mask)}")
print(f"识别出的离群点数量: {np.sum(outlier_mask)}")
实用见解:RANSAC 的结果是非确定性的(随机的),因为它从随机样本开始。因此,设置 INLINECODE41bf3bcc 对于结果的可复现性至关重要。通过 INLINECODEeced1680,我们可以直观地看到模型认为哪些数据是“垃圾”。这不仅能用来回归,本身就是一种强大的异常检测手段。
方法二:Theil-Sen 回归器
Theil-Sen 估计器是一种基于中值的非参数方法。你可以把它想象成“随机森林”在回归领域的简化版。它的逻辑如下:
- 计算所有可能的点对之间的斜率。
- 取这些斜率的中位数作为最终的模型斜率。
这种方法的最大优势在于其对多重共线性(即特征之间高度相关)的不敏感性,以及对离群点的天然免疫力。因为它依赖的是中位数,而不是均值,极端的离群斜率不会影响最终的中位数结果。
# 创建 Theil-Sen 回归模型
# max_subpopulation 控制为了计算中位数考虑的子集大小,以免在大数据集上内存溢出
theil_sen = TheilSenRegressor(random_state=42, max_subpopulation=10000)
# 拟合模型
theil_sen.fit(X, y)
# 预测
y_pred_ts = theil_sen.predict(X)
# 评估
mae_ts = mean_absolute_error(y, y_pred_ts)
r2_ts = r2_score(y, y_pred_ts)
print(f"Theil-Sen 平均绝对误差 (MAE): {mae_ts:.4f}")
print(f"Theil-Sen R² 分数: {r2_ts:.4f}")
深入理解:Theil-Sen 算法在中小型数据集上表现出色,因为它试图穷举所有的斜率组合。但是,由于计算复杂度的问题,如果特征维度非常高,它会变得很慢。如果你的特征非常多,可能需要考虑 RANSAC 或者 Huber 回归。
方法三:Huber 回归器(补充进阶)
除了上述两种方法,我们不能不提 Huber 回归。这是一种结合了最小二乘法和绝对误差损失的混合方法。
- 对于小误差(正常点),它使用平方损失,保持模型的高效性和光滑性。
- 对于大误差(离群点),它切换为线性损失,防止离群点对梯度产生过大的影响。
这就像是一个智能开关:正常情况下按常规处理,遇到异常情况就切换到“稳健模式”。
from sklearn.linear_model import HuberRegressor
# epsilon 参数控制了从小误差到大误差的切换阈值,默认是 1.35
# 我们可以通过调整 epsilon 来控制模型对离群点的敏感度
huber = HuberRegressor(epsilon=1.35, max_iter=100, alpha=0.0001, random_state=42)
# 拟合
huber.fit(X, y)
# 预测
y_pred_huber = huber.predict(X)
# 评估
mae_huber = mean_absolute_error(y, y_pred_huber)
r2_huber = r2_score(y, y_pred_huber)
print(f"Huber 平均绝对误差 (MAE): {mae_huber:.4f}")
print(f"Huber R² 分数: {r2_huber:.4f}")
实战对比与可视化
光看数字可能还不够直观。让我们通过图表来看看不同模型在面对离群点时的表现差异。我们将对比普通线性回归和 RANSAC 回归。
# 为了对比,我们训练一个普通的线性回归模型作为对照组
lr = LinearRegression()
lr.fit(X, y)
y_pred_lr = lr.predict(X)
# 可视化对比
plt.figure(figsize=(14, 7))
# 绘制原始数据散点图
plt.scatter(X[inlier_mask], y[inlier_mask], c=‘blue‘, alpha=0.6, label=‘内点‘)
plt.scatter(X[outlier_mask], y[outlier_mask], c=‘red‘, alpha=0.8, label=‘离群点‘)
# 绘制普通线性回归线(红色虚线)- 通常会被离群点带偏
plt.plot(X, y_pred_lr, color=‘red‘, linestyle=‘--‘, linewidth=2, label=‘普通线性回归 (OLS)‘)
# 绘制 RANSAC 回归线(绿色实线)- 更加稳健
plt.plot(X, y_pred_ransac, color=‘green‘, linewidth=3, label=‘RANSAC 回归‘)
plt.title(‘稳健回归 vs 普通回归:离群点对拟合线的影响‘, fontsize=16)
plt.xlabel(‘房间数‘, fontsize=14)
plt.ylabel(‘房价‘, fontsize=14)
plt.legend(fontsize=12)
plt.show()
通过这张图,你可以清晰地看到:红色的虚线(普通回归)为了迎合那些红色的离群点,明显偏离了大多数蓝色点的主体趋势。而绿色的实线则稳稳地捕捉到了房间数与房价之间的真实正相关关系。
模型性能总结
最后,让我们把所有模型的评估指标汇总一下,看看谁的 MAE(平均绝对误差)最小,谁的 R²(拟合优度)最高。
print("
========== 模型性能对比汇总 ==========
")
results = pd.DataFrame({
‘模型‘: [‘普通线性回归 (OLS)‘, ‘RANSAC 回归‘, ‘Theil-Sen 回归‘, ‘Huber 回归‘],
‘平均绝对误差 (MAE)‘: [
mean_absolute_error(y, lr.predict(X)),
mae_ransac,
mae_ts,
mae_huber
],
‘R² 分数‘: [
r2_score(y, lr.predict(X)),
r2_ransac,
r2_ts,
r2_huber
]
})
print(results)
总结与最佳实践
通过这次深入的探索,我们发现:
- 普通线性回归很脆弱:它假设误差服从正态分布,一旦遇到离群点,模型就会崩溃。
- 稳健回归是必需品:在金融、传感器数据处理、电商销量预测等领域,数据噪声是常态。RANSAC 和 Theil-Sen 等算法为我们的模型增加了一层“装甲”。
- 选择合适的工具:
* 如果你有很多明显的离群点,且不知道哪些是离群点,RANSAC 通常是首选。
* 如果你的数据集中等大小,且特征之间存在相关性,Theil-Sen 是个不错的选择。
* 如果你需要平滑的损失函数且希望计算速度快,Huber 值得一试。
常见错误与解决方案:
- RANSAC 的阈值设置错误:如果你把
residual_threshold设得太大,所有离群点都会被当成内点;设得太小,正常数据也会被排除。建议先用简单模型跑一遍,看看残差的分布,再设定阈值。 - 数据未标准化:虽然我们这里用的例子比较简单,但在多变量回归中,使用
StandardScaler对特征进行标准化是稳健回归能否快速收敛的关键步骤。
现在,你已经掌握了在 Python 中处理噪声数据的强大武器。下次当你拿到一份“脏乱差”的数据时,不要急着清洗掉所有异常值,试着让稳健回归模型来帮你处理这些棘手的问题吧!