写在前面的

这篇文章主要是用来有编程经验的人复习用的笔记,要求如下。

  • 知道线性回归是什么(机器学习要过一遍基础。
  • 有一定编程经验

※ 如果以上不达标,那么这篇文章肯定就不是写给你的。

1 线性回归

对于有编程经验的人,我个人倾向于通过代码来学习线性回归,代码里不懂的直接去问AI。而且要多问为什么?如果看不懂接下来是什么,那就要随便找一个

线性回归是什么呢

线性回归是一种基础的统计学和机器学习技术,用于通过一个或多个自变量(特征)预测连续的因变量(标签)数值。它建立一条最佳拟合直线(或超平面),使得实际数据点与预测值之间的差异(即损失)最小化。常用方法包括最小二乘法。

英文出自wiki

Linear regression is a statistical method used to model the relationship between a dependent variable ( y 𝑦 ) and one or more independent variables ( x 𝑥 ) by fitting a straight line, typically using the least-squares method. It predicts outcomes, such as sales or prices, by minimizing the vertical distances between data points and the line.

1-1 学习目标

标准 1

你能说清楚线性回归是干什么的。

标准 2

你能看懂这个式子

y^=w1x1+w2x2++b\hat{y} = w_1x_1 + w_2x_2 + \cdots + b

标准 3

你知道 fit 本质是在找让误差变小的参数。

标准 4

你知道最小二乘是让误差平方和最小

标准 5

你会用代码训练、预测、看系数、看评估指标。

标准 6

你知道正规方程和梯度下降都是求参数的方法,但你不一定要会完整证明。

2 完整代码

下面是通过学生的学习时长,出勤率和作业完成率,来判断考试分数的问题。

import warnings
warnings.filterwarnings("ignore")  # 忽略一些不影响学习的警告信息

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# sklearn 自带的糖尿病回归数据集
from sklearn.datasets import load_diabetes

# 数据集切分、交叉验证
from sklearn.model_selection import train_test_split, cross_val_score, KFold

# Pipeline: 把“预处理 + 模型”串起来
from sklearn.pipeline import Pipeline

# 标准化
from sklearn.preprocessing import StandardScaler

# 线性回归模型
from sklearn.linear_model import LinearRegression

# 回归评估指标
from sklearn.metrics import (
    mean_absolute_error,
    mean_squared_error,
    r2_score
)

# 固定随机种子,保证每次切分数据结果一致,方便复现
RANDOM_STATE = 42


def evaluate_regression(y_true, y_pred, dataset_name="dataset"):
    """
    这个函数专门用来评估回归模型效果
    输入:
        y_true: 真实值
        y_pred: 预测值
        dataset_name: 当前评估的数据集名字(如 Validation / Test)
    输出:
        返回一个字典,里面包含多个指标
    """

    # 平均绝对误差:平均每次大概错多少
    mae = mean_absolute_error(y_true, y_pred)

    # 均方误差:误差平方后再平均
    mse = mean_squared_error(y_true, y_pred)

    # RMSE:对 MSE 开根号,单位和原目标值一致,更直观
    rmse = np.sqrt(mse)

    # R²:拟合优度,越接近 1 越好
    r2 = r2_score(y_true, y_pred)

    print(f"===== {dataset_name} Evaluation =====")
    print(f"MAE : {mae:.4f}")
    print(f"MSE : {mse:.4f}")
    print(f"RMSE: {rmse:.4f}")
    print(f"R²  : {r2:.4f}")
    print()

    # 返回结果,后面可以继续使用
    return {
        "mae": mae,
        "mse": mse,
        "rmse": rmse,
        "r2": r2
    }


def plot_residuals(y_true, y_pred):
    """
    画残差图(Residual Plot)
    残差 = 真实值 - 预测值
    作用:
        看模型误差有没有明显模式
        如果残差随机分布在 0 附近,通常比较理想
    """
    residuals = y_true - y_pred

    plt.figure(figsize=(8, 5))
    plt.scatter(y_pred, residuals, alpha=0.7)
    plt.axhline(y=0, linestyle="--")  # 画一条 y=0 的参考线
    plt.xlabel("Predicted Value")
    plt.ylabel("Residual (True - Predicted)")
    plt.title("Residual Plot")
    plt.tight_layout()
    plt.show()


def plot_true_vs_pred(y_true, y_pred):
    """
    画真实值 vs 预测值
    如果模型效果好,点会比较接近对角线
    """
    plt.figure(figsize=(6, 6))
    plt.scatter(y_true, y_pred, alpha=0.7)

    # 找到横纵坐标的最小值和最大值,用来画对角线
    min_val = min(np.min(y_true), np.min(y_pred))
    max_val = max(np.max(y_true), np.max(y_pred))

    # 理想情况下,预测值=真实值,所以画一条 y=x 的参考线
    plt.plot([min_val, max_val], [min_val, max_val], linestyle="--")

    plt.xlabel("True Value")
    plt.ylabel("Predicted Value")
    plt.title("True vs Predicted")
    plt.tight_layout()
    plt.show()


def main():
    # =========================================================
    # 1. 读取数据
    # =========================================================
    # load_diabetes(as_frame=True) 会直接返回 pandas DataFrame 格式数据
    diabetes = load_diabetes(as_frame=True)

    # diabetes.frame 里包含:
    # - 所有特征列
    # - target 目标列
    df = diabetes.frame.copy()

    print("===== Raw Data Preview =====")
    print(df.head())  # 看前5行
    print()

    # =========================================================
    # 2. 拆分特征 X 和目标 y
    # =========================================================
    # X: 所有输入特征
    # y: 目标值(我们要预测的连续数值)
    X = df.drop(columns=["target"])
    y = df["target"]

    print("===== Feature Columns =====")
    print(list(X.columns))  # 打印所有特征名
    print()
    print(f"X shape: {X.shape}")  # 特征矩阵形状: (样本数, 特征数)
    print(f"y shape: {y.shape}")  # 目标向量形状: (样本数,)
    print()

    # =========================================================
    # 3. 先拆出最终测试集
    # =========================================================
    # 生产/项目里,测试集一般只在最后使用一次
    # 避免提前“偷看”测试集,导致评估不真实
    X_train_val, X_test, y_train_val, y_test = train_test_split(
        X, y,
        test_size=0.2,         # 20% 做最终测试集
        random_state=RANDOM_STATE
    )

    # =========================================================
    # 4. 再从 train_val 里切出验证集
    # =========================================================
    # 最终比例大致是:
    # train = 60%
    # valid = 20%
    # test  = 20%
    X_train, X_valid, y_train, y_valid = train_test_split(
        X_train_val, y_train_val,
        test_size=0.25,        # train_val 的 25% -> 占总数据 20%
        random_state=RANDOM_STATE
    )

    print("===== Data Split =====")
    print("Train:", X_train.shape, y_train.shape)
    print("Valid:", X_valid.shape, y_valid.shape)
    print("Test :", X_test.shape, y_test.shape)
    print()

    # =========================================================
    # 5. 建立 Pipeline
    # =========================================================
    # Pipeline 的好处:
    # 1) 把预处理和模型串起来
    # 2) 防止数据泄漏
    # 3) 代码更整洁
    pipeline = Pipeline([
        # 先做标准化
        ("scaler", StandardScaler()),

        # 再训练线性回归模型
        ("model", LinearRegression())
    ])

    # =========================================================
    # 6. 在训练集上训练模型
    # =========================================================
    # 本质:
    # 在训练数据上学习最合适的 coef_ 和 intercept_
    pipeline.fit(X_train, y_train)

    # =========================================================
    # 7. 在验证集上预测并评估
    # =========================================================
    y_valid_pred = pipeline.predict(X_valid)
    valid_metrics = evaluate_regression(
        y_valid, y_valid_pred, dataset_name="Validation"
    )

    # =========================================================
    # 8. 做交叉验证
    # =========================================================
    # 这里不在 test 上做,而是在 train_val 上做
    # 作用:
    # 看模型在不同数据切分下是否稳定
    cv = KFold(
        n_splits=5,            # 5折交叉验证
        shuffle=True,          # 先打乱
        random_state=RANDOM_STATE
    )

    # scoring="r2" 表示每一折用 R² 来评估
    cv_scores = cross_val_score(
        pipeline,
        X_train_val,
        y_train_val,
        cv=cv,
        scoring="r2"
    )

    print("===== Cross Validation (R²) =====")
    print("Each fold:", np.round(cv_scores, 4))
    print("Mean R² :", np.mean(cv_scores).round(4))
    print("Std  R² :", np.std(cv_scores).round(4))
    print()

    # =========================================================
    # 9. 用 train+valid 重新训练最终模型
    # =========================================================
    # 原因:
    # 既然模型流程已经确认了,就可以把更多数据拿来训练
    final_pipeline = Pipeline([
        ("scaler", StandardScaler()),
        ("model", LinearRegression())
    ])
    final_pipeline.fit(X_train_val, y_train_val)

    # =========================================================
    # 10. 在最终测试集上做评估
    # =========================================================
    # 注意:
    # test 集只在这里出现一次
    y_test_pred = final_pipeline.predict(X_test)
    test_metrics = evaluate_regression(
        y_test, y_test_pred, dataset_name="Test"
    )

    # =========================================================
    # 11. 查看线性回归学到的系数
    # =========================================================
    # 从 pipeline 中把真正的模型对象取出来
    model = final_pipeline.named_steps["model"]

    # 把特征名和系数对应起来,方便阅读
    coef_df = pd.DataFrame({
        "feature": X.columns,
        "coefficient": model.coef_
    })

    # 按绝对值大小排序,便于看哪个特征影响更大
    coef_df = coef_df.sort_values(
        by="coefficient",
        key=np.abs,
        ascending=False
    )

    print("===== Coefficients =====")
    print(coef_df)
    print()
    print(f"Intercept: {model.intercept_:.4f}")
    print()

    # =========================================================
    # 12. 查看部分预测结果
    # =========================================================
    result_df = pd.DataFrame({
        "true_value": y_test.values,
        "predicted_value": y_test_pred,
        "residual": y_test.values - y_test_pred
    })

    print("===== Prediction Sample =====")
    print(result_df.head(10))  # 只看前10条
    print()

    # =========================================================
    # 13. 残差分析
    # =========================================================
    plot_residuals(y_test, y_test_pred)
    plot_true_vs_pred(y_test, y_test_pred)

    # =========================================================
    # 14. 对新样本做预测
    # =========================================================
    # 这里为了方便,直接拿测试集前3条当“新样本”
    new_samples = X_test.iloc[:3].copy()
    new_preds = final_pipeline.predict(new_samples)

    print("===== New Sample Prediction =====")
    for i, pred in enumerate(new_preds, start=1):
        print(f"Sample {i} predicted target: {pred:.4f}")
    print()

    # =========================================================
    # 15. 简单打印最终摘要
    # =========================================================
    print("===== Final Summary =====")
    print(f"Validation R²: {valid_metrics['r2']:.4f}")
    print(f"Test R²      : {test_metrics['r2']:.4f}")


# Python 脚本入口
if __name__ == "__main__":
    main()

Q1:如何理解MAE,RMSE,R²?

你可以把这三个指标理解成:从三个角度看模型到底准不准。背后是数学推导出来的。如果数学不好,可以直接记忆下面的结论。毕竟都前辈们总结出来的经验。

MAE = 平均每次大概错多少。大白话就是 模型平均每次会偏离真实值多少

  • 真实房价 100 万,你预测 90 万,错了 10 万
  • 真实房价 200 万,你预测 220 万,错了 20 万

RMSE = 也是看平均错多少,但更讨厌大错。RMSE 也表示平均误差,但它对错得特别离谱的情况更敏感

  • MAE:每次错多少,直接平均
  • RMSE:先把大的错误放大,再平均

假设两个模型:

  • 模型 A:大多数时候都小错
  • 模型 B:平时也还行,但偶尔会错特别大

这时候MAE 可能看起来差不多,但是RMSE 往往会把模型 B 判得更差。

= 这个模型到底有没有学到规律,模型对数据规律解释得怎么样。

R² = 1 最好,几乎完美预测

R² = 0 和乱猜平均值差不多

R² < 0 比乱猜平均值还差

举一个例子平均差几分,但如果有些学生被你错得特别.

  • MAE:平均错多少
  • RMSE:平均错多少,但更怕大错
  • R²:模型有没有学到规律,越接近 1 越好

Q2:为什么需要那两个函数plot_residualsplot_true_vs_pred

因为单看一个分数不够。MAE,RMSE,R²。这些指标只能告诉你:整体上好不好。但是无法看出来,错误整体的规律。所以你才要继续看下面的2个图。

  • 模型是不是系统性偏大或偏小
  • 线性回归的假设有没有被破坏

plot_true_vs_pred 表示 预测值(predicted value)和真实值(true value)是不是接近,如果模型很好,点应该尽量靠近那条对角线 y = x ,因为这表示 真实值 = 预测值 。他的含义就是

  • 模型整体预测得准不准
  • 是否在高值区预测偏低
  • 是否在低值区预测偏高
  • 是否有明显离群点(outlier)

例如你发现,小值预测还行,大值总是预测偏低,那说明模型可能没有把大值区间学好。

plot_residuals 表示残差(residual)就是 真实值减预测值,这个图是在看误差有没有模式

residual=yy^\text{residual} = y - \hat{y}

如果线性回归比较合适,残差通常应该 1️⃣围绕 0 上下随机分布 2️⃣没有明显形状。它的意义就是

  • 模型是不是系统性偏高或偏低
  • 误差是不是随着预测值变大而变大
  • 是否存在非线性关系(nonlinearity)
  • 是否有异方差(heteroscedasticity)

大白话就是

  • plot_true_vs_pred:看预测结果和真实值贴不贴近
  • plot_residuals:看误差是不是随机、有没有异常模式

Q3: 下面这段代码如何理解?

下面仔细说一下

cv = KFold(
    n_splits=5, # 5 折交叉验证
    shuffle=True, # 在切 5 份之前,先把数据打乱。这样更随机,不容易因为原数据顺序问题导致切分不均匀。
    random_state=RANDOM_STATE # 固定随机种子 这样每次运行时,打乱方式一致。作用是:结果可复现(reproducible)
)

这里是在定义一种怎么切数据的规则。

n_splits=5 也就是 5 折交叉验证(5-fold cross validation)

  • 把数据分成 5 份
  • 轮流拿 1 份做验证集
  • 剩下 4 份做训练集

结果就是

  • 每轮 80 条训练
  • 20 条验证
  • 总共做 5 轮
cv_scores = cross_val_score(
    pipeline,
    X_train_val,
    y_train_val,
    cv=cv, # 按你前面定义的 KFold(n_splits=5, ...) 来切
    scoring="r2" # 每一折评估时,用的是 R²。最后拿到的 5 个 R² 分数。每个分数对应一折。
)
  • 用 pipeline 这个模型流程
  • X_train_val, y_train_val 这批数据上
  • 按刚才定义的 cv 规则做 5 折交叉验证
  • 每一折都用 R² 来评分

关于pipeline 不仅有回归,还会有标准化。

  • StandardScaler()
  • LinearRegression()

每一折里,都会先做标准化,再训练线性回归

X_train_val, y_train_val 这里用的是训练+验证这部分数据,不包括最终 test 集。因为交叉验证本来就是训练阶段内部用的。

cv_scores 是一个数组 ,表示 同一个模型,在 5 次不同切分下,各自表现如何 比如可能长这样:

[0.41, 0.52, 0.47, 0.38, 0.50]
  • 第 1 折 R² = 0.41
  • 第 2 折 R² = 0.52
  • 第 3 折 R² = 0.47
  • 第 4 折 R² = 0.38
  • 第 5 折 R² = 0.50

最后打印的意义在于

print("Each fold:", np.round(cv_scores, 4)) # 这是把每一折的 R² 打印出来,并保留 4 位小数。
# 意思就是每一轮的小考试分数。
Each fold: [0.4123 0.5211 0.4738 0.3892 0.5014]

print('Mean R² :', np.mean(cv_scores).round(4)) # 这是算 5 折平均分。
# 这个模型在交叉验证里的平均 R² 大约是 0.46 这个平均值通常比单次切分更稳。
Mean R² : 0.4596

print('Std R² :', np.std(cv_scores).round(4)) # 这是算 标准差(standard deviation)。
# 如果标准差小 Std R² : 0.03 表示 5次结果都差不多 模型比较稳定
# 如果标准差大 Std R² : 0.15 表示5次有好又坏 模型不太稳定

Mean R² 表示这个模型平均效果怎么样?

Std R² 这个模型稳不稳?

比喻的话

  • Each fold = 每次小考成绩
  • Mean R² = 平均成绩
  • Std R² = 成绩稳定性

用 5 折交叉验证,检查线性回归模型的平均表现和稳定性。

Q4: 下面这段代码呢

# 按绝对值大小排序,便于看哪个特征影响更大
coef_df = coef_df.sort_values(
    by="coefficient",
    key=np.abs,
    ascending=False
)
  • 多元线性回归是

    y^=w1x1+w2x2++wnxn+b\hat{y} = w_1x_1 + w_2x_2 + \cdots + w_nx_n + b
  • 每个特征都有一个自己的 w

  • w 的正负看影响方向

  • w 的绝对值大小看影响强弱

严格解释系数大小,最好建立在特征尺度可比的前提下。比如做过标准化以后,这样比较更靠谱。

结论就是把模型学到的系数做成可读表格,并按影响强弱排序,方便解释模型。