ML05.md

逻辑回归

  • 该方法用于分类问题(二元分类,也就是y取0或1)

  • 逻辑函数(sigmoid function)

决策边界

  • 求 时,x 对应的函数即为决策边界;

逻辑回归决定的成本函数

  • 针对一个多参数,二分类问题,我们可以定义其函数为:
  • 可以回顾一下线性回归的成本函数:
  • 如果对二分类定义的函数使用线性回归的成本函数,那么构成的损失函数的图像是非凸成本函数,会包含许多局部最小值,导致梯度下降效率不高。这时候,需要重新定义,逻辑回归函数的成本函数。
  • 这里特别定义:
  • 逻辑回归损失函数:

逻辑回归的简化版代价函数

  • 可以将上述函数合并成一个式子:
  • 代价函数:

将梯度下降用于逻辑回归

  • 梯度下降:

  • 将逻辑回归代价函数代入得到:

    (这里的x_j表示,第i个参数的第j个特征)

过拟合

  • 欠拟合:high bias
  • 过拟合:对于训练集的适应效果太好,导致泛化能力很差,或者说对于其他特征很敏感。high variance

如何解决过拟合

  • 获取更多的数据集
  • 使用较少的(最相关的)特征(参数),但是可能会较大程度的影响模型效果
  • 使用正则化减少参数的权重大小

正则化

  • 正则化代价函数(线性):

正则化线性回归实现梯度下降

  • 梯度下降:

    (同步更新)

正则化逻辑回归实现梯度下降

  • 梯度下降重复执行以下同步更新:


从公式到代码:实现正则化逻辑回归

下面把前面的公式逐步翻译为浏览器中真正运行的 JavaScript。完整实验没有使用机器学习框架,这样可以直接看到数据、梯度、正则项和参数更新之间的关系。

在线实验:在本博客中打开 | 独立公开页面 | 下载完整源码

1. 程序中的参数分别对应什么

数学符号 / 设置 代码中的变量 主要影响
weights[1...] 每个特征对分类结果的影响强度
weights[0] 整体平移决策边界,不参与 L2 正则化
learningRate 每次梯度更新走多远;过大可能震荡或发散
lambdaOptions[lambdaIndex] 收缩权重,控制模型对复杂特征的依赖程度
epoch samplesSeen / train.length 全部训练样本被使用过多少遍
特征次数 linear / quadratic / degree6 决定模型最多能表示多复杂的边界

需要特别区分:特征次数决定模型有没有能力弯曲, 决定是否压制这种能力。 并不是直接修改边界线,而是先改变权重,再间接改变预测概率和边界。

2. 构建带随机性的数据

实验先从截断高斯分布采样 ,然后通过真实边界计算概率,最后按伯努利分布抽取标签。这比用一条曲线把标签硬切成 0 和 1 更符合现实:即使两个样本位置相近,它们也不一定拥有相同标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function trueBoundary(x1) {
return 0.42 * x1 * x1 - 0.08 * x1 - 0.65;
}

function generatePoint(rng, noise, labelFlipRate) {
const x1 = Math.max(-2.65, Math.min(2.65, normal(rng) * 1.16));
const x2 = Math.max(-2.35, Math.min(2.35, normal(rng) * 1.02));

// noise 越大,Sigmoid 越平缓,边界附近的类别重叠越明显。
const logit = (x2 - trueBoundary(x1)) / noise;
let y = rng() < sigmoid(logit) ? 1 : 0;

// 模拟现实中的标注错误或测量错误。
if (rng() < labelFlipRate) y = 1 - y;
return { x1, x2, y };
}

页面使用固定种子的伪随机数生成器,因此相同种子会得到完全相同的数据。比较不同 时应保持种子不变,否则差异可能来自数据变化,而不是正则化。

数据按类别进行 75%/25% 分层划分:

  • 训练集参与梯度更新;
  • 测试集只用于观察泛化能力;
  • 均值、标准差只由训练集计算,避免测试集信息泄漏。

3. 构造特征并只用训练集标准化

线性模型只有 ,二次模型加入 。六次模型包含所有总次数不超过 6 的二元单项式,共 27 个特征,加上偏置后一共 28 个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function rawFeatures(point, model) {
const a = (point.x1 - stats.mean1) / stats.sd1;
const b = (point.x2 - stats.mean2) / stats.sd2;
const degree = model === "linear" ? 1 : model === "quadratic" ? 2 : 6;
const values = [];

for (let total = 1; total <= degree; total++) {
for (let p1 = total; p1 >= 0; p1--) {
const p2 = total - p1;
values.push((a ** p1) * (b ** p2));
}
}
return values;
}

高次幂会迅速放大数值,因此多项式展开后还要逐列标准化。统计量仍然只能来自训练集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function fitFeatureStats(train) {
const rows = train.map(point => rawFeatures(point, model));
const count = rows[0].length;
const means = Array.from(
{ length: count },
(_, j) => rows.reduce((sum, row) => sum + row[j], 0) / rows.length
);
const sds = means.map((mean, j) =>
Math.sqrt(
rows.reduce((sum, row) => sum + (row[j] - mean) ** 2, 0) / rows.length
) || 1
);
return { means, sds };
}

function features(point) {
const raw = rawFeatures(point, model);
const scaled = raw.map(
(value, j) => (value - featureStats.means[j]) / featureStats.sds[j]
);
return [1, ...scaled]; // 索引 0 固定为 1,对应偏置 b。
}

标准化很重要,因为 L2 惩罚的是权重数值。如果不同特征量纲相差很大,单纯比较权重大小或使用同一个 就不公平。

4. 稳定地计算 Sigmoid

直接计算 Math.exp(-z) 时,极端的 可能造成数值溢出。可以根据正负分支计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function sigmoid(z) {
if (z >= 0) {
return 1 / (1 + Math.exp(-Math.min(z, 40)));
}
const expZ = Math.exp(Math.max(z, -40));
return expZ / (1 + expZ);
}

function dot(a, b) {
return a.reduce((sum, value, i) => sum + value * b[i], 0);
}

function predictProbability(point) {
return sigmoid(dot(weights, features(point)));
}

5. 交叉熵和 L2 正则项

程序真正优化的目标为:

交叉熵 正则项

代码中把概率限制在安全范围,避免出现 log(0):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function evaluate(dataset, includeRegularization) {
let loss = 0;
let correct = 0;

for (const point of dataset) {
const p = sigmoid(dot(weights, features(point)));
const safeP = Math.min(1 - 1e-12, Math.max(1e-12, p));
loss += -(point.y * Math.log(safeP)
+ (1 - point.y) * Math.log(1 - safeP));
correct += (p >= 0.5 ? 1 : 0) === point.y ? 1 : 0;
}

loss /= dataset.length;

if (includeRegularization) {
// slice(1) 排除了索引 0 的偏置 b。
const squaredNorm = weights.slice(1)
.reduce((sum, w) => sum + w * w, 0);
loss += lambda * squaredNorm / (2 * train.length);
}

return { loss, accuracy: correct / dataset.length };
}

页面显示的训练损失和测试损失都不包含 L2 项,方便公平比较;梯度下降和早停判断使用的训练目标则包含 L2 项。

6. 正则化逻辑回归最核心的梯度更新

对单个权重 ,数据项的梯度为:

L2 项的导数为:

合并后对应下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function oneUpdate(batch) {
const gradient = new Array(weights.length).fill(0);

// 1. 计算当前批次的交叉熵梯度。
for (const point of batch) {
const x = features(point);
const probability = sigmoid(dot(weights, x));
const error = probability - point.y;

for (let j = 0; j < gradient.length; j++) {
gradient[j] += error * x[j] / batch.length;
}
}

// 2. 加入 L2 梯度。j 从 1 开始,因此偏置 weights[0] 不被惩罚。
for (let j = 1; j < gradient.length; j++) {
gradient[j] += (lambda / train.length) * weights[j];
}

// 3. 所有参数使用同一轮旧梯度同时更新。
for (let j = 0; j < weights.length; j++) {
weights[j] -= learningRate * gradient[j];
}
}

这是整个正则化逻辑回归实现中最重要的一段。可以把每一项理解为:

1
新权重 = 旧权重 - 学习率 ×(分类误差产生的梯度 + 正则化产生的梯度)

其中正则梯度 总是把非零权重往 0 的方向拉。 越大,拉回去的力量越强。

为什么偏置不正则化?

偏置 只负责整体移动边界,不表示模型对某个输入特征的依赖程度。通常只惩罚 ,因此代码中的正则循环从 j = 1 开始。

批量、小批量和随机梯度的区别

三种方法使用同一个 oneUpdate(),区别只是传入的 batch 大小:

1
2
3
if (optimizer === "batch") batchSize = train.length;
if (optimizer === "mini") batchSize = Math.min(16, train.length);
if (optimizer === "sgd") batchSize = 1;
  • 批量梯度:每次使用全部训练样本,轨迹稳定;
  • 小批量梯度:计算效率与稳定性之间的折中;
  • 随机梯度:每次只看一个样本,轨迹抖动更明显。

7. 训练轮数和早停

epoch 不能直接等同于更新次数。一次批量更新等于 1 epoch,但随机梯度需要看到全部 个样本才等于 1 epoch:

1
2
3
function currentEpoch() {
return samplesSeen / train.length;
}

普通实验会开启早停:至少训练 10 epoch,若连续 10 epoch 没有超过 1e-4 的目标函数改善,就停止训练。专门观察过拟合时应关闭早停,否则模型可能在测试损失明显回升之前就停下。

8. 不能只看边界判断过拟合

实验中同时显示四类证据:

  1. 最强信号: 训练损失继续下降,但测试损失开始上升;
  2. 泛化差距: 训练准确率明显高于测试准确率;
  3. 比较指标: 在相同数据和特征尺度下,权重范数 变得很大;
  4. 辅助证据: 边界出现不必要的弯曲。

边界弯曲本身不等于过拟合,因为真实规律也可能是弯曲的。准确率也可能不敏感:预测从 0.55 变成错误且自信的 0.99 时,准确率都只记作一次错误,但交叉熵会显著增大。因此应优先观察训练/测试损失是否分叉。

9. 推荐的对照实验

实验 样本 特征 标签翻转 早停 重点观察
欠拟合 160 线性 0.1 0% 开 直线无法还原真实曲线
合理模型 160 二次 0.1 0% 开 训练与测试指标接近
过度正则化 160 二次 10 0% 开 权重过小,模型重新欠拟合
过拟合演示 80 六次 0 12% 关 训练/测试损失逐渐分叉

比较 时应保持数据种子、模型特征、学习率和训练轮数一致,一次只改变一个变量。

10. 直接在文章中运行实验

如果当前阅读器没有显示嵌入页面,可以使用下面的入口: