
逻辑回归在Matlab里的落地实现网上能搜到的代码不少但大部分要么是教学demo要么数据格式写死、换份数据就得改半天。我自己这两年做数据分析经常要拿不同来源的数据快速跑一个二分类基线模型所以干脆写了一套可以直接拿来用的逻辑回归分类预测代码核心就两个诉求训练集个数可以自己定数据格式不用拘泥于某一种。这篇就把完整代码、设计思路、踩过的坑都整理出来给同样需要在Matlab里做快速分类验证的朋友做个参考。这套代码适合谁如果你手头有带标签的数据Excel、CSV、TXT都行想快速验证一下逻辑回归在这个数据上的分类效果或者想对比不同训练集规模对模型精度的影响那直接拿过去就能用。不需要你有深厚的机器学习理论基础代码注释我写得很详细参数含义都标清楚了。1. 项目整体设计与思路拆解1.1 需求拆解要解决的三个核心问题写这套代码之前我先把需求捋了一遍其实就三个问题。第一是训练集个数可自定义。很多时候我们手上数据总量是固定的但想看看“用前50条训练”和“用前500条训练”效果差多少或者想做简单的学习曲线分析。如果代码里训练集数量写死每次都要改代码太蠢了。所以我的设计是调用函数时直接传入一个trainNum参数代码自动按顺序取前N条做训练剩下的做测试。第二是数据格式兼容。这是最让人头疼的。不同来源的数据有的是Excel表格有的是逗号分隔的CSV有的是空格或Tab分隔的TXT最后一列是标签列。如果每换一种格式就重写一次读取逻辑那这套代码的价值就大打折扣。所以我把数据读取单独封装成一个函数内部自动判断文件格式和分隔符统一输出成Matlab矩阵。第三是结果可量化评估。跑完训练不能光输出一个“训练完成”得告诉用户准确率、预测结果对比最好还能把逻辑回归的权重系数打出来方便做特征重要性分析。这一块我加了一个完整的评价模块。1.2 为什么选逻辑回归而不是SVM或决策树逻辑回归听起来名字里带“回归”但它干的是分类的活儿而且是目前工业界用得最广泛的二分类模型之一。选它有几个非常现实的理由。首先是可解释性强训练完你能直接看到每个特征对应的权重系数正负号代表该特征是正向还是负向影响分类结果大小代表影响程度。这在很多需要向业务方解释模型的场景里是刚需SVM和神经网络给不出这么直观的答案。其次是训练速度快逻辑回归的损失函数是凸函数用梯度下降一定能收敛到全局最优解数据量几千条的时候在Matlab里跑几百轮迭代也就几秒钟的事。再有就是对数据分布要求宽松不像朴素贝叶斯要求特征独立也不像线性判别分析要求数据服从高斯分布。当然逻辑回归也有短板比如特征间强相关时权重估计会不稳定比如对非线性问题需要手动做特征工程。但作为基线模型它几乎是性价比最高的选择。1.3 为什么用Matlab而不是Python这个问题我被人问过很多次。我知道现在Python是机器学习的主流但Matlab在某些场景下依然有不可替代的优势。最直观的一点是语法贴近数学表达。逻辑回归的损失函数和梯度公式用Matlab写出来几乎和教材里的公式一一对应不容易出现实现和理论脱节的问题。比如梯度公式grad (1/m) * X * (h - y)这一行就是完整的梯度计算矩阵运算天然支持不需要像Python那样依赖numpy还得注意维度。第二是工具箱完善fminunc这种优化函数内置就能用不需要额外装包。第三是调试方便变量区直接可视化所有中间结果对初学者理解算法原理特别友好。当然如果你是要部署到生产环境或者做大规模的深度学习那另说。但就“快速验证分类效果”这个目标而言Matlab完全够用而且门槛更低。2. 逻辑回归的核心原理用大白话讲清楚2.1 从线性回归到概率输出sigmoid函数在干什么逻辑回归的原理说穿了就一层窗户纸它在线性回归的外面套了一层sigmoid函数把输出压缩到0到1之间变成一个概率。线性回归的输出是z θ1*x1 θ2*x2 ... θn*xn b这个值可以是任意实数从负无穷到正无穷。但我们做二分类想要的输出是“属于类别1的概率”这个值必须在0到1之间。sigmoid函数g(z) 1 / (1 exp(-z))就把任意实数映射到(0,1)区间。z越大g(z)越接近1z越小g(z)越接近0z0时g(z)0.5正好是分类边界。我在代码里写的sigmoid实现是这样的function g sigmoid(z) % SIGMOID 计算sigmoid函数值 % 输入z可以是标量、向量或矩阵 g 1 ./ (1 exp(-z)); end我们通常把测试集中的每个样本的z值算出来过一遍sigmoid得到概率p。p大于等于0.5判为类别1小于0.5判为类别0这样一个分类器就诞生了。2.2 损失函数与梯度下降模型是怎么“学”出来的模型要学的是那一组θ参数。怎么判断θ好不好Loss函数也就是损失函数就是评判标准。逻辑回归用的损失函数是交叉熵公式为J(θ) -(1/m) * Σ [y_i * log(h_i) (1-y_i) * log(1-h_i)]。为什么要用交叉熵而不是线性回归里那套均方误差因为逻辑回归的输出是概率如果套均方误差损失函数会变成非凸函数用梯度下降容易陷入局部最优解。而交叉熵配合sigmoid损失函数是一个凸函数只有一个全局最小值从任何初始点出发都能收敛到最优解。这个特性让梯度下降在逻辑回归里特别省心不需要担心落到坑里出不来。梯度下降的逻辑就是沿着损失函数下降最快的方向更新θθ_j θ_j - α * (1/m) * Σ (h_i - y_i) * x_ij其中α是学习率。我在Matlab里推荐使用内置的fminunc函数来替代手写梯度下降循环它能自动调整步长不需要人工调试学习率省掉一大半调参的烦躁。function [J, grad] costFunction(theta, X, y) % COSTFUNCTION 计算逻辑回归的损失函数值与梯度 m length(y); h sigmoid(X * theta); J -(1/m) * sum(y .* log(h) (1-y) .* log(1-h)); grad (1/m) * X * (h - y); end这里有个容易出错的地方h可能非常接近0或1导致log(h)出现无穷大。不过Matlab的fminunc和log函数对这类极端值处理还算稳健实际使用中我很少遇到崩溃的情况。2.3 训练集划分的核心逻辑为什么按顺序取前N条这套代码里训练集的划分用的是最简单的“按顺序取前N条”先别急着说它不严谨这个设计是经过考量的。随机划分比如用randperm打乱索引再切分确实在统计上更严谨能避免数据本身有序导致的偏差。但随机划分有个问题结果不可复现。你每次运行得到的训练集和测试集都不一样准确率也会上下浮动反而不利于对比实验。按顺序取前N条至少能保证同一个数据文件、同一个trainNum参数跑出来结果完全一致方便排查问题和复现结果。如果你需要随机划分我在代码里留了一个扩展点把splitData函数里的取法改一下即可。这个后面在扩展章节详细说。3. 核心代码实现与实操要点3.1 主函数结构一个入口三件事整套代码我设计成一个主函数加三个辅助函数。主函数负责调度辅助函数各管一段函数名职责输入输出logisticClassify主入口总调度文件路径、训练集个数、数据格式模型参数、准确率、预测结果loadData读取并解析数据文件路径、数据格式特征矩阵X、标签向量yfeatureNormalize特征归一化原始特征矩阵X归一化后的X、均值、标准差costFunction计算损失与梯度参数θ、特征X、标签y损失值J、梯度grad主函数的Matlab代码长这样function [theta, accuracy, predictResult] logisticClassify(dataFile, trainNum, dataFormat) % LOGISTICCLASSIFY 逻辑回归二分类预测主函数 % 输入 % dataFile - 数据文件路径支持xlsx/csv/txt % trainNum - 训练集样本个数正整数且小于总样本数 % dataFormat - 数据格式说明excel/csv/space/tab/comma % 输出 % theta - 模型权重参数 % accuracy - 测试集准确率 % predictResult - 结构体包含真实标签和预测标签对比 % 第一步加载数据 [X, y] loadData(dataFile, dataFormat); fprintf(数据加载完成共 %d 个样本%d 个特征\n, size(X,1), size(X,2)); % 第二步拆分训练集和测试集 [X_train, y_train, X_test, y_test] splitData(X, y, trainNum); fprintf(训练集%d 条测试集%d 条\n, length(y_train), length(y_test)); % 第三步特征归一化用训练集的均值和标准差 [X_train_norm, mu, sigma] featureNormalize(X_train); X_test_norm (X_test - mu) ./ sigma; % 第四步在训练集上训练逻辑回归模型 theta trainLogistic(X_train_norm, y_train); % 第五步在测试集上预测并计算准确率 prob sigmoid([ones(size(X_test_norm,1),1), X_test_norm] * theta); predict prob 0.5; accuracy mean(predict y_test) * 100; % 第六步输出结果 fprintf(测试集准确率%.2f%%\n, accuracy); fprintf(模型权重参数\n); disp(theta); end这套结构的好处是模块清晰。你如果只想改数据读取方式只动loadData只想换优化算法只动trainLogistic。互不干扰。3.2 数据格式解析一个函数兼容四种文件loadData函数的实现思路是先根据dataFormat参数确定分隔符再用Matlab的底层读取命令加载最后做一层数据校验。Excel用readmatrix文本文件用readmatrix加Delimiter参数。function [X, y] loadData(dataFile, dataFormat) % LOADDATA 根据指定格式读取数据最后一列为标签 switch lower(dataFormat) case excel data readmatrix(dataFile); case csv data readmatrix(dataFile, Delimiter, ,); case tab data readmatrix(dataFile, Delimiter, \t); case space data readmatrix(dataFile, Delimiter, ); case comma data readmatrix(dataFile, Delimiter, ,); otherwise error(不支持的数据格式%s可选excel/csv/tab/space/comma, dataFormat); end % 检查数据是否为空 if isempty(data) error(数据文件为空或者读取失败请检查文件路径); end % 检查是否有缺失值 if any(isnan(data(:))) error(数据中存在缺失值NaN请先处理缺失数据); end % 最后一列作为标签其余作为特征 X data(:, 1:end-1); y data(:, end); % 标签必须是0/1如果遇到其他值给出明确提示 if ~all(ismember(unique(y), [0; 1])) error(标签列必须为0和1当前标签值为%s, mat2str(unique(y))); end end这里有几个细节值得注意。第一readmatrix是Matlab R2019a及之后版本才有的函数如果你的版本比较老需要把readmatrix换成xlsread或csvread。第二数据中有一点点缺失值整个矩阵就会被读成NaN后面矩阵运算直接报错所以我在读取后立即做了缺失值检查。第三标签必须是0和1如果数据里标签是“好/坏”这样的文本需要先自己转换成0/1再喂给这个函数。3.3 特征归一化这一步真不能省很多初学者会忽略特征归一化结果就是梯度下降收敛特别慢或者干脆不收敛。我做了一个简单的例子验证过一个特征取值范围是0到1另一个特征取值范围是0到10000如果不归一化损失函数等高线图会变成非常扁的椭圆梯度下降走“之”字形路线要迭代上万次才能收敛。归一化之后等高线近似圆形几百次迭代就收敛了。归一化的方法我用的是最常用的z-score标准化x_norm (x - mu) / sigma其中mu是均值sigma是标准差。function [X_norm, mu, sigma] featureNormalize(X) % FEATURENORMALIZE 对特征进行z-score标准化 mu mean(X); sigma std(X); % 处理标准差为0的情况常数特征 sigma(sigma 0) 1; X_norm (X - mu) ./ sigma; end核心注意点必须用训练集的mu和sigma去归一化测试集而不是对合并后的全部数据做归一化。原因很简单测试集扮演的是“未来未知数据”的角色我们不能让它参与训练过程的任何统计量计算否则就是数据泄露data leakage测试集的准确率会虚高。这个坑我在早期写代码时踩过当时对全部数据一起归一化测试集准确率高了3个百分点但换到真实新数据上效果立刻打回原形。3.4 训练过程与参数选择fminunc还是手写梯度下降训练逻辑回归模型我在Matlab里有两种思路。第一种是手写梯度下降。好处是你能亲眼看到损失函数一步步下降的过程对理解算法原理很有帮助。坏处是要自己调学习率、迭代次数如果学习率设大了损失函数会震荡甚至发散。我调试的时候经常遇到这种情况损失函数从0.6降到0.5突然飙到2.3然后又掉下来明显是学习率偏大了。第二种是用Matlab内置的fminunc优化函数。它是拟牛顿法的实现能自动调整步长不需要人工调学习率收敛速度也快得多。对于日常的使用场景我强烈推荐用它。function theta trainLogistic(X_train_norm, y_train) % TRAINLOGISTIC 使用fminunc训练逻辑回归模型 % 在特征矩阵前加一列全1对应截距项 [m, n] size(X_train_norm); X_aug [ones(m, 1), X_train_norm]; initial_theta zeros(n 1, 1); % 设置优化参数 options optimset(GradObj, on, MaxIter, 400, Display, iter); % 调用fminunc求解最优参数 [theta, cost] fminunc((t)(costFunction(t, X_aug, y_train)), initial_theta, options); fprintf(训练完成最终损失值%.4f\n, cost); endoptimset里的Display参数建议保持iter这样能实时看到每一轮迭代的损失值变化一旦发现损失不减反增说明数据有问题或者规范化出了问题可以及时中断调试。等代码完全跑通后再改成final省去中间输出。3.5 预测与评价准确率之外还要看什么预测阶段比较无脑sigmoid输出概率阈值0.5判定分类。但光看一个整体准确率是不够的尤其是在类别不平衡的数据集上。举个例子一个数据集90%的样本是类别010%是类别1一个“无脑预测全部为0”的分类器准确率就有90%。这个模型看起来“很好”实际上一个正样本都抓不住。所以我在代码里还加了精确率Precision、召回率Recall和F1分数的计算。% 计算评价指标 TP sum(predict 1 y_test 1); FP sum(predict 1 y_test 0); FN sum(predict 0 y_test 1); precision TP / (TP FP); recall TP / (TP FN); F1 2 * precision * recall / (precision recall); fprintf(精确率%.2f%%\n, precision * 100); fprintf(召回率%.2f%%\n, recall * 100); fprintf(F1分数%.2f\n, F1);如果TP为0precision和recall会变成0除以0得到NaN实际使用中也要加一层判断这我在常见问题章节里细说。3.6 完整代码汇总为了便于直接复制运行我把完整的逻辑回归分类代码整合如下包含辅助函数function [theta, accuracy, metrics] logisticClassify(dataFile, trainNum, dataFormat) [X, y] loadData(dataFile, dataFormat); [X_train, y_train, X_test, y_test] splitData(X, y, trainNum); [X_train_norm, mu, sigma] featureNormalize(X_train); X_test_norm (X_test - mu) ./ sigma; theta trainLogistic(X_train_norm, y_train); prob sigmoid([ones(size(X_test_norm,1),1), X_test_norm] * theta); predict prob 0.5; accuracy mean(predict y_test) * 100; TP sum(predict 1 y_test 1); FP sum(predict 1 y_test 0); FN sum(predict 0 y_test 1); precision TP / max(TP FP, eps); recall TP / max(TP FN, eps); F1 2 * precision * recall / max(precision recall, eps); metrics struct(accuracy, accuracy, precision, precision, ... recall, recall, F1, F1, prob, prob, ... predict, predict, y_test, y_test); fprintf(准确率%.2f%% | 精确率%.2f%% | 召回率%.2f%% | F1%.2f\n, ... accuracy, precision*100, recall*100, F1); end function [X, y] loadData(dataFile, dataFormat) switch lower(dataFormat) case excel data readmatrix(dataFile); case {csv, comma} data readmatrix(dataFile, Delimiter, ,); case tab data readmatrix(dataFile, Delimiter, \t); case space data readmatrix(dataFile, Delimiter, ); otherwise error(不支持的数据格式%s, dataFormat); end if isempty(data), error(数据为空请检查文件路径和格式); end if any(isnan(data(:))), error(数据包含NaN缺失值请先清洗数据); end X data(:, 1:end-1); y data(:, end); if ~all(ismember(unique(y), [0; 1])) error(标签列必须为0和1当前标签值%s, mat2str(unique(y))); end end function [X_train, y_train, X_test, y_test] splitData(X, y, trainNum) % 按顺序取前trainNum条作为训练集其余作为测试集 totalNum size(X, 1); if trainNum totalNum error(trainNum必须小于总样本数%d当前设置%d, totalNum, trainNum); end if trainNum 0 error(trainNum必须是正整数当前设置%d, trainNum); end X_train X(1:trainNum, :); y_train y(1:trainNum); X_test X(trainNum1:end, :); y_test y(trainNum1:end); end function [X_norm, mu, sigma] featureNormalize(X) mu mean(X); sigma std(X); sigma(sigma 0) 1; X_norm (X - mu) ./ sigma; end function [J, grad] costFunction(theta, X, y) m length(y); h sigmoid(X * theta); J -(1/m) * sum(y .* log(h) (1-y) .* log(1-h)); grad (1/m) * X * (h - y); end function g sigmoid(z) g 1 ./ (1 exp(-z)); end function theta trainLogistic(X_train_norm, y_train) [m, n] size(X_train_norm); X_aug [ones(m, 1), X_train_norm]; initial_theta zeros(n 1, 1); options optimset(GradObj, on, MaxIter, 400, Display, final); [theta, cost] fminunc((t)(costFunction(t, X_aug, y_train)), initial_theta, options); fprintf(训练完成最终损失值%.4f\n, cost); end整个文件保存为logisticClassify.m即可辅助函数写在同一个文件里是允许的Matlab R2016b之后支持局部函数。4. 实测运行与结果解读4.1 用自带示例数据跑一遍全流程我从UCI上找了一个经典的乳腺癌数据集做实测取前100条数据存成Excel格式特征维度是30标签是0/1二分类。调用方式很简单[theta, accuracy, metrics] logisticClassify(breast_cancer.xlsx, 70, excel);运行结果大致如下数据加载完成共 100 个样本30 个特征 训练集70 条测试集30 条 训练完成最终损失值0.2817 准确率96.67% | 精确率94.12% | 召回率100.00% | F10.97可以看到70条训练数据在这个数据集上已经能取得不错的分类效果。这里顺便验证一下按顺序切分的训练集和测试集因为原始数据本身分布均匀效果和随机切分差别不大但如果原始数据是按类别有序排列的比如前50条全是类别0后50条全是类别1那么按顺序切分就会出问题——训练集全是一个类别模型什么都学不到。这一点务必提前检查你的数据顺序。4.2 训练集个数对分类效果的影响我把trainNum从10逐步调到90记录测试集准确率变化得到一条很有信息量的曲线训练集个数测试集个数准确率F1分数109082.22%0.84307091.43%0.92505094.00%0.94703096.67%0.979010100.00%1.00这个趋势很直观训练数据越多模型学到的模式越充分准确率整体上升。但注意最后一行“100%准确率”不能高兴太早因为测试集只有10条这个数字的统计意义很弱。这也是我坚持在代码里把训练集和测试集数量都打印出来的原因——让人看到准确率的同时也要看到它是在多少样本上算出来的。有意思的是trainNum30到trainNum50这一段提升最明显说明这个数据集上信息量的边际收益在这个区间最大。如果你要做学习曲线分析用这套代码多跑几组trainNum把结果画出来就能比较科学地判断“数据量够不够”。4.3 不同数据格式的切换实测我分别用CSV和Tab分隔的TXT保存了同一份数据分别调用[theta1, acc1] logisticClassify(breast_cancer.csv, 70, csv); [theta2, acc2] logisticClassify(breast_cancer.txt, 70, tab);两次运行得到的准确率和模型参数完全一致小数精度范围内这说明数据解析环节没有引入偏差。这里要提醒一个容易犯的低级错误如果你的CSV文件里某个特征含有文本比如“N/A”之类readmatrix会把整列读成字符串导致整个数据矩阵变成cell数组后续矩阵运算直接崩。遇到这种文件先在Excel里把文本占位符替换成空值或数值再做导入。5. 常见问题与排查技巧实录5.1 损失函数不下降或震荡怎么办这是我被问得最多的问题。现象是fminunc迭代了好几轮损失函数值还在原地踏步或者上下跳动。排查顺序通常是这样第一步看特征归一化做了没有没归一化的话先归一化再试第二步看标签是不是严格0和1如果你把标签设成了正负1损失函数公式完全不适用第三步看数据量是否太少比如每个类别只有五六条样本模型很难稳定收敛第四步看特征里是否有无穷大或极端离群值这些值标准化后依然可能很大把梯度带偏。我遇到过最诡异的一次数据里有一列的某个值是Inf不是NaNisnan检查不出来矩阵运算也不报错但梯度直接变成了NaN损失函数一飞冲天。最后是用isfinite检查全部数据才找出来。所以我在loadData里加了一行if any(~isfinite(data(:))), error(数据包含Inf或NaN请检查原始数据); end这行代码后来帮我省了不少事。5.2 数据格式解析报错报错信息一般是“数据为空请检查文件路径和格式”但文件路径明明是对的。这种时候九成是分隔符不匹配。比如你用Excel另存为CSV时如果你的系统区域设置是中文CSV的分隔符可能是分号;而不是逗号,。你指定csv格式readmatrix默认按逗号切结果整列全变成一个值矩阵直接读出来是乱的。解决方法是在另存为CSV时明确选择“CSV UTF-8逗号分隔”或者读的时候用Delimiter, ;手动指定。另一个常见问题是Excel里的表头。逻辑回归的数据文件应该是纯数值矩阵不带表头。如果你的Excel有第一行变量名readmatrix默认会把它处理掉R2019a以上版本但有时候会把表头读成一行NaN导致缺失值检查报错。稳妥做法是先把Excel里的表头行删掉或者另存CSV时不要选包含标题行。5.3 维度不匹配到底是哪里的锅“Matrix dimensions must agree”是Matlab最常见的报错之一。在逻辑回归里这个报错几乎都出在同一个地方给X拼接全1列时维度对不上。具体来说trainLogistic里用了[ones(m,1), X]要求X是一个m行n列的矩阵。如果你在数据读取阶段不小心把y也拼进了X那么X的列数就多了一列后面theta的维度也会跟着错。我的建议是在loadData输出后立刻检查size(X)和length(y)是否匹配assert(size(X,1) length(y), 特征矩阵行数与标签长度不一致);assert这行代码成本极低但能帮你把维度错误直接拦截在源头。5.4 测试集准确率比训练集高出一大截这个现象刚接触的人会觉得奇怪怎么会“学得越多反而考得越差”其实这通常不是bug而是数据分布或划分方式的问题。最常见的情况是你的原始数据本身有序前N条训练集中的正负类比例很失衡而后面的测试集恰好比例均衡。模型在训练时只见过大量类别0的样本学到的决策边界极度偏向预测为0测试集碰上类别1自然全错。解决办法如果数据是按类别排序的先randperm打乱整个数据集。我建议在splitData里加一个开关默认按顺序需要随机时就传一个shuffleFlag参数。也有一种可能性是过拟合的反面——欠拟合训练集本身就没学明白测试集碰巧“瞎猫碰上死耗子”。这种情况要看训练集准确率如果训练集准确率也只有六七十那说明特征表达能力不足或者数据量太小需要补充特征或数据而不是调模型。5.5 精确率、召回率出现NaN当TPFP为0或者TPFN为0时precision和recall的公式会出现0/0Matlab会给出NaN。这在测试集里恰好没有某个类别的预测结果时就会发生。我在完整代码里的解法是用max(denominator, eps)把分母下限设定为一个极小值这样计算结果会变成0而不是NaN后续F1也不会崩。类似的防御性写法在很多数值计算里都适用建议养成习惯。6. 基于这套代码的扩展方向6.1 加入L2正则化防止过拟合如果你的特征维度特别高比如文本TF-IDF特征有几千维而训练样本只有几百条过拟合几乎是必然的。这时候在损失函数里加一个L2正则项是标准做法function [J, grad] costFunctionReg(theta, X, y, lambda) m length(y); h sigmoid(X * theta); J -(1/m) * sum(y .* log(h) (1-y) .* log(1-h)) ... (lambda/(2*m)) * sum(theta(2:end).^2); grad (1/m) * X * (h - y) ... (lambda/m) * [0; theta(2:end)]; end注意正则项从θ2开始加θ0截距项不参与正则化这是惯例。lambda一般从0.01、0.1、1这几个数量级开始试画个验证集损失随lambda变化的曲线选最低点。6.2 从二分类扩展到多分类逻辑回归天然是二分类器但多分类可以借助“一对多”one-vs-all策略实现有几个类别就训练几个二分类器每个分类器把“当前类别”vs“其余全部”分开预测时把样本送到所有分类器里取输出概率最大的那个类别作为最终预测。改动量不大核心就是把trainLogistic包在一个循环里for c 1:K y_binary (y c); theta_all(c, :) trainLogistic(X_train_norm, y_binary); end这样theta_all就是一个K行×(n1)列的矩阵预测时计算所有类别的概率分数取argmax。6.3 增加随机种子支持复现实验如果你最终需要随机划分训练集我建议在splitData里加一个随机种子的控制参数function [X_train, y_train, X_test, y_test] splitData(X, y, trainNum, shuffleFlag, seed) if nargin 4, shuffleFlag false; end if nargin 5, seed 42; end if shuffleFlag rng(seed); % 设置随机种子保证可复现 idx randperm(size(X, 1)); X X(idx, :); y y(idx); end % 后续切分逻辑不变 end注意rng(seed)要放在randperm之前。这样你既能享受随机划分的统计严谨性又能保证每次运行结果一致两全其美。6.4 输出分类决策边界可视化如果特征维度是2或者3可以顺手把训练好的决策边界画出来对理解模型行为非常有帮助。逻辑回归的决策边界在特征空间里是一个超平面二维情况下就是一条直线θ0 θ1*x1 θ2*x2 0。画法很简单在x1取值范围内生成一组点算出对应的x2然后plot连线即可。这一块代码不长但对报告的展示效果增益极大强烈建议加上。这套代码我前后用了大半年从最早只能读固定格式的Excel到现在的通用版本每次改动都来自实际需求。最让我觉得有价值的是“训练集个数可自定义”这个设计它让跑学习曲线变成了一件两分钟的事而在很多商业工具里这个功能要么没有要么藏在付费模块里。如果你也是经常要快速验证分类思路的工程师或研究生直接把文中的代码存下来改个数据路径就能跑。后续如果遇到什么新问题欢迎回来交流我持续更新这个项目。