# 01 · 线性回归与逻辑回归 > AI生成|2026年10月26日|codebuddy **本篇目标**:搞懂机器学习里最基础、也最常被低估的两个模型——线性回归(预测数值)和逻辑回归(预测概率)。它们的地位类似武侠里的马步:**简单、可解释、是所有复杂模型的对比基线**。 **导航**:[栏目首页](./README.md) | 上一篇:[00 · 导论与建模流程](./00-机器学习导论与建模流程.md) | 下一篇:[02 · 决策树与集成学习](./02-决策树与集成学习.md) --- ## 一、核心概念 | | 线性回归 | 逻辑回归 | | --- | --- | --- | | 任务类型 | **回归**(预测连续值) | **分类**(预测离散类别,输出的是概率) | | 输出 | 任意实数,如 352.7 万 | 0~1 之间的概率,如 0.83 | | 输出层 | 无(恒等) | Sigmoid(把实数压进 0~1) | | 损失函数 | MSE(均方误差) | Log Loss / 交叉熵 | | 名字的坑 | — | 叫「回归」但做的是**分类** | 两者共享同一个骨架:**先算特征的加权和 z = wᵀx + b,再决定怎么解释 z**。线性回归直接输出 z;逻辑回归把 z 塞进 Sigmoid 变成概率。 --- ## 二、模型原理 ### 2.1 线性回归 **模型**:给定特征向量 x(含常数项),预测 ŷ = wᵀx = Σ wⱼ xⱼ + b。 **损失函数**(均方误差): ```text J(w) = 1/n · Σ (yᵢ − ŷᵢ)² = 1/n · ||y − Xw||² ``` **两条求解路线**: 1. **正规方程(闭式解)**:令梯度为零,直接解出 ```text w* = (XᵀX)⁻¹ Xᵀy ``` 优点:一步到位、无随机性。缺点:矩阵求逆 O(p³),**特征上万维就扛不住**;且 XᵀX 奇异(特征共线性严重)时无解。 2. **梯度下降**:沿负梯度逐步更新 ```text w ← w − η · ∂J/∂w = w − η · (2/n) Xᵀ(Xw − y) ``` η 是学习率:太大发散,太小收敛慢。scikit-learn 的 `LinearRegression` 走闭式解,需要处理大数据时用 `SGDRegressor`。 **正则化**(在损失里加惩罚项,抑制过大的权重): | 方法 | 惩罚项 | 效果 | 适用 | | --- | --- | --- | --- | | Ridge(岭回归,L2) | λ·Σwⱼ² | 权重整体压缩但**不为零** | 特征都有用、存在共线性 | | Lasso(L1) | λ·Σ|wⱼ| | 部分权重**精确变 0**,等于自动做特征选择 | 特征很多、想要稀疏解 | | ElasticNet | λ₁·||w||₁ + λ₂·||w||₂² | 兼顾两者 | 特征高度相关时比 Lasso 稳定 | 加了 L2 之后闭式解变成 `w* = (XᵀX + λI)⁻¹Xᵀy`,顺便治好了共线性导致的奇异矩阵。 ### 2.2 逻辑回归 **模型**: ```text z = wᵀx + b 线性打分 p = σ(z) = 1 / (1 + e^(-z)) 压成概率 预测规则:p ≥ 阈值 → 正类,否则负类 ``` Sigmoid 的形状:z=0 时 p=0.5,z→+∞ 时 p→1,z→−∞ 时 p→0。 **损失函数**(对数似然损失 / 二分类交叉熵): ```text J(w) = −1/n · Σ [ yᵢ·log pᵢ + (1 − yᵢ)·log(1 − pᵢ) ] ``` 注意它的「惩罚不对称」:真实标签是 1 而模型给出 p=0.01 时,`-log(0.01) ≈ 4.6`,损失巨大——这让逻辑回归对「自信地猜错」极其敏感。 **三个关键认知**: 1. **决策边界是线性的**:能学到的一定是一条直线(高维里是超平面)。想拟合"月牙形"数据,必须先做**特征工程**(多项式特征、分箱、交叉特征)。 2. **输出是概率,阈值可以调**:默认 0.5 只是「两类代价相同」时的选择;风控、医疗场景往往用 0.2 甚至更低来换取高 recall。 3. **多分类用 softmax**:把多个类别的打分做归一化 `softmax(z_k) = e^{z_k} / Σ e^{z_j}`,或拆成多个二分类(One-vs-Rest)。 ### 2.3 评估指标 **回归**: | 指标 | 公式/含义 | 特点 | | --- | --- | --- | | MSE | 均方误差 | 被平方放大,**对异常值敏感** | | RMSE | √MSE | 与目标同量纲,最常用 | | MAE | 平均绝对误差 | 对异常值稳健 | | R² | 1 − SSE/SST | 相对「预测均值」这个基线提升了多少;负数说明还不如直接猜均值 | **分类**(务必背下来的四个 + 两条曲线): ```text 精确率 Precision = TP / (TP + FP) 预测为正的里面,有多少是真的 召回率 Recall = TP / (TP + FN) 真实为正的里面,你抓到了多少 F1 = 2·P·R / (P + R) 两者的调和平均 Accuracy = (TP + TN) / 全部 只在类别均衡时有用 ``` - **ROC 曲线**(横 FPR、纵 TPR),面积 **ROC-AUC**:0.5 随机、1.0 完美;对每个阈值都评价,适合看整体排序能力。 - **PR 曲线**(横 recall、纵 precision),面积 **PR-AUC**:**正负样本极不平衡时比 ROC 更真实**。 --- ## 三、实例 ### 3.1 回归实例:糖尿病病情进展预测 对比普通线性回归、Ridge、Lasso 三种写法,并观察 Lasso 如何自动做特征选择。 ```python import numpy as np from sklearn.datasets import load_diabetes from sklearn.model_selection import train_test_split from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler, PolynomialFeatures from sklearn.linear_model import LinearRegression, RidgeCV, LassoCV from sklearn.metrics import mean_squared_error, r2_score data = load_diabetes() X, y, names = data.data, data.target, data.feature_names X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) def report(tag, model): model.fit(X_train, y_train) pred = model.predict(X_test) rmse = np.sqrt(mean_squared_error(y_test, pred)) r2 = r2_score(y_test, pred) print("{:26s} RMSE={:7.2f} R2={:6.3f}".format(tag, rmse, r2)) report("LinearRegression", make_pipeline(StandardScaler(), LinearRegression())) report("RidgeCV (alpha auto)", make_pipeline( StandardScaler(), RidgeCV(alphas=np.logspace(-3, 3, 50), cv=5))) report("LassoCV (alpha auto)", make_pipeline( StandardScaler(), LassoCV(alphas=np.logspace(-3, 2, 50), cv=5, max_iter=20000, random_state=42))) report("Poly2 + Ridge", make_pipeline( StandardScaler(), PolynomialFeatures(degree=2, include_bias=False), RidgeCV(alphas=np.logspace(-3, 3, 50), cv=5))) # 看 Lasso 的稀疏性:哪些特征被它判定为无关 pipe = make_pipeline(StandardScaler(), LassoCV( alphas=np.logspace(-3, 2, 50), cv=5, max_iter=20000, random_state=42)) pipe.fit(X_train, y_train) coef = pipe.named_steps["lassocv"].coef_ print("\nLasso 选中 {} / {} 个特征:".format(int((coef != 0).sum()), len(coef))) for name, c in sorted(zip(names, coef), key=lambda t: -abs(t[1])): print(" {:8s} {:+8.2f}".format(name, c)) ``` **怎么读输出**: - 四种模型的 RMSE 差距通常不大——这个数据集信号弱、噪声大,**复杂模型也榨不出更多信息**,这正是「先跑基线」的价值。 - `Poly2 + Ridge` 把特征从 10 维扩到 65 维,RMSE 往往会变差,说明这里不存在明显的非线性/交互效应,扩张反而引入噪声。 - Lasso 那行输出的系数里,被置 0 的特征可以直接从建模候选里剔除。 ### 3.2 分类实例:乳腺癌良恶性预测 + 阈值调优 重点是演示**「概率输出的模型,阈值不该默认取 0.5」**。 ```python import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split, cross_val_predict, StratifiedKFold from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import (roc_curve, roc_auc_score, precision_recall_curve, confusion_matrix, classification_report) X, y = load_breast_cancer(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) pipe = make_pipeline( StandardScaler(), LogisticRegression(max_iter=5000, C=1.0, class_weight="balanced"), ) # 用训练集做交叉验证,得到「折外(out-of-fold)」概率,用于挑阈值(不能拿测试集挑!) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) oof = cross_val_predict(pipe, X_train, y_train, cv=cv, method="predict_proba")[:, 1] fpr, tpr, _ = roc_curve(y_train, oof) print("OOF ROC-AUC = {:.4f}".format(roc_auc_score(y_train, oof))) # 在 PR 曲线上挑 F1 最大的阈值 prec, rec, thr = precision_recall_curve(y_train, oof) f1s = 2 * prec * rec / np.clip(prec + rec, 1e-12, None) best = int(np.argmax(f1s[:-1])) # thr 比 prec/rec 少一个点 best_thr = thr[best] print("最优阈值 = {:.3f}(此时 precision={:.3f}, recall={:.3f})".format( best_thr, prec[best], rec[best])) # 整套训练集拟合,测试集只评估一次 pipe.fit(X_train, y_train) prob = pipe.predict_proba(X_test)[:, 1] fig, ax = plt.subplots(1, 2, figsize=(11, 4)) ax[0].plot(fpr, tpr, lw=2) ax[0].plot([0, 1], [0, 1], "k--", lw=1) ax[0].set(xlabel="False Positive Rate", ylabel="True Positive Rate", title="ROC") ax[1].plot(rec, prec, lw=2) ax[1].scatter([rec[best]], [prec[best]], c="red", zorder=5) ax[1].set(xlabel="Recall", ylabel="Precision", title="PR curve (red = best thr)") plt.tight_layout() plt.show() # 默认阈值 0.5 vs 调优后的阈值 for tag, arr in [("thr=0.50", (prob >= 0.5).astype(int)), ("thr={:.3f}".format(best_thr), (prob >= best_thr).astype(int))]: print("\n--- {} ---".format(tag)) print(confusion_matrix(y_test, arr)) print(classification_report(y_test, arr)) ``` **怎么读输出**: - 医疗场景里,**漏诊一个恶性肿瘤的代价(FN)远高于误报(FP)**,所以真正该优化的是 recall;这时把阈值往低处调(如 0.3)往往比换模型更有效。 - 两条混淆矩阵的差别通常会告诉你:0.5 阈值下 FN 更多,调低阈值后 FN 减少但 FP 上升——这就是 precision/recall 之间的取舍,没有免费午餐。 - `class_weight="balanced"` 会自动按类别频率给少数类加权,等价于「把不平衡问题在损失层面补偿掉」。 --- ## 四、常见坑与调参要点 | 坑 | 现象 | 解决 | | --- | --- | --- | | 没做标准化 | 逻辑回归收敛慢、`ConvergenceWarning`、系数不可比较 | Pipeline 里加 `StandardScaler`,尤其是用了正则 | | 多重共线性 | 系数符号乱跳、方差极大 | 用 Ridge;或先做相关性剔除 / PCA | | 数据非线性 | 逻辑回归无论怎么调都不行 | 加多项式特征、分箱(`KBinsDiscretizer`)、换模型 | | 异常值多 | RMSE 被少数点主导 | 用 MAE / `HuberRegressor`;或对目标做对数变换 | | 类别不平衡 | accuracy 很高但一个正类都没抓到 | `class_weight="balanced"`,改看 PR-AUC 与 recall | | C 越大越容易过拟合 | `C` 是**正则强度的倒数**(C 大 = λ 小 = 限制弱) | 网格搜索 C ∈ [0.001, 0.01, 0.1, 1, 10, 100] | | `max_iter` 不够 | 报 convergence warning | 调大 `max_iter`,或换 `solver="liblinear"` / `saga` | **关键超参数速查**: | 超参 | 含义 | 常用取值 | | --- | --- | --- | | `C` / `alpha` | 正则强度(前者倒数,后者正比) | 对数网格 `np.logspace(-3, 3, 7)` | | `penalty` | L1 / L2 / ElasticNet | 特征多选 `l1`,否则 `l2` | | `solver` | 优化算法 | 小数据 `liblinear`;大数据 `saga`;多分类 `lbfgs` | | `class_weight` | 类别权重 | 不平衡时 `balanced` | | `fit_intercept` | 是否学习偏置 | 默认 True;已中心化的数据可关 | --- ## 五、小结 - 线性回归 = 加权求和 + MSE;有闭式解是它最大的优势,加 L2 后还能治共线性。 - 逻辑回归 = 加权求和 + Sigmoid + 交叉熵,输出**校准过的概率**,这在需要「按概率排序/定价」的业务里价值极高(信贷评分、CTR 预估至今仍在用)。 - 二者的共同短板是**线性假设**——能力边界决定了什么时候该换模型。 - 实用技巧:**阈值是模型的第二个旋钮**,调它往往比调超参数收益更大。 下一篇进入表格数据真正的主力:[02 · 决策树与集成学习](./02-决策树与集成学习.md)。