回归分析是机器学习中最基础也最强大的工具之一。它用来预测连续数值型目标变量,比如房价、温度、销售额。虽然概念简单,但回归分析包含了机器学习中许多核心思想,是理解更复杂模型的重要基石。

什么是回归分析?

回归分析研究的是自变量(特征)与因变量(目标)之间的定量关系。它的核心目标是找到一个函数,能够根据输入特征预测输出值。

简单线性回归

只有一个自变量的线性回归称为简单线性回归。它的数学形式是:

$$y = \beta_0 + \beta_1 x + \varepsilon$$

其中 $\beta_0$ 是截距,$\beta_1$ 是斜率(系数),$\varepsilon$ 是误差项。

最小二乘法是最常用的参数估计方法。它的目标是最小化所有数据点的预测值与真实值之间的平方差之和:

$$\min \sum_{i=1}^{n} (y_i - \hat{y}_i)^2$$
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

# 生成模拟数据
np.random.seed(42)
X = np.random.rand(50, 1) * 10  # 0-10之间的随机数
y = 2.5 * X.squeeze() + 1 + np.random.randn(50) * 1.5  # y = 2.5x + 1 + 噪声

# 训练模型
model = LinearRegression()
model.fit(X, y)

print(f"斜率 (β₁): {model.coef_[0]:.3f}")
print(f"截距 (β₀): {model.intercept_:.3f}")
print(f"预测方程: y = {model.coef_[0]:.3f}x + {model.intercept_:.3f}")

# 可视化
plt.figure(figsize=(10, 6))
plt.scatter(X, y, alpha=0.7, label='数据点')
plt.plot(X, model.predict(X), 'r-', linewidth=2, label='回归线')
plt.xlabel('X')
plt.ylabel('y')
plt.title('简单线性回归')
plt.legend()
plt.grid(alpha=0.3)
plt.show()

多元线性回归

当自变量有多个时,就变成了多元线性回归:

$$y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + ... + \beta_p x_p + \varepsilon$$
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
import pandas as pd
import numpy as np

# 加载加州住房数据集
housing = fetch_california_housing()
X = pd.DataFrame(housing.data, columns=housing.feature_names)
y = housing.target

print(f"数据集维度: {X.shape}")
print(f"特征: {list(X.columns)}")
print(X.head())

# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 训练
lr = LinearRegression()
lr.fit(X_train_scaled, y_train)

# 预测
y_pred = lr.predict(X_test_scaled)

# 评估
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)

print(f"\n模型表现:")
print(f"MSE: {mse:.4f}")
print(f"RMSE: {rmse:.4f}")
print(f"R²: {r2:.4f}")

# 系数分析
coef_df = pd.DataFrame({
    'feature': housing.feature_names,
    'coefficient': lr.coef_
}).sort_values('coefficient', key=abs, ascending=False)
print(f"\n特征系数:")
print(coef_df)

线性回归的基本假设

使用线性回归前,需要检查数据是否满足以下假设。如果假设被严重违反,模型的可靠性就会下降。

1. 线性关系

自变量和因变量之间的关系必须是线性的。可以通过散点图或残差图检查。

2. 误差独立

观测值之间相互独立。时间序列数据中这个问题尤其常见——今天的股价和昨天的股价不独立。

3. 同方差性

残差的方差在不同预测值水平上保持一致。如果方差不恒定,称为异方差性

4. 误差正态分布

残差近似服从正态分布。这是进行统计推断(如置信区间、p值)的前提。

5. 无多重共线性

自变量之间不应高度相关。多重共线性会导致系数估计不稳定、难以解释。

# 检查多重共线性 - 计算方差膨胀因子 (VIF)
from statsmodels.stats.outliers_influence import variance_inflation_factor
import statsmodels.api as sm

# 添加常数项
X_with_const = sm.add_constant(X)

vif_data = pd.DataFrame()
vif_data['feature'] = X_with_const.columns
vif_data['VIF'] = [variance_inflation_factor(X_with_const.values, i) 
                   for i in range(X_with_const.shape[1])]
print(vif_data)
# VIF > 10 表示严重的多重共线性

模型解释

系数的含义

在多元线性回归中,每个系数 $\beta_j$ 表示在其他特征不变的情况下,该特征每变化一个单位,目标变量平均变化 $\beta_j$ 个单位。

print("系数解释:")
for name, coef in zip(housing.feature_names, lr.coef_):
    print(f"  {name}: {coef:.4f}")
    print(f"    在其他特征不变时, {name} 增加1个标准差,")
    print(f"    房价中位数平均变化 ${coef * 10000:.0f}")

R-squared (决定系数)

R² 衡量模型解释了目标变量多少比例的方差。取值 0 到 1,越大表示模型拟合越好。

$$R^2 = 1 - \frac{SS_{res}}{SS_{tot}}$$

其中 $SS_{res}$ 是残差平方和,$SS_{tot}$ 是总平方和。

Adjusted R-squared

R² 有一个问题:增加特征总会让 R² 变大或不变,即使加入的是噪音特征。调整后的 R² 会惩罚过多的特征:

$$R^2_{adj} = 1 - \frac{(1-R^2)(n-1)}{n-p-1}$$

其中 n 是样本量,p 是特征数量。

def adjusted_r2(r2, n, p):
    return 1 - (1 - r2) * (n - 1) / (n - p - 1)

n = X_test.shape[0]
p = X_test.shape[1]
print(f"R²: {r2:.4f}")
print(f"Adjusted R²: {adjusted_r2(r2, n, p):.4f}")

多项式回归

当数据表现出非线性关系时,可以通过添加特征的幂次项来扩展线性回归:

from sklearn.preprocessing import PolynomialFeatures

# 生成非线性数据
np.random.seed(42)
X_nonlin = np.random.rand(100, 1) * 6 - 3
y_nonlin = 0.5 * X_nonlin.squeeze()**2 + X_nonlin.squeeze() + 2 + np.random.randn(100) * 0.5

# 创建多项式特征
poly = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly.fit_transform(X_nonlin)

# 训练
poly_model = LinearRegression()
poly_model.fit(X_poly, y_nonlin)

# 可视化对比
X_plot = np.linspace(-3, 3, 200).reshape(-1, 1)
X_plot_poly = poly.transform(X_plot)

plt.figure(figsize=(10, 6))
plt.scatter(X_nonlin, y_nonlin, alpha=0.6, label='数据')
plt.plot(X_plot, poly_model.predict(X_plot_poly), 'r-', label='多项式回归 (degree=2)')
plt.xlabel('X')
plt.ylabel('y')
plt.title('多项式回归')
plt.legend()
plt.grid(alpha=0.3)
plt.show()

degree 参数控制多项式最高次数。太低的 degree 会欠拟合,太高的 degree 会导致过拟合。

正则化:对抗过拟合

当特征很多或存在多重共线性时,普通线性回归的系数会变得很大且不稳定。正则化通过在损失函数中加入惩罚项来约束系数的大小。

Ridge 回归(L2 正则化)

Ridge 回归在损失函数中加入系数平方和的惩罚:

$$\min \sum (y_i - \hat{y}_i)^2 + \alpha \sum \beta_j^2$$

$\alpha$ 控制惩罚力度。$\alpha=0$ 等价于普通线性回归;$\alpha$ 越大,系数越趋近于 0。

from sklearn.linear_model import Ridge, Lasso, ElasticNet

# Ridge 回归
ridge = Ridge(alpha=1.0)
ridge.fit(X_train_scaled, y_train)
ridge_pred = ridge.predict(X_test_scaled)
ridge_r2 = r2_score(y_test, ridge_pred)

print(f"Ridge R²: {ridge_r2:.4f}")
print(f"Ridge 系数范围: {ridge.coef_.min():.3f} ~ {ridge.coef_.max():.3f}")

Lasso 回归(L1 正则化)

Lasso 回归加入系数绝对值之和的惩罚:

$$\min \sum (y_i - \hat{y}_i)^2 + \alpha \sum |\beta_j|$$

Lasso 的一个重要特性是它可以将不重要的特征系数压缩为 0,从而实现自动特征选择。

# Lasso 回归
lasso = Lasso(alpha=0.01)
lasso.fit(X_train_scaled, y_train)
lasso_pred = lasso.predict(X_test_scaled)
lasso_r2 = r2_score(y_test, lasso_pred)

print(f"Lasso R²: {lasso_r2:.4f}")
print(f"Lasso 非零系数数量: {np.sum(lasso.coef_ != 0)}/{len(lasso.coef_)}")

# 查看哪些特征被选中了
selected_features = [housing.feature_names[i] 
                     for i in range(len(lasso.coef_)) 
                     if lasso.coef_[i] != 0]
print(f"选中的特征: {selected_features}")

ElasticNet

ElasticNet 结合了 L1 和 L2 正则化,参数 l1_ratio 控制两者的比例:

elastic = ElasticNet(alpha=0.01, l1_ratio=0.5)
elastic.fit(X_train_scaled, y_train)
elastic_pred = elastic.predict(X_test_scaled)
elastic_r2 = r2_score(y_test, elastic_pred)

print(f"ElasticNet R²: {elastic_r2:.4f}")

正则化对比

# 不同 alpha 值对 Ridge 系数的影响
alphas = [0.001, 0.01, 0.1, 1, 10, 100]
coef_matrix = []

for alpha in alphas:
    ridge = Ridge(alpha=alpha)
    ridge.fit(X_train_scaled, y_train)
    coef_matrix.append(ridge.coef_)

coef_df = pd.DataFrame(coef_matrix, 
                       index=[f'alpha={a}' for a in alphas],
                       columns=housing.feature_names)
print("不同 alpha 下的 Ridge 系数:")
print(coef_df.round(3))
# alpha 越大,系数越趋向于 0

评估指标

from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np

# 计算所有常用回归指标
mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)

# MAPE (平均绝对百分比误差)
mape = np.mean(np.abs((y_test - y_pred) / y_test)) * 100

print(f"MAE : {mae:.4f}")
print(f"MSE : {mse:.4f}")
print(f"RMSE: {rmse:.4f}")
print(f"R²  : {r2:.4f}")
print(f"MAPE: {mape:.2f}%")
指标 公式 说明
MAE $\frac{1}{n}\sum\|y_i - \hat{y}_i\|$ 平均绝对误差,单位与目标变量一致
MSE $\frac{1}{n}\sum(y_i - \hat{y}_i)^2$ 平方误差,放大大误差的影响
RMSE $\sqrt{MSE}$ 根均方误差,单位与目标变量一致
$1 - SS_{res}/SS_{tot}$ 解释方差比例,0-1
MAPE $\frac{100}{n}\sum\|(y_i-\hat{y}_i)/y_i\|$ 平均百分比误差,百分比形式

诊断图

诊断图可以帮助我们检查模型假设是否被满足。

import matplotlib.pyplot as plt
import scipy.stats as stats

# 计算残差
residuals = y_test - y_pred

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 1. 残差 vs 拟合值图
axes[0, 0].scatter(y_pred, residuals, alpha=0.6)
axes[0, 0].axhline(y=0, color='r', linestyle='--')
axes[0, 0].set_xlabel('拟合值')
axes[0, 0].set_ylabel('残差')
axes[0, 0].set_title('残差 vs 拟合值')
# 理想情况:残差随机分布在零线附近,没有明显模式

# 2. Q-Q 图
stats.probplot(residuals, dist="norm", plot=axes[0, 1])
axes[0, 1].set_title('Q-Q 图')
# 理想情况:数据点沿着对角线分布

# 3. 残差直方图
axes[1, 0].hist(residuals, bins=30, edgecolor='black', alpha=0.7)
axes[1, 0].set_xlabel('残差')
axes[1, 0].set_ylabel('频数')
axes[1, 0].set_title('残差分布')

# 4. Scale-Location 图
standardized_residuals = residuals / np.std(residuals)
axes[1, 1].scatter(y_pred, np.sqrt(np.abs(standardized_residuals)), alpha=0.6)
axes[1, 1].set_xlabel('拟合值')
axes[1, 1].set_ylabel('sqrt(|标准化残差|)')
axes[1, 1].set_title('Scale-Location 图')
# 理想情况:点的分布水平均匀

plt.tight_layout()
plt.show()

如何解读这些图:

  • 残差 vs 拟合值:如果残差呈现喇叭形(随拟合值增大而发散),就是异方差性。
  • Q-Q 图:如果点偏离对角线,说明残差不服从正态分布。
  • 残差直方图:应为钟形(正态分布)。
  • Scale-Location 图:红色趋势线应大致水平。

特征选择与 Lasso

上文中提到 Lasso 可以将无用特征的系数压缩为 0。这使它成为一种优秀的特征选择工具。

from sklearn.linear_model import LassoCV

# 使用交叉验证自动选择 alpha
lasso_cv = LassoCV(cv=5, random_state=42)
lasso_cv.fit(X_train_scaled, y_train)

print(f"最优 alpha: {lasso_cv.alpha_:.4f}")
print(f"保留的特征数量: {np.sum(lasso_cv.coef_ != 0)}/{len(lasso_cv.coef_)}")

# 保留的特征
for i, coef in enumerate(lasso_cv.coef_):
    if coef != 0:
        print(f"  ✓ {housing.feature_names[i]}: {coef:.4f}")
    else:
        print(f"  ✗ {housing.feature_names[i]}: 已排除")

实战:完整回归分析流程

把今天学到的所有内容串起来,构建一个完整的回归分析流程:

from sklearn.pipeline import Pipeline
from sklearn.linear_model import RidgeCV
from sklearn.preprocessing import StandardScaler, PolynomialFeatures

# 构建带多项式特征和正则化的流水线
pipeline = Pipeline([
    ('poly', PolynomialFeatures(degree=2, include_bias=False, interaction_only=False)),
    ('scaler', StandardScaler()),
    ('model', RidgeCV(alphas=[0.01, 0.1, 1.0, 10.0], cv=5))
])

# 训练
pipeline.fit(X_train, y_train)

# 评估
y_pred_pipe = pipeline.predict(X_test)
print(f"Pipeline R²: {r2_score(y_test, y_pred_pipe):.4f}")
print(f"最优 alpha: {pipeline.named_steps['model'].alpha_}")

总结

回归分析是机器学习的基础。今天我们讨论了以下核心内容:

  1. 简单线性回归:一个特征预测一个目标
  2. 多元线性回归:多个特征共同预测
  3. 模型假设:线性、独立性、同方差性、正态性、无多重共线性
  4. 多项式回归:通过添加高次项处理非线性关系
  5. 正则化:Ridge(L2)、Lasso(L1)、ElasticNet,对抗过拟合
  6. 评估指标:MAE、MSE、RMSE、R²、MAPE
  7. 诊断图:检查模型假设是否被满足
  8. 特征选择:Lasso 自动选择重要特征

掌握了回归分析,你已经迈入了监督学习的大门。下一篇文章,我们将把目标变量从连续值换成类别标签,进入分类算法的世界。