元数据卡
- 前置知识:ch04 ML 基础、Vol 10 线性代数与微积分
- 预计时间:50 分钟
- 核心难度:进阶
- 阅读模式:高度专注
- 完成标志:能用梯度下降实现线性回归和逻辑回归,理解正则化的效果
你的进度
模型工坊的数据工作台铺满了数据:房屋面积与价格、广告投放与点击率、学习时间与考试成绩。
每一组数据都有一条趋势线隐藏在其中——一条线把数据和预测连起来。你拿起工具,开始从最简单的地方入手:找到一个公式,把输入映射到输出。
你的任务
线性模型假设输出是输入的线性组合。这个假设在回归和分类问题上都有效:线性回归预测连续值,逻辑回归输出概率。你从最小二乘法的解析解开始,过渡到梯度下降,引入正则化处理高维数据。
本章分层
- 必读:线性回归(解析+梯度下降)、逻辑回归、L1/L2 正则化
- 选读:Softmax 回归、广义线性模型
- 进阶:凸优化视角下的线性模型收敛性
破局 · 溯源
你的朋友记录了一组数据:每天的学习时间和考试成绩。你直觉上感觉两者有关——学的时间越长分越高。但"越长"和"越高"之间是什么关系?你需要用一条线去拟合这些点。
线性回归就是这样:找到一条直线 y = wx + b,让所有点到这条直线的垂直距离之和最小。
线性回归
最小二乘法的直观做法:最小化均方误差。
import numpy as np
from sklearn.linear_model import LinearRegression
# 模拟数据:y = 3x + 2 + noise
np.random.seed(42)
X = np.random.randn(100, 1)
y = 3 * X.ravel() + 2 + np.random.randn(100) * 0.5
model = LinearRegression()
model.fit(X, y)
print(f"w = {model.coef_[0]:.3f}, b = {model.intercept_:.3f}")sklearn 的解是调用 LAPACK 的封闭形式解 w = (X^T X)^{-1} X^T y。但这是一个 O(n^3) 操作,当特征维度 d 很大时不可行。
梯度下降
用迭代替代封闭解:
class LinearRegressionGD:
def __init__(self, lr=0.01, epochs=1000):
self.lr = lr
self.epochs = epochs
def fit(self, X, y):
X = np.c_[np.ones(X.shape[0]), X] # 加偏置列
self.weights = np.random.randn(X.shape[1]) * 0.01
self.loss_history = []
for _ in range(self.epochs):
y_pred = X @ self.weights
error = y_pred - y
# MSE 梯度: (2/n) * X^T * (y_pred - y)
grad = (2 / len(y)) * X.T @ error
self.weights -= self.lr * grad
self.loss_history.append(np.mean(error ** 2))
def predict(self, X):
X = np.c_[np.ones(X.shape[0]), X]
return X @ self.weights运行 1000 轮后,你的梯度下降逼近了封闭解相同的 w 和 b。
由于梯度下降只依赖梯度,它扩展到任意大规模数据——深度学习的训练引擎就是梯度下降的变体。
逻辑回归
线性回归预测连续值。但如果你想要"是否及格"(0 或 1)呢?逻辑回归在线性组合后面套一个 sigmoid 函数,输出变成 [0, 1] 的概率:
P(y=1|x) = 1 / (1 + exp(-(wx + b)))损失函数从 MSE 换成二元交叉熵(也称为逻辑损失):
class LogisticRegressionGD:
def __init__(self, lr=0.01, epochs=1000):
self.lr = lr
self.epochs = epochs
def sigmoid(self, z):
return 1 / (1 + np.exp(-np.clip(z, -250, 250)))
def fit(self, X, y):
X = np.c_[np.ones(X.shape[0]), X]
self.weights = np.random.randn(X.shape[1]) * 0.01
for _ in range(self.epochs):
z = X @ self.weights
y_pred = self.sigmoid(z)
# 交叉熵梯度: (1/n) * X^T * (y_pred - y)
grad = (1 / len(y)) * X.T @ (y_pred - y)
self.weights -= self.lr * grad
def predict_proba(self, X):
X = np.c_[np.ones(X.shape[0]), X]
return self.sigmoid(X @ self.weights)
def predict(self, X, threshold=0.5):
return (self.predict_proba(X) >= threshold).astype(int)逻辑回归的决策边界仍然是线性的——它只是把线性输出映射到了概率空间。但"线性边界"意味着两类之间必须能用一条直线分开(线性可分)。如果数据不是线性可分的,你需要特征变换或更复杂的模型。
正则化
当特征维度爆炸时(比如文本分类中每个词是一个特征),线性模型容易过拟合。正则化在损失函数中添加系数的惩罚项:
- L2(Ridge):加 w^2 惩罚,鼓励权重均匀分布
- L1(Lasso):加 |w| 惩罚,鼓励权重稀疏(很多变为 0)
- 弹性网(ElasticNet):加 L1 + L2
from sklearn.linear_model import Ridge, Lasso, ElasticNet
# L2 正则化
ridge = Ridge(alpha=1.0)
ridge.fit(X, y)
# L1 正则化(自动特征选择)
lasso = Lasso(alpha=0.1)
lasso.fit(X, y)
print(f"非零系数数量: {np.sum(lasso.coef_ != 0)}")在文本分类中,L1 正则化自动选出最关键的词——其他词的权重变为 0,模型的推理时计算显著变快。
常见陷阱
- 梯度下降的学习率:过大不收敛,过小太慢。试 log 尺度的几个值(0.1, 0.01, 0.001),观察 loss 曲线。
- 特征尺度对梯度下降的影响很大。两个特征的范围差 1000 倍时,梯度向量被大特征主导。标准化是必须的预处理。
- 逻辑回归中类别不均衡的影响:负类过多会导致模型偏向预测负类。可以用 class_weight='balanced' 或过采样/欠采样。
- 线性回归假设误差独立同分布且方差恒定(同方差性)。如果违反(比如预测值越大,误差越大),标准差估计不准确。
- 共线性问题:两个高度相关的特征会让系数估计值波动巨大,且符号可能反直觉。
通关挑战
- 热身(10 分钟):用 sklearn.datasets.load_breast_cancer,训练一个逻辑回归,比较标准化前后收敛速度和最终准确率。
- 挑战(30 分钟):在线性回归的最优解附近到底发生了什么?固定 x 值,手动计算损失函数的 Hessian 矩阵,验证它是正定的(凸函数条件)。
- 观察:在 Lasso 中从 alpha=0.0001 到 alpha=10,观察非零系数数量如何随正则化强度变化。
旅人笔记
线性模型是机器学习世界的"Hello, World"。它假设简单、解释性强、泛化性好,但受限于线性决策边界。当数据规模大、噪声少、特征重要时,线性模型仍然是最佳起手式。
-> 下一站预告
线性模型受限于决策边界的线性。下一章你将砍掉这个限制——用树和集成方法拟合任意复杂的边界。