☰
逻辑回归二分类Matlab实战:从原理到代码完整实现
2026/10/14 8:13:26 网站建设 项目流程

逻辑回归二分类这种需求,在Matlab里做起来比很多人想象的要顺手得多。我个人这几年处理过不少类似的问题:像用户流失预测、工业设备故障诊断、医疗筛查这类“是/否”判断,第一反应往往就是逻辑回归,因为它简单、可解释性强,而且只要数据不是太离谱,效果通常都不差。这篇文章我会把从原理到代码再到踩坑的完整过程写出来,包括手写梯度下降和直接调用内置函数两条路线,以及我在实际调参中总结的一些经验。不管你是刚开始接触机器学习,还是已经用Matlab跑过一些简单分类任务,这篇内容应该都能给你一个能直接落地的参考。

1. 逻辑回归二分类的核心思路拆解

很多刚开始做分类的朋友容易把逻辑回归想复杂,其实它的本质就是“在线性回归外面套了一层概率转换函数”。我们拿到一组特征和对应的标签,标签只有0和1(比如“坏账”和“正常”),逻辑回归要做的就是找到一组权重,让特征的线性组合经过sigmoid函数之后,能够尽量接近真实的标签概率。

1.1 为什么线性回归不能直接做分类

这里有一个新手很容易踩的认知误区:既然逻辑回归是在线性回归基础上加了个sigmoid,那为什么不直接用线性回归拟合标签?原因是线性回归的输出是连续值,可以跑到0以下、1以上,这没法直接解释为概率。更重要的是,训练时线性回归用的是均方误差,对分类任务来说,这个损失函数会让决策边界被离群点严重带偏。

我举个例子,假设我们预测某个设备是否故障,样本里绝大多数正常状态下特征值是1到3之间,有一个故障样本特征值是20。线性回归为了压低这个样本的误差,会把整个决策边界往正常样本那边拉,结果很多正常的样本反而被判为故障。逻辑回归的sigmoid函数把线性输出压缩到0到1之间,配合交叉熵损失,对这类“极端点”的惩罚更合理,决策边界鲁棒性会好很多。

1.2 sigmoid函数与概率解释

逻辑回归的核心公式很简单:

[ z = w^T x + b ] [ p = \frac{1}{1 + e^{-z}} ]

p表示样本属于类别1的概率。当z趋向正无穷,p趋向1;z趋向负无穷,p趋向0;z=0时,p=0.5,这就是决策边界。我们把p大于等于0.5判定为类别1,小于0.5判定为类别0,这就完成了二分类。

这里有一个很实用的细节值得记住:sigmoid输出不仅仅是分类结果,它还是置信度。比如p=0.9和p=0.6虽然都被判定为类别1,但置信度差异很大。在实际业务里,我们通常不是只看最终的0/1,还会看p值排序,这样可以用阈值调整来控制误报率。我自己在做一个风控类的模拟项目时,就经常把阈值从0.5调高到0.7,就是为了优先保证“别把正常用户误判成风险用户”。

1.3 损失函数与梯度下降推导

逻辑回归用的损失函数是二元交叉熵:

[ Loss = -\frac{1}{N} \sum_{i=1}^{N} [y_i \log(p_i) + (1-y_i) \log(1-p_i)] ]

为什么用这个而不是均方误差?因为逻辑回归加sigmoid后,如果继续用均方误差,损失函数关于权重是非凸的,梯度下降很容易陷入局部最优。交叉熵则保证了损失函数是凸函数,理论上能找到全局最优解。

对权重w求梯度,最终的梯度表达式非常优美:

[ \frac{\partial Loss}{\partial w} = \frac{1}{N} \sum_{i=1}^{N} (p_i - y_i) x_i ]

也就是说,梯度等于预测概率与真实标签的差乘以特征值。这个形式在向量化实现里特别方便,一行矩阵运算就能搞定,这也是为什么Matlab里实现逻辑回归非常简洁。

2. 数据准备与预处理环节

在写训练代码之前,数据预处理往往决定了最终效果的上限。很多人在这个环节偷懒,后面再怎么调参都是白费功夫。我见过太多实际案例:特征量纲差了三个数量级,梯度下降要么收敛极慢,要么直接震荡发散,最后还怪算法不行。

2.1 特征缩放的重要性

如果某个特征的数值范围是[0, 1000],另一个是[0, 0.001],那么梯度下降时,大数值特征对应的权重更新幅度会非常大,小数值特征的权重几乎不动。这会让优化过程变成“之”字形路线,收敛速度极慢。

我常用的方法是Z-score标准化:

[ x_{new} = \frac{x - \mu}{\sigma} ]

在Matlab里可以这样写:

mu = mean(X_train); sigma = std(X_train); X_train_norm = (X_train - mu) ./ sigma; X_test_norm = (X_test - mu) ./ sigma;

注意一个关键点:标准化参数必须只从训练集计算,然后复用同样的mu和sigma去变换测试集。千万不要把训练集和测试集混在一起做标准化,这样会造成数据泄漏,评估指标虚高。实际工程中如果把预处理写成一个函数,这个坑会好避免很多。

2.2 训练集与测试集划分

划分数据的时候除了随机打乱,还有一个容易忽略的问题:标签类别比例。如果原始数据里类别0有900个,类别1有100个,随机划分后分配到测试集里的类别1可能只剩20个,这样评估出来的召回率或精确率的置信度会很低。

一种稳妥做法是分层采样,保证训练集和测试集里正负样本比例与整体一致。Matlab里可以用cvpartition实现:

cv = cvpartition(y, 'HoldOut', 0.2); idxTrain = training(cv); idxTest = test(cv); X_train = X(idxTrain, :); y_train = y(idxTrain); X_test = X(idxTest, :); y_test = y(idxTest);

cvpartition默认就会保持类别比例,这也是我通常在分类任务里的首选。

2.3 处理类别不平衡问题

如果你的正负样本比例严重失衡,比如正样本只占5%,那么逻辑回归直接训练出来的模型会倾向于把所有样本都预测为负类,因为这样整体准确率也能到95%。解决办法主要有几种:

  • 对少数类样本过采样,比如SMOTE算法;
  • 对多数类样本欠采样;
  • 在损失函数里给不同类别加权。

损失函数加权在Matlab里可以直接通过调整样本权重实现,相当于把少数类每个样本的loss乘以一个系数。我在实际项目里更倾向于先试权重加权,因为它不改变样本分布,计算代价小。如果效果还不够,再考虑SMOTE。

3. 手写逻辑回归的完整Matlab实现

从零实现一遍逻辑回归,能让你对梯度下降和向量化有更直观的理解。而且手写代码调试起来非常方便,很多内置函数黑箱里的问题,在你亲手实现过一次之后都会迎刃而解。

3.1 主函数设计

我会把代码分成几个模块:数据加载与预处理、模型训练、模型预测、评估。这样结构清晰,方便复用。下面是训练函数的核心实现,我直接给出一个可以跑的版本:

function [w, b, loss_history] = train_logistic(X, y, lr, num_iter) % X: m x n 特征矩阵(已标准化) % y: m x 1 标签(0或1) % lr: 学习率 % num_iter: 迭代次数 [m, n] = size(X); w = zeros(n, 1); b = 0; loss_history = zeros(num_iter, 1); for iter = 1:num_iter z = X * w + b; p = 1 ./ (1 + exp(-z)); % 防止log(0)导致NaN,加一个小epsilon eps = 1e-12; loss = -mean(y .* log(p + eps) + (1 - y) .* log(1 - p + eps)); loss_history(iter) = loss; % 梯度计算 dw = (X' * (p - y)) / m; db = mean(p - y); % 参数更新 w = w - lr * dw; b = b - lr * db; end end

这个代码的关键在于向量化:X * w + b一次算出了所有样本的线性输出,exp(-z)直接得到所有样本的sigmoid值,梯度里X' * (p - y)就是矩阵转置乘误差向量。这些操作充分利用了Matlab的矩阵运算能力,跑起来比for循环逐样本计算要快一两个数量级。

3.2 梯度下降的两种变体及其取舍

上面用的是批量梯度下降,每次迭代用全部样本来计算梯度。优点是梯度方向稳定,损失曲线平滑;缺点是当数据集特别大时,每次迭代的计算开销很高。

实际操作中我还会用随机梯度下降或者小批量梯度下降。小批量就是从训练集里随机抽一小部分样本(比如64或128个)来计算梯度,这样每轮迭代更快,还能引入一些随机性,帮助跳出局部极小。如果数据集不到几万条,批量梯度下降完全够用,代码也简单。数据量到了百万级,再考虑小批量不迟。

3.3 学习率与迭代次数的调参经验

学习率的选择是手写逻辑回归最考验经验的地方。我踩过不少坑,总结出几个速查经验:

  • 学习率太大,比如lr = 1,损失函数会震荡甚至发散,表现为loss_history出现NaN或忽高忽低。
  • 学习率太小,比如lr = 0.0001,收敛极慢,迭代几千次损失还降不下去。
  • 标准化之后,lr = 0.1是一个比较稳妥的起点;如果数据维度很高,可以降到0.01试试。

迭代次数不要一开始就固定,我习惯先设置num_iter = 500,画出损失曲线,观察损失是否已经平缓。如果迭代结束时损失还在明显下降,就增加迭代次数;如果很早就不变了,说明学习率可以适当调大一点。

还有一个技巧:在梯度下降过程中保存每一轮的损失,如果发现损失在连续50轮内下降小于1e-5,可以提前终止训练。这样既能节省时间,也能避免过拟合。

3.4 预测与决策边界可视化

训练完成后,预测函数很直接:

function [y_pred, p_pred] = predict_logistic(w, b, X) p_pred = 1 ./ (1 + exp(-(X * w + b))); y_pred = double(p_pred >= 0.5); end

如果特征只有二维,我通常会画决策边界图,直观判断分类效果。画法是用meshgrid生成网格点,对每个网格点计算预测概率,然后用contour画出p=0.5的等高线。下面这段代码可以直接用:

[x1_grid, x2_grid] = meshgrid(linspace(min(X(:,1)), max(X(:,1)), 100), ... linspace(min(X(:,2)), max(X(:,2)), 100)); grid_features = [x1_grid(:), x2_grid(:)]; p_grid = 1 ./ (1 + exp(-(grid_features * w + b))); p_grid = reshape(p_grid, size(x1_grid)); figure; scatter(X(y==1,1), X(y==1,2), 'r', 'filled'); hold on; scatter(X(y==0,1), X(y==0,2), 'b', 'filled'); contour(x1_grid, x2_grid, p_grid, [0.5 0.5], 'k', 'LineWidth', 2); xlabel('特征1'); ylabel('特征2'); legend('类别1', '类别0', '决策边界');

画出的决策边界如果是平滑的曲线,说明模型学习到的线性边界与数据分布匹配;如果出现剧烈扭曲,大概率是数据标准化没做好,或者特征之间存在多重共线性。

4. 用Matlab内置函数快速实现与正则化

如果只是想快速得到结果,不打算手工调试梯度下降,Matlab内置函数是更好的选择。我平时做原型验证的时候,也经常先跑内置函数,确认数据没问题后再根据需求决定要不要手写。

4.1fitglm实现二分类逻辑回归

fitglm是广义线性模型的拟合函数,逻辑回归就是其中一种。用法:

% X_train为标准化的特征矩阵,y_train为0/1标签 mdl = fitglm(X_train, y_train, 'Distribution', 'binomial', 'Link', 'logit');

训练完成后,可以直接用predict函数得到预测值。

y_pred = predict(mdl, X_test);

这里y_pred得到的是概率值,需要自己设置阈值转成0/1标签。fitglm的好处是返回的模型对象里包含了系数估计、标准误、p值等统计信息,这在做特征筛选时特别有用。

4.2 正则化:L1与L2的取舍

当特征数量比较多的时候,逻辑回归容易过拟合。解决办法是加正则化项。手写代码时,梯度里需要加上正则化项的导数。比如L2正则化,损失函数变为:

[ Loss_{reg} = Loss + \lambda \sum_{j} w_j^2 ]

梯度变为:

[ \frac{\partial Loss_{reg}}{\partial w} = \frac{1}{N} X^T(p - y) + 2\lambda w ]

L1正则化则是加λ * sum(abs(w)),梯度里对w取符号函数。L1的好处是能够把权重稀疏化,让一部分特征的权重变成0,相当于自动特征选择。如果业务上需要解释模型,或者特征特别多,优先考虑L1;如果只是控制过拟合,L2更稳定。

在Matlab里,fitglm本身不自带正则化参数,但我们可以通过lasso函数结合逻辑回归来实现,或者直接使用fitclinear,这个函数专门支持高维数据的线性分类模型,并且内置L1/L2正则化。

mdl = fitclinear(X_train, y_train, 'Learner', 'logistic', 'Regularization', 'lasso');

4.3 与SVM、决策树等模型对比的选型参考

逻辑回归不是万能的。它适合特征与标签之间存在近似线性关系的场景。如果数据本身决策边界是高度非线性的,逻辑回归的效果就会比较差。我通常在项目里做个快速基线对比:

  • 先用逻辑回归跑一遍,得到基础准确率;
  • 再用SVM或随机森林跑一遍;
  • 如果后两者明显优于逻辑回归,说明数据非线性程度高,优先考虑非线性模型。

但注意,逻辑回归的“可解释性”优势是SVM和随机森林很难替代的。在医疗、金融这些需要向业务方解释“为什么判定为1”的场景,逻辑回归的权重系数可以直接说明每个特征的影响方向和强度。我自己在做一个模拟信用评估项目时,就是靠逻辑回归的系数分析,找出了两个强相关的风险特征,后来才进一步优化特征工程,整体效果提升明显。

5. 常见问题与排查技巧实录

这部分我要重点展开,因为实际项目里绝大多数时间都花在调试上。下面几个问题是我自己和身边朋友在跑逻辑回归时反复遇到的。

5.1 损失函数不收敛或者出现NaN

这是一个高频问题。现象是训练过程中loss_history里出现NaN,或者损失曲线一直剧烈震荡。排查顺序如下:

  1. 检查数据是否标准化。没有标准化的数据配上一个稍大的学习率,很容易让梯度瞬间爆炸。
  2. 检查学习率是否过大。如果loss在某一步突然变成NaN,大概率是学习率太大了,参数更新跨过了“悬崖”。试着把学习率除以10。
  3. 检查标签是否为0/1。如果标签里有2或者其他数值,sigmoid输出永远无法拟合,损失会一直很高。
  4. 检查特征矩阵是否存在缺失值或无穷值。用sum(isnan(X))和sum(isinf(X))快速排查。

另外,在我写的代码里加了eps = 1e-12防止log(0),这一步在Matlab里很重要。如果没有这个保护,当p非常接近0或1时,log会得到-Inf,梯度计算就崩了。

5.2 预测结果全为0或全为1

模型把所有样本都判成同一个类别,通常是类别极不平衡,或者决策边界根本没有学出来。这时候先看训练集上的准确率:

  • 如果训练集上也是全预测一个类,说明模型没有学到有效信号。检查特征是否有用、有没有标准化、学习率是不是设太小导致收敛到很差的局部解。
  • 如果训练集表现正常,测试集全预测一个类,说明测试集和训练集分布差异太大,或者划分数据时出了问题,比如不同类别的样本被错误打乱。

从概率角度说,如果训练集里正样本只有5%,默认的0.5阈值很少输出正类。这时候可以先不设阈值,直接看预测概率的分布,然后根据业务场景调整阈值。之前我做一个设备故障分类的项目,正样本只有3%,我把阈值调低到0.15,然后在验证集上重新评估精确率和召回率,最终找到了一个可用的平衡点。

5.3 过拟合与欠拟合的表现

过拟合的典型表现是训练准确率接近100%,但测试准确率明显下降。逻辑回归模型复杂度不高,但如果特征维度非常大,同样会过拟合。

解决方法按优先级排序:

  1. 增加训练数据量;
  2. 添加L2正则化,约束权重幅度;
  3. 如果特征线性相关严重,用PCA降维;
  4. 用交叉验证选择最优的正则化系数λ。

欠拟合则表现为训练集和测试集的准确率都比较低,说明模型容量不够。这时候可以添加多项式特征,比如将x1^2、x1*x2加入特征集,让决策边界变成曲线。

5.4 特征共线性对系数的影响

逻辑回归的系数解释依赖于特征的独立性。如果两个特征高度相关,它们的权重系数可能会被分配到任意一个特征上,导致系数不稳定。一个简单判断方法:训练完成后,多次打乱数据重新训练,如果权重系数波动很大,很可能存在共线性问题。

解决办法是移除相关特征,或者用PCA提取主成分后再训练。我这里特别提醒一句,不要只看系数的绝对值大小来判断特征重要性,因为不同特征的量纲不同,系数可比性很差。正确做法是使用标准化后的特征训练,或者计算每个特征的置信区间。

6. 一个完整实战案例复盘

光讲原理可能还不够,我拿一个我自己在练习中反复打磨过的数据集来做一遍完整复盘。这个模拟项目的目标是基于两个特征判断某组件是否合格,数据量不大,总共2000个样本,标签是0或1,两个特征分别表示尺寸偏差和表面粗糙度。这个例子足够小,可以完整展示整个流程,而且任何人不需要额外数据源就能理解。

6.1 数据加载与预处理

我用随机生成的方式构造数据,其中两个类别大致服从不同的分布。生成之后先看数据的基本情况:

load sample_data.mat; % 假设数据结构为X(2000x2), y(2000x1) disp(mean(y)); % 查看正样本比例

如果正样本比例在0.3到0.7之间,算是比较均衡的情况。接下来做标准化和划分:

cv = cvpartition(y, 'HoldOut', 0.2); X_norm = (X - mean(X(cv.training(1), :))) ./ std(X(cv.training(1), :)); X_train = X_norm(cv.training, :); y_train = y(cv.training); X_test = X_norm(cv.test, :); y_test = y(cv.test);

这里用mean(X(cv.training(1), :))不太对,应该分别取训练集索引。实际上可以这样写:

idxTrain = training(cv); idxTest = test(cv); mu = mean(X(idxTrain, :)); sigma = std(X(idxTrain, :)); X_train = (X(idxTrain, :) - mu) ./ sigma; X_test = (X(idxTest, :) - mu) ./ sigma;

注意这里我特意强调要用训练集的mu和sigma来变换测试集,这样才符合真实场景中“模型没有见过测试集”的逻辑。

6.2 训练与调参过程

我先用手写版本训练,学习率设为0.1,迭代500次,记录损失曲线。第一次跑完,看到损失从0.68下降到0.31,已经比较平滑。测试集准确率达0.87,说明线性决策边界基本可用。

不过我发现特征2对分类的作用比特征1更明显,为了进一步验证,我尝试去掉特征1后再训练。结果准确率掉到了0.82,说明特征1虽然单独看不强,但与特征2组合后提供了互补信息。这也提醒我:不要单独靠单特征相关性来筛选特征,至少要看成对特征组合。

然后我用fitglm跑一遍,输出结果,发现两个特征的p值都小于0.001,说明都是统计显著的。这个结果让我对手写代码实现的效果心里更有底了。

6.3 评估指标的选择

准确率不是一个适合类别不平衡问题的指标。更可靠的指标包括精确率、召回率、F1分数和AUC。我专门写了一个简单的评估函数:

function [prec, rec, f1, auc] = eval_metrics(y_true, p_pred) % 用不同阈值计算精确率和召回率 thresh = 0.5; y_pred = double(p_pred >= thresh); tp = sum(y_pred == 1 & y_true == 1); fp = sum(y_pred == 1 & y_true == 0); fn = sum(y_pred == 0 & y_true == 1); prec = tp / (tp + fp); rec = tp / (tp + fn); f1 = 2 * prec * rec / (prec + rec); % AUC计算:使用Matlab的perfcurve [~, ~, ~, auc] = perfcurve(y_true, p_pred, 1); end

在实际案例中,阈值0.5下精确率为0.85,召回率为0.90,F1是0.87,AUC为0.93。如果我把阈值提高到0.7,精确率提升到0.92,但召回率掉到0.7。业务场景如果更在意“宁可错杀不可放过”,就选0.3阈值;如果更在意“别冤枉好人”,就选0.7阈值。逻辑回归的价值就在这里,你可以通过调整阈值灵活平衡业务风险,而不需要重新训练模型。

7. 经验总结与后续扩展建议

说实话,逻辑回归在Matlab里的实现难度并不高,真正的难点是理解数据、理解评估指标、理解调参背后的意义。我最后一次分享几个非常实用的经验。

第一个经验:先写一个简单的训练函数,哪怕你最终打算用fitglm,也值得先手写一遍。因为手写过程会强迫你理解梯度、损失、标准化这些概念,后面用任何模型框架都能更快定位问题。我在这个案例里就是先手写跑通,然后才对照内置函数的结果,一旦结果不一致,我能很快推断是数据处理、初始化还是梯度方向的问题。

第二个经验:损失曲线的价值远超你的想象。不要只盯着最终准确率,训练时保存损失序列,画出来看它的形态。正常情况是平滑下降,最后趋于平缓。如果看到锯齿状震荡,通常是学习率偏大;如果下降速度持续很慢,可能是特征没有标准化,也可能是学习率太小。记录损失曲线这个习惯,能帮你省下大量盲调的时间。

第三个经验:不要迷信准确率,业务场景决定评估指标。逻辑回归给的预测概率是可以“加工”的,阈值、权重、分类成本都可以作为调参维度。在工业异常检测这种场景,正样本极稀少,我一般会同时报告0.5阈值下的F1和不同阈值下的AUC,给业务方一个完整的模型表现画像,而不是只丢一个准确率数字。

后续可以扩展的方向也很多:把特征扩展成多项式,加入交互项,让线性模型变成广义线性模型;把单个逻辑回归做成多个模型的集成;甚至把它作为深度学习网络的输出层。但所有这些扩展都建立在同一个扎实基础上:你已经掌握了逻辑回归二分类的原理、Matlab实现和调优方法,这会是你后面所有分类任务的底层能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询