MLE / Research · ML Coding / Fundamentals

TikTok 手写 Logistic Regression:稳定 BCE、权重和偏置梯度

从线性logit到sigmoid推导平均损失,用有限差分核梯度,说明训练与阈值不同步骤。

题目重点:从线性logit到sigmoid推导平均损失,用有限差分核梯度,说明训练与阈值不同步骤。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练二分类线性logit=z=x·w+b,无正则/权重,平均BCE;代码提供loss和梯度,优化循环可按约定迭代。
  • 特征和参数有限,但过大线性和仍需限制数值范围;目标标签0/1。

解题路线

  1. 完整训练循环在稳定 BCE 梯度之后初始化权重/偏置,每轮先算本轮 loss 与梯度,再同步执行 w←w−ηdw、b←b−ηdb,保留更新前 loss。示例使用 full-batch、零初始化、固定迭代数、无正则,不把训练 loss 改善当泛化证据;验证集划分与标准化参数只能从训练集估计。
  2. 模型p=σ(z),它是概率模型,不先把p按0.5硬化再求损失。BCE=−ylogp−(1−y)log(1−p),可改写softplus(z)−yz。
  3. 稳定softplus=max(z,0)+log1p(exp(−|z|)),避免exp(1000)溢出;sigmoid正负分支分别避免大正指数。不要通过随意clip概率改变正确logit损失。
  4. 单样本对z梯度p−y,权重为(p−y)x,偏置为p−y;全batch平均。零特征时权重梯度0但偏置仍可学习基础率,bias不能漏。
  5. 训练更新w←w−lr·dw、b←b−lr·db,检查损失、梯度与验证集。每次loss下降并非泛化保证,学习率太大可震荡;特征尺度差会影响优化条件。
  6. 正则可加L2等,但要明确是否惩罚bias、系数中的1/2以及mean/sum约定;多类softmax与多标签独立sigmoid不是相同模型。
  7. 0.5决策阈值只在特定代价下自然,业务可用验证集选阈值,AUC排名与校准另测;不要以训练准确率独自决定质量。
  8. 有限差分分别扰动每个w与b,核解析梯度;极端logit单测forward稳定,不在sigmoid饱和处单凭相对误差判断错误。

手动走一遍

w=0,b=0时每例p0.5,正例梯度对z为−0.5,负例+0.5。若x=0且正例更多,只bias会调整基础概率;大正z预测负类时BCE仍应近似z而非无穷。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

from math import exp, log1p, isfinite


def sigmoid(z):
    if z >= 0:
        return 1 / (1 + exp(-z))
    e = exp(z)
    return e / (1 + e)


def logistic_loss_gradient(x, y, weights, bias):
    if not x or len(x) != len(y) or any(len(row) != len(weights) for row in x):
        raise ValueError('matching rectangular training data required')
    if any(label not in (0, 1) for label in y):
        raise ValueError('binary labels required')
    if any(not isfinite(v) for row in x for v in row) or any(not isfinite(v) for v in weights) or not isfinite(bias):
        raise ValueError('finite data required')
    loss, grad, db = 0.0, [0.0] * len(weights), 0.0
    for row, label in zip(x, y):
        z = bias + sum(a * b for a, b in zip(row, weights))
        loss += max(z, 0) - label * z + log1p(exp(-abs(z)))
        residual = sigmoid(z) - label
        for j, value in enumerate(row):
            grad[j] += residual * value
        db += residual
    n = len(x)
    return loss / n, [g / n for g in grad], db / n


def train_logistic(x, y, learning_rate=0.1, steps=100):
    """Full-batch gradient descent, zero initialization; no regularization.
    History is the objective before each update. Fixed iteration budget."""
    if not isfinite(learning_rate) or learning_rate <= 0 or steps < 0:
        raise ValueError('positive finite rate and nonnegative steps required')
    if not x:
        raise ValueError('nonempty training data required')
    weights, bias, history = [0.0] * len(x[0]), 0.0, []
    logistic_loss_gradient(x, y, weights, bias)  # validate even for zero steps
    for _ in range(steps):
        loss, dw, db = logistic_loss_gradient(x, y, weights, bias)
        history.append(loss)
        weights = [w - learning_rate * g for w, g in zip(weights, dw)]
        bias -= learning_rate * db
    return weights, bias, history

复杂度

N×D输入每步O(ND),梯度O(D)工作空间,数据O(ND)由调用方持有。T次全batch训练O(TND),不承诺一步求最优。

易错点与追问

容易失分的边界

  • 先threshold再反向。
  • 偏置漏梯度。
  • 损失mean但梯度sum。
  • sigmoid单公式数值溢出。

继续深挖

  • L2正则怎样改?
  • 稀疏特征如何加速?
  • 类别不平衡怎样选损失/阈值?
✓已复制微信号!