5 minutes
回归分析
回归分析是机器学习中最基础也最强大的工具之一。它用来预测连续数值型目标变量,比如房价、温度、销售额。虽然概念简单,但回归分析包含了机器学习中许多核心思想,是理解更复杂模型的重要基石。
什么是回归分析?
回归分析研究的是自变量(特征)与因变量(目标)之间的定量关系。它的核心目标是找到一个函数,能够根据输入特征预测输出值。
简单线性回归
只有一个自变量的线性回归称为简单线性回归。它的数学形式是:
$$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}$ | 根均方误差,单位与目标变量一致 |
| R² | $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_}")
总结
回归分析是机器学习的基础。今天我们讨论了以下核心内容:
- 简单线性回归:一个特征预测一个目标
- 多元线性回归:多个特征共同预测
- 模型假设:线性、独立性、同方差性、正态性、无多重共线性
- 多项式回归:通过添加高次项处理非线性关系
- 正则化:Ridge(L2)、Lasso(L1)、ElasticNet,对抗过拟合
- 评估指标:MAE、MSE、RMSE、R²、MAPE
- 诊断图:检查模型假设是否被满足
- 特征选择:Lasso 自动选择重要特征
掌握了回归分析,你已经迈入了监督学习的大门。下一篇文章,我们将把目标变量从连续值换成类别标签,进入分类算法的世界。