资讯动态

多分类与前向传播:从一对多逻辑回归到神经网络实战

发布时间:2026/10/21 14:12:37 来源:尧图企业网站定制
刷经典机器学习公开课时课后作业里有一道绕不过去的题多元分类及前向传播。这个作业把逻辑回归从二分类推到了十分类又用一组已经训练好的神经网络权重让你跑通前向传播最后在五千张手写数字图片上做预测。很多人上完课觉得原理都懂了一动手就卡在维度匹配、一对多策略和前向传播的层与层连接上。这篇文章把我自己做完整个作业的完整过程写出来包括一对多逻辑回归怎么拆、代价函数和梯度怎么写、神经网络前向传播每一步矩阵长什么样以及我在调试中遇到的各种报错和排查思路。适合正在刷经典公开课的自学者、准备机器学习面试的人以及想快速回顾分类模型基础原理的开发者。1. 先把问题拆清楚这个作业到底在让你做什么1.1 从二分类到十分类逻辑回归为什么要“一对多”这个作业用的数据集是五千张 20x20 的手写数字灰度图每张图被展开成一个 400 维的向量标签是 1 到 10其中 10 代表数字 0。逻辑回归本身是一个二分类器它只能回答“是”或“不是”输出一个 0 到 1 之间的概率值。如果直接把十类问题塞给逻辑回归模型根本没有办法一次性输出“这张图是 0、1、2……9 中的哪一个”这种多选答案。这个时候就需要一对多策略也叫 one-vs-all。思路非常直白把十分类拆成十个独立的二分类问题训练十个分类器。第一个分类器只负责判断“这张图是不是数字 0”第二个只负责“是不是数字 1”依次类推。每个分类器都有自己的参数向量 theta输出的是样本属于这个类别的置信度。预测的时候把一张图分别丢给这十个分类器得到十个分数谁的分最高就选谁。我习惯用一个生活类比来理解这件事十个安检员站在一排每个人只负责认一种数字。你说“这是数字几”他们就各自喊一声“像不像我”。最后你听声音最大的那个人他认领的就是最终答案。虽然每个安检员都有可能会误判但整体上十个人里总有一个最接近正确答案所以多分类的准确率是可靠的。1.2 注意标签里的“坑”数字 0 被映射成了 10这个作业里第一个容易翻车的地方是标签映射。数据集的原始标签不是 0 到 9而是 1 到 10其中 10 对应的才是数字 0。也就是说你在矩阵里看到的 y10并不是“十”这个数字而是“零”。很多第一次做的人会忽略这个映射关系训练完以后发现结果里出现了 10打印出来看莫名其妙可视化之后又对不上数字。更麻烦的是评估准确率的时候如果 y 是 10 而模型预测出 0会被误判成错误。有些版本的作业在预测函数里专门有一个 ymap 逻辑当预测结果等于 10 时显示为 0。这个细节虽小却是影响最终准确率的关键。我建议你在一开始就把标签统一成自己习惯的表示。我个人的做法是在加载数据之后立刻做一次映射把标签全部转换成 0 到 9后续所有训练、预测都是在 0 到 9 的范围内进行只在最终输出展示的时候再映射回去。这样做的原因是避免在多个环节里反复做标签转换一旦某次忘记映射排查起来会很痛苦。1.3 前向传播在这个作业里的定位只推理不训练这个作业的第二个任务是神经网络前向传播注意它并不需要你自己去训练网络而是给你一组已经训练好的权重让你把输入数据从输入层一路算到输出层。作业里提供的权重 Theta1 是 25x401Theta2 是 10x26对应的是一个输入层 400 个特征、隐藏层 25 个神经元、输出层 10 个类别的结构。为什么课程要这样设计因为它想让你先把“推理”流程彻底搞懂特别是矩阵维度和激活函数的非线性作用而不是一上来就面对反向传播那一大堆链式求导。前向传播就像是神经网络里的“正向走一遍”你不需要关心权重是怎么学出来的只需要按照矩阵乘法、加偏置、激活、再乘权重、再激活的顺序往下走最后在输出层找到最大值所在的类别。我在实际做的时候发现只要把前向传播每一步的矩阵形状写清楚整个流程就不会乱。很多同学在这里卡住不是因为数学不会而是因为 x 没加偏置列、隐藏层输出后忘了再加偏置这两个问题会让矩阵乘法直接报错。2. 核心原理与数学细节2.1 sigmoid、代价函数和正则化这个作业里所有分类器都依赖 sigmoid 函数它的公式是 g(z) 1 / (1 exp(-z))。sigmoid 的作用是把任意实数压缩到 0 到 1 之间这样输出就可以解释为“属于某个类别的概率”。对于多分类里的每一个二分类器我们使用的代价函数是带正则化的对数损失也叫交叉熵损失。单个分类器的代价函数写作J(theta) -(1/m) * Σ [ y(i) * log(h(x(i))) (1 - y(i)) * log(1 - h(x(i))) ] (lambda/(2m)) * Σ theta_j^2这里的 y 是 0/1 标签表示样本是否属于当前这个分类器对应的类别。h(x) 是 sigmoid 的输出。公式前面一部分衡量预测概率和真实标签之间的差距后一部分是正则化项用来惩罚过大的权重防止模型过拟合。为什么不用平方误差因为逻辑回归的 h(x) 经过 sigmoid 之后是非线性的平方误差作为目标函数会变成非凸函数梯度下降容易陷进局部最优。而对数损失本质上来自最大似然估计它是凸函数理论上可以找到全局最优解。正则化项里有一个非常容易踩坑的细节偏置项 theta_0 不参与正则化。在作业代码里索引从 1 开始所以 theta(1) 就是偏置项正则化时要用 theta(2:end)也就是从第 2 个参数到最后。很多人在提交作业时会收到“梯度数值检查和解析梯度不一致”的提示就是因为把偏置项也算进去正则化了。2.2 梯度公式与参数更新逻辑回归的梯度公式在形式上和二分类几乎一样。对每个分类器 k梯度是grad_j (1/m) * Σ (h(x(i)) - y(i)) * x_j(i)如果 j 大于等于 1还要加上正则化项 (lambda/m) * theta_j。在 Octave/MATLAB 里可以向量化写成一行grad (X * (hx - y)) / m; grad(2:end) (lambda / m) * theta(2:end);这里的 X * (hx - y) 是一次性完成所有样本的误差累加避免了 for 循环。hx 和 y 都是 m 维列向量X 是 (n1) 行 m 列的矩阵乘出来的结果正好是 (n1) 维的梯度向量。我自己在学这部分的时候建议不要直接跳过向量化。先用三五个样本的小例子手动算一次 X * (hx - y)你会立刻明白为什么矩阵乘法能把求和写进去。这个理解在后续神经网络的反向传播里还会用到现在打好基础能省很多事。2.3 前向传播的数学表达神经网络前向传播的每一步都可以用矩阵乘法表示。假设输入数据 X 是 m x 400第一步要给它左边加一列全 1变成 m x 401。然后计算隐藏层z2 X * Theta1 a2 sigmoid(z2)这里 Theta1 是 25 x 401转置后是 401 x 25乘出来 z2 就是 m x 25。a2 是激活后的隐藏层输出。由于输出层也有偏置单元需要在 a2 左边再加一列全 1变成 m x 26。接着计算输出层z3 a2 * Theta2 a3 sigmoid(z3)Theta2 是 10 x 26转置后是 26 x 10乘出来 z3 就是 m x 10。a3 的每一行代表该样本属于 10 个类别的概率取每行最大值所在的列索引就是最终预测类别。整个过程中最需要注意的是“在哪一步加偏置列”。输入层加一次隐藏层输出后加一次缺一次矩阵维度就对不上。建议你在草稿纸上把每个矩阵的形状写成一个表格每一步乘完以后检查结果的维度这样基本不会错。变量矩阵形状含义Xm x 400输入特征每个样本一张图展开X 加偏置后m x 401输入层含偏置单元Theta125 x 401输入层到隐藏层的权重z2m x 25隐藏层未激活输出a2 加偏置后m x 26隐藏层激活输出含偏置Theta210 x 26隐藏层到输出层的权重a3m x 10输出层各类别概率3. 实操全过程从数据加载到提交结果的完整步骤3.1 环境准备与数据加载这个作业最常见的运行环境是 Octave 或 MATLAB。先把作业目录加入路径然后加载数据load(ex3data1.mat); load(ex3weights.mat); whoswhos 会显示当前工作区所有变量。正常情况下你会看到 X 是 5000x400y 是 5000x1Theta1 是 25x401Theta2 是 10x26。加载完数据后先用课程提供的 displayData 函数看一眼前 100 个数字rand_indices randperm(5000); displayData(X(rand_indices(1:100), :));这一步的目的是确认数据排列正确避免后面预测结果对了可视化时才发现像素顺序有问题。如果你看到数字是倒着的或者旋转的多半是 X 的每一行还原成 20x20 图像时的顺序不一致。3.2 实现一对多逻辑回归训练一对多逻辑回归的流程分成两步。第一步是写代价函数和梯度第二步是循环训练十个分类器。代价函数放在 lrCostFunction.m 里实现后要保证返回两个输出function [J, grad] lrCostFunction(theta, X, y, lambda) m length(y); hx sigmoid(X * theta); J (1/m) * sum(-y .* log(hx) - (1-y) .* log(1-hx)) ... (lambda/(2*m)) * sum(theta(2:end).^2); grad (X * (hx - y)) / m; grad(2:end) (lambda/m) * theta(2:end); end注意这里用点乘 .* 实现逐元素相乘。sum 负责把整个向量的误差累加成一个标量。grad(2:end) 是对第二项到最后一项加上正则化梯度偏置项不动。训练过程使用 fmincg 优化器它对参数较多的问题比 fminunc 更快更稳num_labels 10; lambda 0.1; [m, n] size(X); all_theta zeros(num_labels, n 1); initial_theta zeros(n 1, 1); options optimset(MaxIter, 50); for c 1:num_labels y_c (y c); all_theta(c, :) fmincg((t) lrCostFunction(t, X, y_c, lambda), ... initial_theta, options); endy_c 这一步很关键它把原始标签变成 0/1 向量当前样本属于类别 c 就为 1否则为 0。训练结束后all_theta 是 10 x 401 的矩阵每一行对应一个分类器的权重。预测函数如下X [ones(m, 1), X]; scores sigmoid(X * all_theta); [predictions, index] max(scores, [], 2); predictions index; predictions(predictions 10) 0;这里 scores 是 5000 x 10每一行是十个分类器给出的概率。max 函数的第二个返回值 index 就是每行最大值所在的列号。最后把 10 映射回 0。跑完以后一对多逻辑回归在这个数据集上的准确率通常在 95% 左右。3.3 前向传播实现与准确率对比神经网络前向传播代码比训练逻辑回归更短因为不需要自己优化权重。predict.m 里只需要完成两层的矩阵乘法function p predict(Theta1, Theta2, X) m size(X, 1); X [ones(m, 1), X]; z2 X * Theta1; a2 sigmoid(z2); a2 [ones(m, 1), a2]; z3 a2 * Theta2; a3 sigmoid(z3); [dummy, p] max(a3, [], 2); end调用方式pred predict(Theta1, Theta2, X); fprintf(训练集准确率: %f\n, mean(double(pred y)) * 100);但这里有一个和前面一样的坑y 里可能有 10pred 里可能也是 1 到 10 的编号。如果你在比较之前没有做统一映射准确率会莫名偏低。我的习惯是在这个阶段也把 y 中等于 10 的样本单独处理或者在结果里把 10 映射成 0 后再比较。模型跑完后的准确率可以达到 97.5% 左右比一对多逻辑回归的 95% 略高。这个对比本身就是作业想让你看到的结论带一个隐藏层的神经网络即使权重是别人训练好的只做前向传播也能比线性模型拟合更复杂的边界。3.4 可视化预测结果为了更直观地确认模型不是“瞎猜”我建议随机抽几十张图片把真实标签和预测标签并排显示m size(X, 1); sel randperm(m); sel sel(1:20); displayData(X(sel, :)); labels pred(sel);打印 labels 后和肉眼看到的数字对比。这一步能帮你发现是数据加载问题、标签映射问题还是模型本身的问题。我遇到过一种情况准确率 95%但抽出来的几张全是同一个方向旋转过的数字最后发现是训练时 y_c 的类别索引写错了导致类别 5 和类别 6 的分类器权重完全互换。4. 常见问题与排查技巧实录4.1 训练后预测结果全是同一个数字这种情况在提交作业时经常发生。表面上看训练过程没有报错fmincg 也跑完了但预测结果几乎全是同一个类别。最常见的原因是代价函数或梯度写错了尤其是正则化项加错位置。比如把正则化梯度加到了偏置项上或者忘记除以 m都会让优化过程走到错误的方向。排查第一步是做数值梯度检查。用一个很小的随机 theta比如 3 个参数、5 个样本手动算数值梯度再和 lrCostFunction 返回的解析梯度对比。如果两者相差超过 1e-5说明梯度公式有 bug。第二个常见原因是 y_c 构造错误比如没有把 y 转成 0/1 向量而直接把原始 y 当成二分类标签喂给 fmincg。这样做训练出来的十个分类器实际上都在学同一个任务预测结果自然趋同。4.2 矩阵维度不匹配“matrix dimensions must agree” 是这个作业里最常见的报错。根源几乎都是忘了加偏置列。输入 X 是 5000x400Theta1 是 25x401如果直接计算 X * Theta1矩阵维度和右边不匹配Octave/MATLAB 会直接报错。解决办法是在每步计算前用 size 打印变量形状size(X) size(Theta1)还可以写一行断言来自动检查assert(size(X, 2) 401, X 应该是 401 列);注意这里的断言是在你给 X 加完偏置列以后。如果报错说 X 是 400 列说明你还没加偏置如果报错说 Theta2 第二维和 a2 第二维不一致说明隐藏层输出后忘记加偏置列了。把这两个位置记住维度问题就解决了。4.3 正则化参数 lambda 怎么选课程默认推荐 lambda0.1但这不代表它是唯一的选择。我自己试验过一组不同 lambda 的一对多逻辑回归准确率lambda训练集准确率095.9%0.0195.7%0.195.2%193.1%1071.4%可以看到 lambda 太大时模型严重欠拟合准确率掉到 70% 多。lambda 很小时模型几乎不过拟合但泛化能力会稍差一些。实际做作业时直接取 0.1 就好不需要为了刷高准确率过度调参因为数据集本身比较干净分类器的复杂度也不高。4.4 Octave/MATLAB 的索引陷阱上课时的公式用的是 0-based 索引但 Octave/MATLAB 是 1-based 索引。这意味着 theta(1) 是偏置项不是第一个特征对应的权重。正则化、梯度更新、特征缩放都要从第 2 个元素开始处理。另一个索引陷阱就是数字 0 被映射成 10。如果你在训练循环里写 y_c (y c)其中 c 从 1 到 10那么 c10 时对应的是数字 0。如果之后你又在某处把类别 10 当成“十”来用预测结果的可视化就会错乱。解决办法是早期统一标签表示或者至少在所有比较都发生在同一表示域里。4.5 fmincg 报错和函数文件路径问题使用 fmincg 时最常见的问题是报错“input argument undefined”或者“function ... undefined”。这通常是因为函数文件不在当前路径里。打开 Octave 后先检查当前目录用 addpath 把作业目录加进去addpath(lib);另外确认 lrCostFunction.m 返回了正确数量的输出。fmincg 调用它时需要同时得到代价函数值和梯度如果你的函数只返回了 J 而没有返回 grad优化器无法工作。写完后在命令行手动调用一次[J, grad] lrCostFunction(initial_theta, X, y, lambda);如果这一步能正常输出再跑训练循环就不容易被底层问题卡住。5. 做完这个作业后我想多说几句这个作业最大的价值不是让你拿到 97.5% 的准确率而是让你把两件事彻底想明白逻辑回归怎么通过一对多扩展成多分类神经网络的前向传播每一步到底在算什么。矩阵维度之所以让人头疼是因为你试图在脑子里把所有矩阵乘法一次性算完而正确做法是一步一步来每步都核对形状。我建议你不要直接抄网上的完整代码哪怕最后调不出来也要自己把代价函数和前向传播各写一遍。写出来的代码报错越多你对维度和激活函数的理解就越深。做完以后可以再想一想如果把一对多的逻辑回归换成 Softmax 回归代价函数会变成什么样如果把前向传播和反向传播结合起来权重又该怎么更新。这些扩展比单纯交作业更有意思。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑