线性回归与梯度下降——从零手写一个模型

个人主页: > for_ever_love__ <
其他栏目: > 我想学python了 <

其他栏目: > iOS项目总结大全 <

其他栏目: > iOS UI <

文章目录

线性回归与梯度下降------从零手写一个模型

承上:上一篇《监督学习全景》我们知道回归用 MSE、数据集要三段划分。

本篇:本篇不用任何机器学习库,从零用 NumPy 实现线性回归与梯度下降。

启下:下一篇《逻辑回归与 Softmax》把回归输出压成概率,变成分类器。

学完这一节,你能动手做:

  1. 手推 MSE 的梯度并用数值法验证正确性
  2. 实现 BGD、SGD、Mini-batch 三种梯度下降并比较
  3. 说清为什么大模型求不出解析解、只能迭代训练

上一篇我们把监督学习的地图铺开了。这一篇,我们不用任何机器学习库,只靠 NumPy 从零训练一个模型。

为什么要这么做?因为线性回归是最简单的神经网络 ------一个没有隐藏层、没有激活函数的网络。你今天搞懂它的训练过程,后面理解几十亿参数的大模型只是"规模"上的差别,原理完全一样。

本篇你会亲手实现:损失函数、梯度计算、参数更新、批量训练、学习率调度,最后和 sklearn 对比验证。

一、问题定义:找一条最贴合的直线

假设我们有一批数据:房屋面积 (x) 和价格 (y)。想找一条直线 ŷ = w·x + b,让预测值尽量接近真实值。

复制代码
ŷ = w·x + b
  • w:权重(斜率),每平米值多少钱
  • b:偏置(截距),底价

目标 :找到让损失最小的 w 和 b。

先造点带噪声的数据:

python 复制代码
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

n = 100
X = 2 * np.random.rand(n, 1)                 # 面积(单位:100㎡),0~2
y = 4 + 3 * X + np.random.randn(n, 1) * 0.8  # 真实关系 y = 3x + 4 + 噪声

print("X 前5个:", X[:5].ravel().round(2))
print("y 前5个:", y[:5].ravel().round(2))

真实参数是 w=3, b=4。我们的模型能不能把它们"学"出来?

二、损失函数:MSE

python 复制代码
def mse_loss(y_true, y_pred):
    return np.mean((y_pred - y_true) ** 2)

def predict(X, w, b):
    return X @ w + b          # 矩阵写法,支持任意特征数

# 随便猜一组参数看看损失
w = np.array([[0.0]])
b = np.array([0.0])
print("初始 loss:", round(float(mse_loss(y, predict(X, w, b))), 4))

w=0, b=0 时 loss 很大(约 40 多)。我们的任务就是让它降下去。

三、手推梯度

损失:

复制代码
L = (1/n) · Σ (ŷᵢ - yᵢ)²
ŷ = X·w + b

用链式法则:

复制代码
∂L/∂ŷ = (2/n)(ŷ - y)
∂ŷ/∂w = X        →   ∂L/∂w = Xᵀ · ∂L/∂ŷ
∂ŷ/∂b = 1        →   ∂L/∂b = Σ ∂L/∂ŷ

写成矩阵形式非常简洁:

python 复制代码
def gradients(X, y, w, b):
    n = X.shape[0]
    y_pred = predict(X, w, b)
    err = y_pred - y                 # (n,1)
    dw = (X.T @ err) * 2 / n         # (features,1)
    db = np.sum(err) * 2 / n         # scalar
    loss = np.mean(err ** 2)
    return loss, dw, db

loss, dw, db = gradients(X, y, w, b)
print("loss:", round(float(loss), 4), " dw:", dw.ravel(), " db:", round(float(db), 4))

用数值梯度验证一下(这个习惯要养成):

python 复制代码
def numerical_check(X, y, w, b, eps=1e-6):
    _, dw_a, db_a = gradients(X, y, w, b)
    # 检查 dw
    w1 = w.copy(); w1[0, 0] += eps
    w2 = w.copy(); w2[0, 0] -= eps
    dw_n = (mse_loss(y, predict(X, w1, b)) - mse_loss(y, predict(X, w2, b))) / (2 * eps)
    # 检查 db
    db_n = (mse_loss(y, predict(X, w, b + eps)) - mse_loss(y, predict(X, w, b - eps))) / (2 * eps)
    print(f"dw 解析={dw_a[0,0]: .6f}  数值={dw_n: .6f}")
    print(f"db 解析={db_a: .6f}  数值={db_n: .6f}")

numerical_check(X, y, w, b)

两者一致,说明梯度推导正确。

四、梯度下降:一步步走到谷底

核心就一行:w -= lr * dw。

python 复制代码
def train(X, y, lr=0.1, epochs=200, verbose=True):
    n, d = X.shape
    w = np.zeros((d, 1))
    b = np.zeros(1)
    history = []

    for epoch in range(epochs):
        loss, dw, db = gradients(X, y, w, b)
        w -= lr * dw          # 沿梯度反方向更新
        b -= lr * db
        history.append(float(loss))
        if verbose and (epoch % 40 == 0 or epoch == epochs - 1):
            print(f"epoch {epoch:3d}  loss={loss:.4f}  w={w.ravel().round(3)}  b={b.round(3)}")
    return w, b, history

w_trained, b_trained, hist = train(X, y, lr=0.1, epochs=200)
print("\n最终参数: w =", w_trained.ravel(), " b =", b_trained)
print("真实参数: w = [3.]  b = [4.]")

跑完你会看到 w ≈ 3.0、b ≈ 4.0------模型把数据的真实规律学出来了。

画出损失曲线:

python 复制代码
plt.figure(figsize=(7, 4))
plt.plot(hist, lw=2)
plt.xlabel("epoch"); plt.ylabel("MSE loss")
plt.title("Loss Curve")
plt.grid(alpha=0.3); plt.tight_layout(); plt.show()

一条平滑下降最后趋于平缓的曲线------这就是训练成功的样子。

五、学习率:最重要的超参数

学习率决定每步走多大。走小了慢,走大了发散。

python 复制代码
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for ax, lr in zip(axes, [0.01, 0.1, 1.2]):
    try:
        _, _, h = train(X, y, lr=lr, epochs=100, verbose=False)
        ax.plot(h, color="tab:blue")
        ax.set_title(f"lr={lr}  最终loss={h[-1]:.3f}")
    except Exception as e:
        ax.set_title(f"lr={lr}  发散")
    ax.set_xlabel("epoch"); ax.set_ylim(0, 5)
plt.tight_layout(); plt.show()
学习率 现象 处理
太小(0.001) loss 缓慢下降,几百轮还没收敛 调大,或增加 epoch
合适(0.1) 快速下降后收敛 ✅
偏大(1.0+) 震荡,loss 忽高忽低 调小
太大(>2) 发散,loss 变 inf/nan 立刻调小

找学习率的实用技巧:从 1e-3 开始,每次 ×3 试(1e-3 → 3e-3 → 1e-2 → 3e-2 ...),观察哪个量级 loss 降得最快又不炸。

加上学习率衰减

训练后期要"小心地靠近谷底",所以逐步减小步长:

python 复制代码
def train_with_decay(X, y, lr=0.5, epochs=200, decay=0.99):
    n, d = X.shape
    w = np.zeros((d, 1)); b = np.zeros(1)
    history = []
    cur_lr = lr
    for epoch in range(epochs):
        loss, dw, db = gradients(X, y, w, b)
        w -= cur_lr * dw
        b -= cur_lr * db
        cur_lr *= decay                    # 指数衰减
        history.append(float(loss))
    return w, b, history

_, _, h_decay = train_with_decay(X, y)
print("带衰减最终 loss:", round(h_decay[-1], 5))

六、批量梯度下降的三种形态

真实训练不会一次把所有数据喂进去,有三种粒度:

python 复制代码
def train_minibatch(X, y, lr=0.1, epochs=50, batch_size=16, seed=0):
    """Mini-batch 梯度下降:深度学习的事实标准"""
    rng = np.random.default_rng(seed)
    n, d = X.shape
    w = np.zeros((d, 1)); b = np.zeros(1)
    history = []
    for epoch in range(epochs):
        idx = rng.permutation(n)              # 每轮打乱顺序
        for start in range(0, n, batch_size):
            batch = idx[start:start + batch_size]
            Xb, yb = X[batch], y[batch]
            loss, dw, db = gradients(Xb, yb, w, b)
            w -= lr * dw
            b -= lr * db
        history.append(float(mse_loss(y, predict(X, w, b))))
    return w, b, history

w_mb, b_mb, h_mb = train_minibatch(X, y)
print("Mini-batch 结果: w =", w_mb.ravel().round(3), " b =", b_mb.round(3))
方式 每次用多少样本 优点 缺点
BGD 批量 全部 梯度准确、稳定收敛 大 dataset 内存爆炸、慢
SGD 随机 1 个 更新快、能跳出局部最优 梯度噪声大、震荡
Mini-batch 16/32/64/128 兼顾,可并行(GPU 友好) 需调 batch size

深度学习几乎都用 mini-batch,因为 GPU 最擅长一次算一批矩阵乘法。大模型训练里 batch size 常常是 512 甚至更大。

七、多元线性回归:特征不止一个

真实场景有很多特征。代码完全不变,因为我们已经用了矩阵写法:

python 复制代码
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

data = fetch_california_housing()
Xh, yh = data.data, data.target.reshape(-1, 1)

X_train, X_test, y_train, y_test = train_test_split(Xh, yh, test_size=0.2, random_state=42)

# 必须标准化!否则不同量纲的特征会让训练崩溃
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s  = scaler.transform(X_test)

w_h, b_h, _ = train(X_train_s, y_train, lr=0.05, epochs=300, verbose=False)

pred_test = predict(X_test_s, w_h, b_h)
rmse = np.sqrt(mse_loss(y_test, pred_test))
print(f"手写线性回归 测试集 RMSE = {rmse:.4f}")

八、和 sklearn 对比验证

写对了没有?拿官方实现对比:

python 复制代码
from sklearn.linear_model import LinearRegression, SGDRegressor
from sklearn.metrics import mean_squared_error, r2_score

sk = LinearRegression().fit(X_train_s, y_train)
sk_pred = sk.predict(X_test_s)

print("sklearn LinearRegression RMSE:", round(np.sqrt(mean_squared_error(y_test, sk_pred)), 4))
print("手写梯度下降       RMSE:", round(rmse, 4))
print("R² (sklearn):", round(r2_score(y_test, sk_pred), 4))
print("R² (手写)   :", round(r2_score(y_test, pred_test), 4))

print("\n权重最大差异:", np.abs(sk.coef_.ravel() - w_h.ravel()).max())

两者结果几乎一致(差异在 1e-3 量级)。说明我们的实现是正确的。

有意思的是,LinearRegression 用的是正规方程(解析解):

python 复制代码
# 正规方程:w = (XᵀX)⁻¹ Xᵀy
X_aug = np.c_[np.ones(len(X_train_s)), X_train_s]      # 加一列 1 表示偏置
w_closed = np.linalg.pinv(X_aug.T @ X_aug) @ X_aug.T @ y_train
print("正规方程 b =", w_closed[0].round(4), " sklearn b =", round(sk.intercept_[0], 4))
解法 原理 适用
正规方程 一步解析求解 (XᵀX)⁻¹Xᵀy 特征 < 1万,小数据集
梯度下降 迭代逼近 任意规模,深度学习的唯一选择

大模型有几十亿参数,XᵀX 这个矩阵根本存不下(几十亿 × 几十亿),所以只能用梯度下降。这就是为什么你只听过"训练大模型"而没听过"解方程求大模型"。

九、常见坑与注意事项

坑 现象 解决
忘了标准化 loss 不降或 NaN 先 StandardScaler
学习率太大 loss 变 inf/nan 降到 1/10 重试
特征里有字符串 UFuncTypeError 先编码(One-Hot)
没加偏置 b 直线必须过原点,拟合差 记得加 bias 项
只在训练集看指标 上线效果差 一定要看测试集
形状搞错 (100,1) vs (100,) 统一用二维 (n, d)

形状提示 :np.mean(err**2) 返回标量,而 err 是 (n,1)。矩阵乘法时注意 X.T @ err 得到 (d,1),正好和 w 同形。

十、本篇小结

  1. 线性回归 ŷ = Xw + b 就是最简单的神经网络------没有隐藏层、没有激活函数。
  2. 手动推导了 MSE 的梯度 ∂L/∂w = 2Xᵀ(ŷ-y)/n,并用数值梯度验证正确。
  3. 梯度下降 w -= lr·dw 是训练的核心动作;学习率是最重要的超参数,可用衰减策略精调。
  4. Mini-batch 是深度学习的事实标准(BGD/SGD/Mini-batch 三选一)。
  5. 正规方程 vs 梯度下降:大模型参数太多,求不出解析解,只能迭代------这就是梯度下降不可替代的原因。
  6. 手写实现与 sklearn 结果一致(RMSE 差异 < 1e-3),验证了正确性。

下一篇 逻辑回归与 Softmax :把线性回归的输出"压"成概率,就变成了分类器。我们会从零实现 Softmax 回归,并揭示一个关键事实------大模型最后一层输出下一个词的概率,用的正是 Softmax。

本篇是《大模型开发从 0 到 1》专栏第 18 篇,阶段 3「机器学习基础」第 2 篇。专栏文章按「分类专栏」归类,顺序学习体验最佳。

相关推荐
code_whiter1 小时前
7.自动化测试常用函数
python·功能测试
小园子的小菜1 小时前
Python协程深度解析:从原理演进到实战避坑
后端·python
砚底藏山河1 小时前
量化实战:截面因子有效性检验(IC 分析与分层回测)
java·python·金融·maven
GitFun2 小时前
给策略加了条-8%止损线,11年只触发1次
python·股票·量化交易
马六六i3 小时前
市面上正规的IP驱动产业新场景新工具有哪些
大数据·网络·python·tcp/ip
AOI小白新手上路3 小时前
anomalib 缺陷检测复现笔记:从跑通库到 EfficientAD 落地
人工智能·笔记·机器学习
I Am a robert girl4 小时前
稀有事件估计的迭代去对齐:从源码视角拆解重要性采样新范式
开发语言·python·机器学习·重要性采样·蒙特卡洛方法·稀有事件估计
天若有情6734 小时前
开源|Comfort Lang v1.0.1,一款主打清爽交互的新型命令式编程语言
python·开源·交互·编程语言·项目分享·repl·comfort lang
打工仔折腾 AI4 小时前
System Prompt 替代 Few-shot:用规则约束大模型输出的省钱实践
java·人工智能·python·spring·langchain·prompt·ai agent 实战