在模型中加入更多特征,乍一看似乎是提升性能的明显途径。如果一个模型能从更多信息中学习,它理应做出更好的预测。然而在实践中,这种直觉往往会引入隐藏的结构性风险。每一个额外的特征都会在上游数据管道、外部系统和数据质量检查上增加一个新的依赖点。一个缺失的字段、一次模式更改或一个延迟的数据集,都可能悄然降低模型在生产环境中的预测性能。
更深层次的问题不在于计算成本或系统复杂性,而在于权重的不稳定性。在回归模型中,尤其是当特征相互关联或信息量较弱时,优化器难以以一种有意义的方式分配权重。当模型试图在重叠的信号间分配影响时,系数可能会发生不可预测的偏移,而低信号的变量可能仅仅因为数据中的噪声而显得重要。随着时间的推移,这会导致模型在纸面上看起来很复杂,但在部署后却表现得不稳定。
在本文中,我们将研究为什么添加更多特征会使回归模型更不可靠,而不是更准确。我们将探讨相关特征如何扭曲系数估计,弱信号如何被误认为是真实模式,以及为什么每个额外特征都会增加生产环境的脆弱性。为了让这些概念更具体,我们将使用一个房产价格数据集,通过示例来比较“大而全”模型与更精简、更稳定的替代方案的行为。
导入依赖项
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import seaborn as sns
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings("ignore")
plt.rcParams.update({
"figure.facecolor": "#FAFAFA",
"axes.facecolor": "#FAFAFA",
"axes.spines.top": False,
"axes.spines.right":False,
"axes.grid": True,
"grid.color": "#E5E5E5",
"grid.linewidth": 0.8,
"font.family": "monospace",
})
SEED = 42
np.random.seed(SEED)
这段代码通过调整背景颜色、网格外观和移除不必要的坐标轴脊线,设置了一个清晰、一致的 Matplotlib 样式,以便于进行更清晰的可视化。同时,它设置了一个固定的 NumPy 随机种子(42),以确保任何随机生成的数据在多次运行中都是可重复的。
合成房产数据集
N = 800 # 训练样本数
# ── 真实信号特征 ────────────────────────────────────
sqft = np.random.normal(1800, 400, N) # 强信号
bedrooms = np.round(sqft / 550 + np.random.normal(0, 0.4, N)).clip(1, 6)
neighborhood = np.random.choice([0, 1, 2], N, p=[0.3, 0.5, 0.2]) # 分类变量
# ── 派生/相关特征 (多重共线性) ───────────────────────
total_rooms = bedrooms + np.random.normal(2, 0.3, N) # ≈ bedrooms
floor_area_m2 = sqft * 0.0929 + np.random.normal(0, 1, N) # ≈ sqft (平方米)
lot_sqft = sqft * 1.4 + np.random.normal(0, 50, N) # ≈ sqft (缩放后)
# ── 弱/虚假特征 ───────────────────────────────────
door_color_code = np.random.randint(0, 10, N).astype(float)
bus_stop_age_yrs = np.random.normal(15, 5, N)
nearest_mcdonalds_m = np.random.normal(800, 200, N)
# ── 纯噪声特征 (模拟90个随机列) ──────────────────────
noise_features = np.random.randn(N, 90)
noise_df = pd.DataFrame(
noise_features,
columns=[f"noise_{i:03d}" for i in range(90)]
)
# ── 目标变量:房价 ─────────────────────────────────────
price = (
120 * sqft
+ 8_000 * bedrooms
+ 30_000 * neighborhood
- 15 * bus_stop_age_yrs # 微小的真实影响
+ np.random.normal(0, 15_000, N) # 不可约减的噪声
)
# ── 组装 DataFrames ──────────────────────────────────────
signal_cols = ["sqft", "bedrooms", "neighborhood",
"total_rooms", "floor_area_m2", "lot_sqft",
"door_color_code", "bus_stop_age_yrs",
"nearest_mcdonalds_m"]
df_base = pd.DataFrame({
"sqft": sqft,
"bedrooms": bedrooms,
"neighborhood": neighborhood,
"total_rooms": total_rooms,
"floor_area_m2": floor_area_m2,
"lot_sqft": lot_sqft,
"door_color_code": door_color_code,
"bus_stop_age_yrs": bus_stop_age_yrs,
"nearest_mcdonalds_m": nearest_mcdonalds_m,
"price": price,
})
df_full = pd.concat([df_base.drop("price", axis=1), noise_df,
df_base[["price"]]], axis=1)
LEAN_FEATURES = ["sqft", "bedrooms", "neighborhood"]
NOISY_FEATURES = [c for c in df_full.columns if c != "price"]
print(f"精简模型特征数量 : {len(LEAN_FEATURES)}")
print(f"含噪模型特征数量: {len(NOISY_FEATURES)}")
print(f"数据集形状 : {df_full.shape}")
这段代码构建了一个旨在模仿真实世界房产定价场景的合成数据集,其中只有少数变量真正影响目标变量,而许多其他变量则引入冗余或噪声。数据集包含800个训练样本。核心信号特征,如房屋面积(sqft)、卧室数量(bedrooms)和社区类别(neighborhood),代表了房价的主要驱动因素。除此之外,还特意创建了几个与核心变量高度相关的派生特征,例如 floor_area_m2(房屋面积的单位转换)、lot_sqft 和 total_rooms。这些变量模拟了多重共线性,这是真实数据集中常见的问题,即多个特征携带重叠信息。
该数据集还包括弱特征或虚假特征——如 door_color_code、bus_stop_age_yrs 和 nearest_mcdonalds_m——它们与房产价格几乎没有或有很少的有意义关联。为了进一步复现“大而全模型”的问题,脚本生成了90个完全随机的噪声特征,代表了大型数据集中经常出现的无关列。目标变量 price 使用一个已知公式构建,其中房屋面积、卧室数量和社区的影响最强,而公交车站的年龄影响非常小,随机噪声则引入了自然变异性。
最后,定义了两个特征集:一个精简模型,仅包含三个真实信号特征(sqft, bedrooms, neighborhood);一个含噪模型,包含除目标变量外的所有可用列。这种设置使我们能够直接比较一个最小的、高信号特征集与一个充满冗余和无关变量的、特征繁多的大型模型的性能表现。
通过多重共线性导致的权重稀释
print("\n── 相关特征对之间的相关性 ──")
corr_pairs = [
("sqft", "floor_area_m2"),
("sqft", "lot_sqft"),
("bedrooms", "total_rooms"),
]
for a, b in corr_pairs:
r = np.corrcoef(df_full[a], df_full[b])[0, 1]
print(f" {a:20s} ↔ {b:20s} r = {r:.3f}")
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
fig.suptitle("权重稀释:相关特征对",
fontsize=13, fontweight="bold", y=1.02)
for ax, (a, b) in zip(axes, corr_pairs):
ax.scatter(df_full[a], df_full[b],
alpha=0.25, s=12, color="#3B6FD4")
r = np.corrcoef(df_full[a], df_full[b])[0, 1]
ax.set_title(f"r = {r:.3f}", fontsize=11)
ax.set_xlabel(a); ax.set_ylabel(b)
plt.tight_layout()
plt.savefig("01_multicollinearity.png", dpi=150, bbox_inches="tight")
plt.show()
print("已保存 → 01_multicollinearity.png")
这一部分展示了多重共线性,即多个特征包含几乎相同信息的情况。代码计算了三对特意构造的相关特征对的相关系数:sqft vs floor_area_m2,sqft vs lot_sqft,以及 bedrooms vs total_rooms。
如打印结果所示,这些关系非常强(r ≈ 1.0, 0.996, 和 0.945),意味着模型接收了多个描述相同底层房产特征的信号。 散点图可视化了这种重叠。因为这些特征几乎完全同步变化,回归优化器难以确定哪个特征应该在预测目标变量时获得权重。模型往往不是将明确的权重分配给一个变量,而是以任意的方式将影响分散到相关特征上,导致系数不稳定且被稀释。这就是为什么添加冗余特征会使模型可解释性变差、稳定性降低的关键原因之一,即使预测性能最初看起来相似。
跨重训练周期的权重不稳定性
N_CYCLES = 30
SAMPLE_SZ = 300 # 每次重新训练的数据切片大小
scaler_lean = StandardScaler()
scaler_noisy = StandardScaler()
# 在全部数据上拟合标准化器,以便单位可比
X_lean_all = scaler_lean.fit_transform(df_full[LEAN_FEATURES])
X_noisy_all = scaler_noisy.fit_transform(df_full[NOISY_FEATURES])
y_all = df_full["price"].values
lean_weights = [] # 形状: (N_CYCLES, 3)
noisy_weights = [] # 形状: (N_CYCLES, 3) -- 仅取前3列以便比较
for cycle in range(N_CYCLES):
idx = np.random.choice(N, SAMPLE_SZ, replace=False)
X_l = X_lean_all[idx]; y_c = y_all[idx]
X_n = X_noisy_all[idx]
m_lean = Ridge(alpha=1.0).fit(X_l, y_c)
m_noisy = Ridge(alpha=1.0).fit(X_n, y_c)
lean_weights.append(m_lean.coef_)
noisy_weights.append(m_noisy.coef_[:3]) # sqft, bedrooms, neighborhood
lean_weights = np.array(lean_weights)
noisy_weights = np.array(noisy_weights)
print("\n── 30次重训练周期的系数标准差 ──")
print(f"{'特征':<18} {'精简模型 σ':>10} {'含噪模型 σ':>10} {'放大倍数':>14}")
for i, feat in enumerate(LEAN_FEATURES):
sl = lean_weights[:, i].std()
sn = noisy_weights[:, i].std()
print(f" {feat:<16} {sl:>10.1f} {sn:>10.1f} ×{sn/sl:.1f}")
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
fig.suptitle("权重不稳定性:精简模型 vs. 含噪模型 (30次重训练)",
fontsize=13, fontweight="bold", y=1.02)
colors = {"lean": "#2DAA6E", "noisy": "#E05C3A"}
for i, feat in enumerate(LEAN_FEATURES):
ax = axes[i]
ax.plot(lean_weights[:, i], color=colors["lean"],
linewidth=2, label="精简模型 (3个特征)", alpha=0.9)
ax.plot(noisy_weights[:, i], color=colors["noisy"],
linewidth=2, label="含噪模型 (100+个特征)", alpha=0.9, linestyle="--")
ax.set_title(f'系数: "{feat}"', fontsize=11)
ax.set_xlabel("重训练周期")
ax.set_ylabel("标准化后的权重")
if i == 0:
ax.legend(fontsize=9)
plt.tight_layout()
plt.savefig("02_weight_instability.png", dpi=150, bbox_inches="tight")
plt.show()
print("已保存 → 02_weight_instability.png")
本实验模拟了真实生产系统中模型定期在新数据上重新训练时发生的情况。在30个重训练周期中,代码随机抽取数据集的子集,并拟合两个模型:一个使用三个核心信号特征的精简模型,以及一个使用包含相关变量和随机变量的完整特征集的含噪模型。通过跟踪每个重训练周期中关键特征的系数,我们可以观察到学习到的权重随时间的稳定性。
结果显示出一个清晰的模式:含噪模型表现出显著更高的系数变异性。
例如,sqft 系数的标准差增加了2.6倍,而 bedrooms 与精简模型相比变得不稳定了2.2倍。绘制的线条使这种影响一目了然——精简模型的系数在重训练周期中保持相对平滑和一致,而含噪模型的权重波动则大得多。这种不稳定性是因为相关和不相关的特征迫使优化器以不可预测的方式重新分配权重,使得模型的行为更不可靠,即使整体精度看起来相似。
信噪比 (SNR) 的下降
correlations = df_full[NOISY_FEATURES + ["price"]].corr()["price"].drop("price")
correlations = correlations.abs().sort_values(ascending=False)
fig, ax = plt.subplots(figsize=(14, 5))
bar_colors = [
"#2DAA6E" if f in LEAN_FEATURES
else "#E8A838" if f in ["total_rooms", "floor_area_m2", "lot_sqft",
"bus_stop_age_yrs"]
else "#CCCCCC"
for f in correlations.index
]
ax.bar(range(len(correlations)), correlations.values,
color=bar_colors, width=0.85, edgecolor="none")
# 图例块
from matplotlib.patches import Patch
legend_elements = [
Patch(facecolor="#2DAA6E", label="高信号 (精简集)"),
Patch(facecolor="#E8A838", label="相关/低信号"),
Patch(facecolor="#CCCCCC", label="纯噪声"),
]
ax.legend(handles=legend_elements, fontsize=10, loc="upper right")
ax.set_title("信噪比:每个特征与价格的 |相关系数|",
fontsize=13, fontweight="bold")
ax.set_xlabel("特征排名 (按 |r| 排序)")
ax.set_ylabel("与价格的 |皮尔逊 r|")
ax.set_xticks([])
plt.tight_layout()
plt.savefig("03_snr_degradation.png", dpi=150, bbox_inches="tight")
plt.show()
print("已保存 → 03_snr_degradation.png")
这一部分通过计算每个特征与目标变量(price)的绝对相关系数来衡量其特征的信号强度。条形图按相关性对所有特征进行排序,用绿色突出显示真正的高信号特征,橙色表示相关或弱特征,灰色表示大量的纯噪声特征。
可视化结果表明,只有少数变量携带有意义的预测信号,而大多数变量贡献很小或根本没有贡献。当许多低信号或噪声特征被包含在模型中时,它们会稀释整体的信噪比,使得优化器更难一致地识别出真正重要的特征。
特征漂移模拟
def predict_with_drift(model, scaler, X_base, drift_col_idx,
drift_magnitude, feature_cols):
"""向一个特征列注入漂移并测量预测偏移"""
X_drifted = X_base.copy()
X_drifted[:, drift_col_idx] += drift_magnitude
return model.predict(scaler.transform(X_drifted))
# 在整个数据集上重新拟合两个模型
sc_lean = StandardScaler().fit(df_full[LEAN_FEATURES])
sc_noisy = StandardScaler().fit(df_full[NOISY_FEATURES])
m_lean_full = Ridge(alpha=1.0).fit(
sc_lean.transform(df_full[LEAN_FEATURES]), y_all)
m_noisy_full = Ridge(alpha=1.0).fit(
sc_noisy.transform(df_full[NOISY_FEATURES]), y_all)
X_lean_raw = df_full[LEAN_FEATURES].values
X_noisy_raw = df_full[NOISY_FEATURES].values
base_lean = m_lean_full.predict(sc_lean.transform(X_lean_raw))
base_noisy = m_noisy_full.predict(sc_noisy.transform(X_noisy_raw))
# 漂移 "bus_stop_age_yrs" 特征 (低信号,但在含噪模型中存在)
drift_col_noisy = NOISY_FEATURES.index("bus_stop_age_yrs")
drift_range = np.linspace(0, 20, 40) # 公交车站年龄最多漂移20年
rmse_lean_drift, rmse_noisy_drift = [], []
for d in drift_range:
preds_noisy = predict_with_drift(
m_noisy_full, sc_noisy, X_noisy_raw,
drift_col_noisy, d, NOISY_FEATURES)
# 精简模型甚至没有这个特征 → 不受影响
rmse_lean_drift.append(
np.sqrt(mean_squared_error(base_lean, base_lean))) # 设计上为0
rmse_noisy_drift.append(
np.sqrt(mean_squared_error(base_noisy, preds_noisy)))
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(drift_range, rmse_lean_drift, color="#2DAA6E",
linewidth=2.5, label="精简模型 (特征不存在)")
ax.plot(drift_range, rmse_noisy_drift, color="#E05C3A",
linewidth=2.5, linestyle="--",
label='含噪模型 ("bus_stop_age_yrs" 发生漂移)')
ax.fill_between(drift_range, rmse_noisy_drift,
alpha=0.15, color="#E05C3A")
ax.set_xlabel("特征漂移幅度 (年)", fontsize=11)
ax.set_ylabel("预测偏移 RMSE ($)", fontsize=11)
ax.set_title("特征漂移敏感性:\n每个额外特征 = 额外的故障点",
fontsize=13, fontweight="bold")
ax.legend(fontsize=10)
plt.tight_layout()
plt.savefig("05_drift_sensitivity.png", dpi=150, bbox_inches="tight")
plt.show()
print("已保存 → 05_drift_sensitivity.png")
本实验说明了特征漂移如何在生产环境中悄然影响模型预测。代码向一个弱特征(bus_stop_age_yrs)逐渐引入漂移,并测量模型预测的变化程度。由于精简模型不包含此特征,其预测保持完全稳定,而随着漂移幅度的增长,含噪模型变得愈发敏感。
结果图显示,随着特征漂移,预测误差稳步增加,这突显了一个重要的生产现实:每一个额外的特征都变成了另一个潜在的故障点。即使是低信号变量,如果其数据分布发生变化或上游数据管道发生更改,也可能引入不稳定性。FINISHED