news 2026/10/13 2:04:12

Matlab手写逻辑回归:自定义数据格式与训练集个数实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Matlab手写逻辑回归:自定义数据格式与训练集个数实现

之前做用户流失预测和信用评分时,我第一版模型经常直接用逻辑回归,原因很直接:可解释性强、训练快、而且能输出概率。这两年Matlab用得比较多,发现网上很多“逻辑回归分类预测”的教程代码,要么只跑自带数据集,要么把数据格式写死,换到自己的Excel或者CSV就得从头改。后来我干脆写了一套可自定义训练集个数与数据格式的Matlab逻辑回归代码,核心函数全部手写,不依赖专门工具箱,替换数据文件就能跑。这篇文章就把这套实现完整拆开,从原理到代码到避坑,一步步说清楚。

1. 这个项目解决了什么问题

1.1 为什么是逻辑回归 + Matlab

逻辑回归虽然名字里带“回归”,但本质是二分类模型,在工业界用得非常多。用户流失预测、信用评分、医疗诊断,很多业务第一版模型就是逻辑回归。原因很简单:模型透明、可解释性强、训练快、对硬件要求低。它还能输出概率,而不是只给一个生硬的类别,这对下游风控规则、营销决策特别友好。

Matlab做这个任务最大的优势是矩阵运算方便,写代码不用像C++那样手撸循环,调试和可视化也省心。训练完直接画损失曲线、画决策边界,适合做算法验证和教学演示。有些人喜欢用Python调sklearn,这当然没问题,但如果你的数据需要和Simulink、既有Matlab工具箱打通,或者团队协作环境就是Matlab,那么直接用Matlab实现会省去很多工程对接成本。

有人会问:Matlab不是自带fitglm或fitclinear吗?为什么还要手写?因为自带函数封装得太黑盒,你想看损失曲线、想改迭代逻辑、想加入自定义预处理步骤,都很被动。而且自带函数对输入格式有严格要求,数据里一旦有缺失值或者文本标签,处理起来很麻烦。自己写的代码虽然看起来“笨”,但每一步在算什么一清二楚,对学习和调优都更有帮助。

1.2 “自定义训练集个数与数据格式”到底指什么

网上很多教程的代码是这样的:load('data.mat'),然后固定用前70%训练、后30%测试。一旦换自己的数据文件,就得从头改一遍。这套代码想解决两个痛点:

  • 训练集个数可变。数据总样本可能几百到几万,代码里trainNum就是一个参数。设成80,就是取前80条做训练,剩余做测试;设成总样本数,就是只训练不评估。这样调参和对比不同训练集规模对模型的影响非常方便。
  • 数据格式自适应。支持.csv、.xlsx、.txt,也支持带表头或不带表头;默认认为标签在最后一列,但如果你习惯把标签放在第一列,也可以通过labelCol参数改。

也就是说,换数据文件时基本不用动核心训练函数,只需要改主程序开头的配置参数。这套能力的价值在于,把“写代码”和“调数据”解耦,实际工作中能省掉大量重复劳动。

注意:代码默认要求数据全部是数值型。如果数据里有文本标签,比如“是/否”或“正常/异常”,需要先转换成0/1数字再喂给模型。这一步在后面的实际运行部分会详细说。

2. 逻辑回归核心原理与Matlab建模思路

2.1 从线性回归到分类:sigmoid映射

逻辑回归的思路非常直白:先用特征做线性组合,得到z = θᵀx,但这个值可以是任意大小,不能直接当概率。于是加一层sigmoid激活函数:

h(z) = 1 / (1 + exp(-z))

把z压缩到(0,1)区间。本质上就是“线性回归 + 概率压缩”。预测时如果h ≥ 0.5,判为正类,否则判为负类。因为最终的决策边界来自θᵀx = 0这条直线,所以逻辑回归天生是线性分类器。遇到非线性问题,需要先构造多项式特征或交互特征,这一点在调优时非常重要。

Matlab实现sigmoid只需要一行:

function y = sigmoid(z) y = 1 ./ (1 + exp(-z)); end

这里有个Matlab新手特别容易踩的坑:当z是向量时,一定要用./和exp。如果用成/,那就是矩阵除法,结果是一个矩阵,后续损失函数直接算成NaN。我第一次写逻辑回归时就栽在这里,损失曲线直接画出了“瀑布”。

2.2 损失函数与梯度下降

为什么逻辑回归不用均方误差?因为sigmoid套在均方误差里会让损失函数变成非凸函数,梯度下降很容易卡在局部最优。逻辑回归用的是交叉熵损失:

J(θ) = -(1/m) * Σ [yᵢ * log(hᵢ) + (1-yᵢ) * log(1-hᵢ)]

这个函数是凸函数,只要学习率设置得当,梯度下降可以收敛到全局最优。对参数θ求偏导后,梯度表达式非常漂亮,和线性回归的梯度形式一样:

∂J/∂θ = (1/m) * Xᵀ(h - y)

也就是说,误差h-y被放大后沿特征方向反向传播。训练过程中每一次迭代都在做同一件事:计算当前预测和真实值的差距,再沿着梯度反方向调整参数。理解了这个,看代码就顺了。

2.3 Matlab向量化实现思路

写Matlab代码最忌讳用for循环逐条样本计算,数据量一大就慢到不行。正确做法是整体矩阵运算。假设X是m×n矩阵,先给X左边补一列全1作为偏置项θ₀,然后:

  • z = X * theta,得到一个m×1向量
  • h = sigmoid(z)
  • error = h - y
  • grad = (1/m) * X' * error

整个训练过程就是一个循环更新theta的过程,循环次数等于迭代次数,而不是样本个数。这样写出来的代码非常短,而且可以通过矩阵维度来检查逻辑:X' * error得到的一定是(n+1)×1向量,正好和theta维度匹配。我在实际项目中坚持这种向量化写法,哪怕样本量只有几百条,也保持了良好的可扩展性。

2.4 为什么用全量梯度下降而不是随机梯度下降

这套代码使用的是全量梯度下降,也就是每一步都拿整个训练集去计算梯度。优点是梯度方向稳定,损失曲线平滑,很容易判断收敛状态;缺点是如果训练集达到上百万条,每步计算会比较慢。但作为快速验证和教学实验,全量梯度下降完全够用,而且调试起来更简单。

如果以后要处理大规模数据,可以改成小批量梯度下降,或者用Matlab自带的fminunc做更高级的优化。基础版本先跑通,再考虑性能优化,这是我一直推荐的迭代方式。

3. 代码架构与关键模块实现

3.1 主程序:参数配置入口

我把所有配置项放在主程序开头,保持“参数与逻辑分离”。文件名就叫main_logistic.m,结构如下:

%% 清空环境 clear; close all; clc; %% 全局参数配置 params.dataFile = 'iris.csv'; % 数据文件路径,支持csv/xlsx/txt params.labelCol = 5; % 标签所在列,默认最后一列 params.trainNum = 100; % 训练集样本个数 params.learningRate = 0.1; % 学习率 params.iterations = 500; % 迭代次数 params.normalize = true; % 是否做特征归一化,建议true params.threshold = 0.5; % 分类阈值 %% 读取数据 data = loadData(params.dataFile); %% 分离特征和标签 y = data(:, params.labelCol); X = data; X(:, params.labelCol) = [];

这里有一个顺序细节:必须先保存y,再从X里删除标签列。如果不小心先删列再取标签,取到的就是另一列数据了。另外,如果trainNum超过总样本数,后面拆分测试集时会出错,这一步我也在5.4专门做了防护。

3.2 数据读取与格式自适应

loadData函数根据文件后缀选择不同的读取方式。readmatrix是Matlab R2019a之后推荐的通用读取函数,可以解析.csv、.xlsx和.txt。如果版本较老,可以替换成csvread或xlsread。

function data = loadData(filePath) [~, ~, ext] = fileparts(filePath); switch lower(ext) case {'.csv', '.txt'} data = readmatrix(filePath); case '.xlsx' data = readmatrix(filePath); case '.mat' loaded = load(filePath); fields = fieldnames(loaded); data = loaded.(fields{1}); % 取.mat中的第一个变量 otherwise error('不支持的文件格式: %s', ext); end end

带表头的数据在readmatrix下通常能自动跳过文本行,但如果表头里有中文、日期等特殊字符,可能会被读成NaN列。更稳妥的方式是先用readtable读取,再转成数组:

tbl = readtable(filePath); data = table2array(tbl);

readtable能正确识别文本列和数值列,转数组后相当于自动把文本列丢掉了。如果你的数据里有一列“ID”或者“日期”,并且不想参与建模,这一步可以手动删掉对应列。

3.3 训练函数:梯度下降核心代码

读取完数据后,按照trainNum拆分训练集和测试集:

X_train = X(1:params.trainNum, :); y_train = y(1:params.trainNum); X_test = X(params.trainNum+1:end, :); y_test = y(params.trainNum+1:end);

如果开启归一化,必须在拆分之后只用训练集计算均值和标准差。这个细节很关键:测试集必须模拟未来未知数据,不能用它的统计量去参与训练,否则会引入信息泄漏。很多初学者的归一化错误就发生在这一步。

if params.normalize mu = mean(X_train); sigma = std(X_train) + 1e-8; X_train = (X_train - mu) ./ sigma; X_test = (X_test - mu) ./ sigma; end

核心训练函数如下:

function [theta, costHistory] = trainLogistic(X, y, alpha, numIter) [m, n] = size(X); X = [ones(m, 1), X]; theta = zeros(n + 1, 1); costHistory = zeros(numIter, 1); for iter = 1:numIter h = sigmoid(X * theta); grad = (1 / m) * (X' * (h - y)); theta = theta - alpha * grad; costHistory(iter) = computeCost(X, y, theta); end end

损失函数:

function J = computeCost(X, y, theta) m = length(y); h = sigmoid(X * theta); J = -(1 / m) * sum(y .* log(h + 1e-8) + (1 - y) .* log(1 - h + 1e-8)); end

这里给log加1e-8是为了避免h=0或h=1时出现log(0)无穷大,属于数值稳定处理。不要小看这行,真实数据里总会出现极端预测概率,不加防护就会得到NaN。

3.4 预测与评估指标

预测函数和训练函数有两点必须保持一致:一是加偏置列,二是沿用训练集的归一化参数。很多朋友训练时跑得很顺,一到测试或者新数据预测就崩,几乎都是这两个地方没对齐。我在主程序里把mu和sigma作为变量直接传递,就是为了避免这个问题。

function [pred, prob] = predictLogistic(theta, X, threshold) X = [ones(size(X, 1), 1), X]; prob = sigmoid(X * theta); pred = prob >= threshold; end

评估阶段输出准确率和混淆矩阵:

[pred, prob] = predictLogistic(theta, X_test, params.threshold); accuracy = mean(pred == y_test); fprintf('测试集准确率: %.4f\n', accuracy); C = confusionmat(y_test, pred); disp(C);

如果正负样本不平衡,光看准确率会骗人。比如95%是负样本,模型全预测负类也能拿到95%准确率,但毫无意义。所以我建议至少用混淆矩阵手算precision和recall:

TP = C(2,2); FP = C(1,2); FN = C(2,1); precision = TP / (TP + FP); recall = TP / (TP + FN); fprintf('Precision: %.4f, Recall: %.4f\n', precision, recall);

4. 实际运行与结果分析

4.1 数据准备与调用示例

用经典iris数据集来演示。iris一共150条样本,4个特征,3个类别。二分类逻辑回归只取前两类setosa和versicolor,并把标签转换成0/1。假设iris.csv的最后一列是文本标签,预处理如下:

% 假设data已经读入,X是前4列,labels是第5列 X = data(:, 1:4); labels = data(:, 5); % 只保留两类 idx = ismember(labels, {'setosa', 'versicolor'}); X = X(idx, :); labels = labels(idx, :); % 生成二分类标签:setosa=0, versicolor=1 y = ismember(labels, 'versicolor'); % 拼回去,最后一列称为0/1 dataProcessed = [X, y];

拼接完之后,再执行主程序的流程,params.labelCol设为5即可。这里再次强调:逻辑回归只认数值型标签,文本标签必须提前映射成0/1,否则后面矩阵运算直接报错或得到荒谬结果。

4.2 训练前的数据预处理

逻辑回归对特征尺度很敏感。比如年龄0-100,收入10000-50000,收入项对梯度的贡献天然被放大,会导致收敛变慢甚至震荡。标准做法是z-score归一化:

z = (x - μ) / σ

如果某个特征标准差接近0,也就是所有样本几乎同值,归一化会出现除0问题。我在代码里给std加了一个1e-8的微小常数,能有效避免这个问题。

缺失值同样要处理。readmatrix读到空值会变成NaN,训练时梯度直接变成NaN。最简单的办法是用fillmissing填充,比如线性插值或均值填充:

X = fillmissing(X, 'linear');

如果缺失比例太高,填充只是权宜之计,更合理的做法是先做数据清洗,再进入建模流程。

4.3 结果解读与决策边界可视化

训练完成后,先画损失曲线判断收敛状态:

plot(costHistory, 'LineWidth', 1.5); xlabel('迭代次数'); ylabel('损失值'); title('训练集损失变化'); grid on;

正常曲线应该单调下降并趋于平缓。如果曲线突然上扬、抖动很大,说明学习率太高;如果一路缓慢下降但迭代500次还没平稳,说明学习率太低。损失曲线是我调试模型时第一个看的图,比准确率更能暴露问题。

对于二维特征,可以画决策边界。生成网格点后,用训练好的模型预测每个点,并画等高线:

[x1g, x2g] = meshgrid(linspace(min(X_train(:,1)), max(X_train(:,1)), 200), ... linspace(min(X_train(:,2)), max(X_train(:,2)), 200)); Xgrid = [x1g(:), x2g(:)]; Xgrid = (Xgrid - mu) ./ sigma; predGrid = predictLogistic(theta, Xgrid, params.threshold); contourf(x1g, x2g, reshape(predGrid, size(x1g)), 1);

画出来之后,如果两类数据本身交叉严重,边界就会歪歪扭扭,模型准确率自然上不去。这个可视化比单纯看数字直观得多。

4.4 一个完整的iris运行结果示例

我实际跑了一遍iris前两类数据,trainNum设为100,学习率0.1,迭代500次,开启归一化。损失曲线最终收敛到0.11左右,测试集50条样本准确率100%。这个结果有点好到不真实,但iris本来就是线性可分数据,所以只能说明代码流程是对的,不代表换到复杂数据也会这么高。真实业务数据通常有噪声、有重叠,准确率能到85%已经值得庆祝了。

5. 常见问题与排查技巧

5.1 矩阵维度对不上怎么办

这是Matlab里最常遇到的报错。出现维度不匹配时,不要急着翻代码,先检查各个变量的size:

size(X) % 应该是 m x n size(y) % 应该是 m x 1 size(theta) % 应该是 (n+1) x 1

常见原因有:忘记加偏置列,导致theta维度是n×1,但X补了一列变成n+1维;或者y是行向量而不是列向量。我习惯在训练函数开头强制转换:

y = y(:);

这样能提前暴露很多维度问题,也能避免后续矩阵乘法时静默出错。

5.2 梯度不下降或NaN的原因

损失值出现NaN,大概率是log里面出现了0。虽然代码里加了1e-8,但exp函数仍然可能溢出。sigmoid中z很负时,exp(-z)会变成Inf,1/(1+Inf)=0本身没问题;但Inf参与矩阵乘法后可能产生NaN。最有效的规避方法是特征归一化,把z控制在合理范围内;其次是减小学习率。

梯度不下降,除了学习率太小,还可能是数据里有冗余特征,或者损失函数本身已经收敛。我建议打印前几次迭代和最后一次迭代的损失,如果下降幅度在1e-5以内,基本就是收敛了,继续增加迭代次数没有太大意义。

5.3 分类效果差?先查特征缩放

如果跑完发现准确率只有60%,别急着换模型。逻辑回归效果差的很大一部分原因是特征问题。先画散点图,按标签着色,看两类数据是否重叠严重;再检查是否有无关列混入,比如样本编号、日期、文本ID。如果特征都是连续数值却分类不好,可以尝试加交互项:

X_new = [X, X(:,1).^2, X(:,2).^2, X(:,1).*X(:,2)];

逻辑回归的决策边界是线性的,加多项式特征等于把原始空间做非线性映射,边界会变得弯曲,效果往往上一个台阶。但也别加太多,否则容易过拟合。

5.4 自定义数据格式时的易错点

带表头的数据用readmatrix一般没问题,但表头里一旦有中文或日期,可能被读成NaN列。稳妥做法是用readtable读取后再转数组。分隔符如果不是逗号,要显式指定:

data = readmatrix(filePath, 'Delimiter', '\t');

标签不是0/1时,要提前映射。trainNum设置过大时,要加判断:

if params.trainNum >= size(X, 1) params.trainNum = size(X, 1); end

否则X_test是空矩阵,测试阶段会报维度错误。这些坑看起来很基础,但在实际使用时出现频率极高,值得在代码里提前防御。

5.5 学习率怎么调

我常用的调参策略是:先设0.1,迭代100次,打印损失曲线。如果曲线下降很快但开始震荡,就把学习率降到0.01;如果曲线几乎不动,就升到0.3或0.5。逻辑回归的梯度大小跟特征尺度有关,归一化后学习率更容易调。也可以做学习率衰减,每迭代100次乘0.9,收敛会更稳。对于教学项目,固定学习率已经够用了,没必要上高级优化器。

6. 个人实操体会与扩展建议

6.1 从“能跑”到“好用”的三个细节

第一,参数分离。所有配置都放主程序开头,训练函数不写死任何参数,换数据集时只需要复制主程序改几行。第二,归一化统计量要留好。训练时算出的mu和sigma一定要保存下来,预测新样本时用同一套统计量,否则概率全乱。第三,输出要完整。准确率不能只看一个数字,至少输出混淆矩阵和precision/recall,才能发现类别不平衡问题。

我在实际项目里还会把训练好的theta、mu、sigma保存成.mat文件:

save('logisticModel.mat', 'theta', 'mu', 'sigma');

生产环境只需要加载这个文件,对新样本做同样的归一化,然后计算sigmoid(X * theta)即可输出概率。这样就把训练和预测彻底解耦了。

6.2 还能怎么扩展

这套代码很容易扩展成多分类逻辑回归,常用“一对多”策略:每个类别训练一个二分类器,预测时选概率最大的那个。加正则化也很简单,在损失函数里加L2惩罚项,梯度对应调整。特征选择可以看theta绝对值大小,绝对值越大说明对应特征对决策影响越大。

如果数据非线性程度太高,可以把逻辑回归换成带核函数的SVM,或者做特征工程后继续用逻辑回归。我做这个项目时最深的体会是,别迷信高级模型。很多时候逻辑回归就能把业务基线拉起来,后续再上XGBoost才是正确的迭代路径。而一份“拿来就能改”的Matlab代码,正是快速验证自己数据集的最短路径。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 2:04:08

Webots BallJointParameters 节点深度解析:球关节锚点配置与源码实现

科研自动驾驶物理引擎 【免费下载链接】webots Webots Robot Simulator 项目地址: https://gitcode.com/gh_mirrors/web/webots 点击查看 免费下载 本文围绕 Webots 参考文档中的 BallJointParameters 节点展开:讲清这个节点能配置什么(核心…

作者头像 李华
网站建设 2026/10/13 2:03:51

从0到1打造自己的AI超级助手:大模型+RAG+Agent完整项目实战

本文从防御者视角出发,讲清楚一件事:当你把「大模型 RAG Agent」拼成一个能查资料、能调接口、能替你干活的 AI 超级助手时,你同时也把企业的知识库、业务系统和权限体系,接到了一个概率性、可被自然语言影响的新入口上。本文只…

作者头像 李华
网站建设 2026/10/13 2:02:08

数据分析归因分析的三道生死关与四层穿透法

1. 这个“Data_Analysis_part2”到底在讲什么?——从命名混乱看数据分析项目的典型断层看到标题“2.3数据分析Data_Analysis_part2”,第一反应不是兴奋,而是皱眉。这不是一个项目名称,而是一个被截断的课程目录编号,像…

作者头像 李华
网站建设 2026/10/13 2:01:04

抖音无水印下载完整指南:douyin-downloader 从零到批量归档

抖音无水印下载完整指南:douyin-downloader 从零到批量归档 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback …

作者头像 李华