ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLC局部受限线性编码:中小规模图像分类的高效MATLAB方案

LLC局部受限线性编码:中小规模图像分类的高效MATLAB方案 简介这是一份面向图像分类与计算机视觉研究者的Matlab实现资源完整对应CVPR 2010论文《Locality-constrained Linear Coding for Image Classification》中的局部受限线性编码LLC算法适合希望复现经典方法、开展特征编码实验的研究生与工程师。资源包共10个文件以8个.m源码文件为主涵盖SIFT特征提取、特征归一化、词典生成、LLC近似编码与池化分类等完整流程另含1个.mat预训练词典数据与1个readme使用说明压缩包整体约575KB。目前已有205人学习下载。借助该资源读者可对照论文逐模块理解LLC相较于传统稀疏编码在计算效率与分类精度上的优势目录结构清晰、脚本划分明确便于在Caltech101等数据集上直接运行或二次开发是入门视觉词袋与局部特征编码的可靠实战参考。1. 局部受限线性编码为什么是图像分类的性价比之选一次点破LLC的适用范围做图像分类大作业或中小规模数据集项目时很多人一上来就想到预训练CNN或Transformer但手里没有GPU、数据量只有几千张、又要求代码能在课堂演示现场跑完——这时候传统的视觉词袋框架反而更稳。局部受限线性编码Locality-constrained Linear Coding简称LLC正是这种环境下最值得先试的编码方案它用“和码字越近编码权值越大”的局部性约束替代了稀疏编码里的L1稀疏约束在同样使用SIFT特征和线性SVM的情况下准确率能逼近甚至超过用RBF核的稀疏编码而计算量小一个数量级。这篇文章我会从LLC在词袋流程里的位置讲起给出可直接在MATLAB里跑的dense SIFT、K-means码书、LLC近似编码和空间金字塔池化代码再把我实际调参踩过的坑一条条列出来包括码书大小、近邻数、特征标准化的影响以及PCA白化这个让精度再涨一截的进阶技巧。适合刚接触图像分类算法、想在MATLAB里快速落地并复现对比实验的读者。2. 从码书到编码LLC在BoVW图像分类流程中解决的是什么问题要理解LLC得先把它放回Bag of Visual WordsBoVW这个经典框架里。BoVW把图像看成是一堆局部特征的集合整条流水线分为五步提取局部特征、离线学习码书、对每个局部特征做编码、把一张图的所有编码池化成固定长度向量、最后交给分类器。LLC做的是第三步也就是把每个SIFT描述子表示成码字的线性组合。这一步的表示质量直接决定后面池化和分类的天花板。2.1 BoVW的五个环节特征提取、码书、编码、池化、分类BoVW最早的形态是硬量化Vector QuantizationVQ每个局部特征只在码书里找一个最近的码字置1其余置0。它的致命伤是“硬”两个靠近码字边界的特征可能一个被分配到左侧码字一个被分配到右侧码字明明视觉上很像编码结果却完全不同。后来稀疏编码Sparse CodingSC用L1范数让每个特征同时选几个码字解决了硬量化的离散问题精度明显提升但代价是每个特征都要解一次优化问题在数万甚至数十万局部特征面前速度慢得让人不想跑第二遍。LLC的思路是与其用稀疏性约束让优化过程慢慢选出码字子集不如直接用“局部性”来限定候选范围。它的做法是先按欧氏距离找出离当前特征最近的K个码字然后只在这K个码字组成的子字典上求线性表示。这样一来LLC编码天然是稀疏的非零系数只出现在近邻码字上但不需要求解L1优化一个特征一次最小二乘就出结果。后面的池化和分类环节LLC与普通BoVW完全一样所以它可以无缝替换原来的编码模块。2.2 硬量化、稀疏编码与LLC谁的编码误差更小单看重建误差稀疏编码理论上表现最好因为它是在全局字典上搜索最优组合。但图像分类任务关心的是编码是否保留了特征之间的相似性结构而不是单纯追求重建误差最小。硬量化把相似性变成了0/1开关稀疏编码的系数虽然有区分度但它的码字选择受字典的过完备性和初始化影响很大相近的两个特征在稀疏约束下可能选了完全不同的码字集合编码向量之间缺乏平滑性。LLC的最大池化特性在这里体现得很明显。LLC的系数具备“局部平滑性”相近的图像特征编码后非零系数的位置和幅值都相近而同一个空间区域内的SIFT特征它们的编码会在少数几个空间邻近的码字上同时有响应。正因为这种平滑性LLC用简单的线性SVM就能把分类边界拉出来不需要再配非线性核。我在实际对比里看到对同一批SIFT特征LLC的测试精度通常比硬量化高8到12个百分点比稀疏编码略高1到2个点但训练时间只有稀疏编码的几十分之一。2.3 LLC的目标函数与局部性约束的数学直觉LLC的原始目标函数写出来是这样min_c ||x - Dc||^2 λ ||d ⊙ c||^2约束条件是1^T c 1其中x是一个待编码的局部特征D是整个码书c是编码系数向量d是x到每个码字的欧氏距离向量⊙表示逐元素相乘。第二项的含义很直接如果一个码字离x很远d里对应的值就大那这个码字上的系数就会被惩罚得更狠。所以最优解会把表示权重压在离x近的码字上。这个约束从数学上保证了“局部性”而且整个目标函数没有L1范数全部是二次项最优解可以直接用最小二乘推导出来不需要迭代。工程上更常用的近似解法是两段式先对每个x在D中找K近邻得到子字典D_N再在D_N上解一个带仿射约束的最小二乘把系数归一化到和为1。严格说这已经不是原问题的精确解但经验上损失极小。原论文里也可以加入重建残差项来弥补近似误差但大多数项目里省略它并不会让精度明显变化。我一般会保留一个小的正则化项防止子字典出现奇异矩阵导致系数爆炸这一点在后面的代码里会体现。3. MATLAB实现LLCdense SIFT、K-means与编码函数的可运行代码这一章给出一个能直接运转的最小实现。环境方面我自己常用的是MATLAB 2023b新版MATLAB 2026b也完全兼容但需要提前装好Statistics and Machine Learning Toolbox因为后面要用到fitcecoc。特征提取用VLFeat工具箱这是一个C语言实现、带MATLAB接口的计算机视觉库dense SIFT和K-means都靠它。数据集建议从Caltech101或15-Scene这类公开数据集下载类别数5到15类最好每类50到100张图跑一轮实验几分钟就能出结果。3.1 用vl_dsift提取dense SIFT特征与参数选择dense SIFT和普通SIFT的关键区别在于它不需要检测关键点而是在图像上按固定步长滑窗取patch并计算描述子。这对图像分类更合适因为分类任务需要的是整幅图的均匀覆盖而不是少数角点。VLFeat里对应函数是vl_dsift最小用法如下% 读取图像并转为单精度灰度图 im im2single(imread(imgPath)); if size(im, 3) 3 im rgb2gray(im); end % 步长4像素patch宽度4个cell实际16像素 [frames, descs] vl_dsift(im, Step, 4, Size, 4, Fast, true); % 转换为N行128列便于后续K-means descs single(descs);这里的Step参数控制采样密度Step越小特征越多精度通常略高但计算量成倍涨。Step4在一般图像上会得到几万个特征足够用了。Size参数控制patch的cell数Size4表示每个patch分成4乘4的cell每个cell统计8方向直方图拼起来就是128维。Fast参数让VLFeat在计算时做一些近似加速对最终精度影响很小开启后速度能提升一截。还有一个容易被忽略的参数是Paddingvl_dsift默认不处理边界像素靠近图像边缘的patch会被丢弃导致边界区域没有特征。后面池化时如果金字塔分块恰好压在边界上会让某一格的统计量明显偏少。我建议提取时加上Padding, 8让采样范围向外扩8像素避免这种边界空洞。3.2 用vl_kmeans学习码书以及码书大小选择码书本质上是把训练集里所有SIFT描述子聚类成K个中心每个中心看作一个视觉单词。学习码书时不需要把所有特征都丢进去每类随机抽几千个总样本控制在二十万以内就够。VLFeat的vl_kmeans在速度和内存上都优于MATLAB自带的kmeans特别是它支持多个初始化并行搜索。实际操作如下% 假设trainFiles是训练图像路径列表 allFeats []; for i 1:numel(trainFiles) im im2single(imread(trainFiles{i})); if size(im, 3) 3, im rgb2gray(im); end [~, descs] vl_dsift(im, Step, 4, Size, 4, Fast, true); % 每张图随机抽4000个控制总量 sel randperm(size(descs, 2), min(4000, size(descs, 2))); allFeats [allFeats; double(descs(:, sel))]; end % 再用vl_colsubset均匀抽20万 X single(vl_colsubset(allFeats, 200000)); [centers, ~] vl_kmeans(X, 1024, NumInit, 3, Verbose, true); D centers; % 128 x 1024每列一个码字码书大小直接影响分类精度和编码计算量。对128维SIFT特征512到2048都是常见选择。我做过多次对比在15-Scene数据集上256码字准确率约78%1024码字能到83%左右再往上到2048提升不到1个点但编码和池化时间几乎翻倍。所以编码类项目里默认从1024开始调是最稳妥的。NumInit控制K-means随机初始化的次数这个参数值得开大一点K-means本身是个非凸优化初始化不好会收敛到局部最小值码书质量差了一截后面怎么调都补不回来。3.3 LLC近似编码近邻搜索加仿射最小二乘LLC编码是整个流程的核心我一般实现成函数单独放一个文件里。输入是单个128维特征向量x和码书D输出是稀疏系数向量code。代码如下function code llc_encode(x, D, kNN) % x: 128 x 1 特征向量 % D: 128 x m 码书每列为码字 % 计算x到每个码字的欧氏距离 dist sqrt(sum((D - repmat(x, 1, size(D, 2))) .^ 2, 1)); % 取最近的kNN个索引 [~, idx] sort(dist, ascend); idx idx(1:kNN); % 在近邻子字典上解最小二乘 Dn D(:, idx); coeff Dn \ x; % 仿射约束系数和为1 s sum(coeff); if abs(s) 1e-12 coeff zeros(kNN, 1); else coeff coeff / s; end % 放回全尺寸编码向量 code zeros(size(D, 2), 1); code(idx) coeff; end这个函数逐行看dist是x到全部码字的欧氏距离用sort排序后取前kNN个索引这是“局部受限”的体现。第二步Dn \ x是用反斜杠解最小二乘MATLAB会自动走QR或正规方程路线对小规模子矩阵效率很高。第三部分做仿射归一化也就是让系数相加等于1这是LLC原论文约束条件在近似解里的保留它保证了编码对特征的尺度变化有一定鲁棒性。最后把系数放回全尺寸向量其他位置补零。kNN的值一般取5到10。太小子字典表达能力不够重建不充分太大局部性被稀释编码退化成全局线性编码。在1024码书下我固定kNN5精度最高且计算量最小。需要说明的是这个版本去掉了原目标函数里的距离惩罚项λ实际效果差异很小但代码简单且数值更稳定。逐特征调用时几万次循环在MATLAB里会比较慢可以使用parfor把外层循环并行化或者直接写成矩阵运算批量处理后面进阶章节会提一下优化方向。4. 空间金字塔池化与线性SVM把局部编码变成图像分类决策编码完成后一张图得到了几千到几万个局部编码向量每个向量维度等于码书大小1024。分类器无法直接处理这种长度不固定的输入所以要把它们聚合成一个固定维度的图像级向量。这一步通常使用空间金字塔匹配加上最大池化。4.1 为什么要用空间金字塔局部编码缺少位置信息如果只是把一张图所有特征编码做一次池化等于完全丢掉特征的空间位置信息。比如一张蓝天草地各占一半的图和一张蓝天在中间草地绕一圈的图池化结果几乎一样。空间金字塔匹配的做法是把图像按1乘1、2乘2、4乘4等尺度切格子每个格子分别池化再把所有格子的结果拼起来。这样既保留了全局轮廓又保留了不同尺度下的局部空间分布。实现时有一个隐含前提局部特征的采样网格要和金字塔格子能对应上。我用的是最省事的近似方案假设dense SIFT按Step4均匀采样那就在图像高度上近似有floor(imgH/4)个采样点宽度上同理。把编码结果按这个网格reshape成三维矩阵再做分块池化。对于实验性质的项目这个近似足够稳定不需要真正处理边缘特征点坐标的精确映射。4.2 最大池化与平均池化的取舍每个格子内部常见池化方式有最大池化和平均池化两种。最大池化取格子里所有编码逐维的最大值响应最强的码字在特征里保留下来这符合视觉词袋里“这个区域存在某个视觉模式”的语义平均池化则是把响应做了平滑信息更稠密但判别性偏弱。在LLC场景下我实际测试的结果是最大池化普遍高2到3个点因为LLC系数已经通过仿射归一化把尺度对齐过最大值保留的是最稳定的激活模式。平均池化适合特征本身已经是稠密表示的场景比如CNN的feature map用在LLC编码上容易把弱响应噪声也带了进来。金字塔级数选择上1乘1加2乘2加4乘4是最常见的配置对应向量维度1024乘(1416)等于21504维。再往上加到8乘8维度到69632精度提升往往只有零点几个百分点SVM训练时间却成倍增加不值得。function spFeat pyramid_pooling(codeMap, levels) % codeMap: H x W x KH和W是采样网格尺寸K是码书大小 spFeat []; for l levels gh round(size(codeMap, 1) / l); gw round(size(codeMap, 2) / l); for i 1:l for j 1:l % 取出当前金字塔格子 patch codeMap((i-1)*gh1 : i*gh, ... (j-1)*gw1 : j*gw, :); % 最大池化先沿高再沿宽取max m squeeze(max(max(patch, [], 1), [], 2)); spFeat [spFeat; m(:)]; end end end % L2归一化 spFeat spFeat / (norm(spFeat) eps); end这个函数里levels通常传[1 2 4]。每次循环会按级别把格子切开每个格子内部做两次max操作先对高度维度取最大值得到1乘W乘K再对宽度维度取最大值得到1乘1乘Ksqueeze后变成K乘1的列向量。最终把所有格子的向量拼接并做L2归一化。L2归一化这一步不能省。因为不同图像的边缘长度、有效特征数量不一致池化后的特征向量的模长天然有差异。SVM对输入尺度敏感如果不归一化模长大的图像会在距离计算里占主导分类边界被少数图像带偏。归一化之后每张图像的特征向量都被映射到单位超球面上距离度量变得公平。4.3 用fitcecoc训练线性SVMMATLAB自带的fitcecoc接口做多类SVM很方便它内部会把多分类问题拆成一对其余或者一对一。对LLC这种高维稀疏特征用线性核就够不需要RBF核。RBF核需要额外调gamma而且在高维空间里容易过拟合训练时间也长。% trainFeat: N x 21504 的训练特征 % trainLabel: N x 1 的类别标签 tpl templateSVM(KernelFunction, linear, BoxConstraint, 1); mdl fitcecoc(trainFeat, trainLabel, Learners, tpl); % 测试 predLabel predict(mdl, testFeat); acc mean(predLabel testLabel); fprintf(Accuracy: %.2f%%\n, acc * 100);BoxConstraint就是SVM的C值控制对误分类样本的惩罚力度。C越大训练集拟合越充分但泛化能力可能下降。LLC特征维度高但每维都有意义C1是通用起点。如果发现训练准确率远高于测试准确率说明过拟合把C调小到0.1反过来训练准确率都上不去就把C调到10试试。还需要注意fitcecoc默认使用类别先验概率加权如果你的数据集类别数量悬殊记得在templateSVM里设置Prior, uniform否则结果会被样本量大的类别主导。5. 避坑笔记LLC图像分类里的5个真实翻车现场这个项目我前后跑过不少次很多坑不是看代码能看出来的必须跑到一半才暴露。挑出五个最常见的翻车现场每条按现象、原因、解决的顺序写清楚。5.1 dense SIFT边界空洞导致金字塔最外圈特征稀疏现象精度比论文复现结果低三四个点且金字塔层数越高掉得越明显查看单张图像的特征分布发现图像四周边界附近几乎没有采样点。原因vl_dsift默认不从图像边界取patchpatch越界就丢弃。4乘4的patch配合Step4大约会在图像四周丢掉16像素宽的区域。对于小图像这一圈空洞可能占到总面积的10%以上。金字塔分块时最外圈的Block恰好大部分落在空洞里统计出来的最大池化值几乎全是0等于给特征向量加了大量无意义的零维度。解决提取特征时显式设置Padding, 8或者更大VLFeat会先把图像边界补齐再采样。补齐方式是在图像外圈复制边缘像素不影响描述子语义。如果已经提取完特征不想重新跑那就在reshape成codeMap时把外圈一行一列丢弃让金字塔分块避开空洞区。5.2 K-means随机初始化不收敛导致两次实验精度差2%现象相同代码、相同数据集连续运行两次精度从83%掉到81%换一次随机种子码书里部分码字完全一致但部分码字明显错位。原因vl_kmeans默认只做一次初始化但K-means是非凸优化最终收敛到哪个局部极小值取决于初始聚类中心。码书一错位LLC近邻搜索选中的子字典就不一样连锁反应到池化和分类。解决把vl_kmeans的NumInit从默认值调大到3到5每个初始化都会跑完整K-means最后选目标函数最小的结果。代价是码书学习时间增加但对离线训练来说完全可以接受。另一个有效办法是先把随机采样到的特征做一次小规模的全局白化再聚类这个在第六章会详细说。总之码书是LLC的地基这个环节必须用多次初始化保证可复现性。5.3 LLC编码系数全是NaN或集中在少数维度现象单张图的编码向量出现NaN或者某些码字在所有图像里的激活次数接近零整列系数稀疏得异常。原因最直接的一种是待编码特征向量x正好是零向量Dn \ x解出来的系数全零再做归一化时分母为零MATLAB直接报NaN。另一种更隐蔽码书D里存在完全相同的码字列导致距离排序时近邻子字典矩阵列线性相关最小二乘解不唯一系数数值极不稳定。解决在llc_encode函数里加一个判断如果abs(sum(coeff))小于1e-12直接把系数置零返回不要继续归一化。码书训练完以后扫一遍删除重复度过高的码字比如两列的欧氏距离小于0.01就只留一列。还有输入特征记得统一用single类型uint8和double混用时数值误差会被距离计算放大。5.4 特征标准化不一致让SVM边界偏向高模长维度现象加了4乘4金字塔后准确率反而比只做2乘2还低猜测一下先看测试集预测概率发现某些类别几乎从不被预测。原因金字塔池化拼接出的向量内部不同块的数值范围差别很大。比如1乘1全局块的某些维度响应值可能到几百而4乘4小格子里平均只有几十。再加上L2归一化是对整个向量做的高数值维度的贡献被放得特别大实际上等于隐式做了特征选择把真正有判别力的小块信息给压没了。解决在做L2归一化之前先对整个训练集特征做一次标准化。正确做法是只在训练集上算每维的均值和标准差再应用到测试集防止测试集信息泄漏进来。标准化后再做L2归一化两个步骤顺序不能反。我用这个改动在一些类别上把单类准确率从60%拉到了75%以上。5.5 类别不平衡下的“假精度”现象全部训练图像的准确率有85%看混淆矩阵却发现在类别数极少的某个类上正确预测的数量为零。原因fitcecoc在训练时会使用类别先验概率做加权样本数多的类别权重高分类边界被推向少数类一侧。整体准确率主要由多数类贡献少数类成了被牺牲的代价。解决在templateSVM里设置Prior, uniform强制各类别权重一致。如果这个调整导致整体准确率下降说明少数类样本本身不足应该考虑对少数类做图像级增强比如轻微旋转和翻转而不是简单复制特征向量。评估时不要只看平均准确率要同时打印混淆矩阵观察每一类对角线的值。我现在的习惯是每次实验都输出混淆矩阵没有这个图就不算实验结束。6. 进阶给SIFT特征先做PCA白化再喂给LLC精度还能再涨一截很多人在LLC框架里只调码书大小和近邻数忽略了特征本身的质量。一个低成本且有效的改进是在提取dense SIFT之后先对训练集的所有描述子做PCA白化再用白化后的特征学码书和做编码。理由很直接LLC的局部性完全建立在欧氏距离上而原始SIFT描述子各维度方差差异很大几个高方差维度会主导近邻搜索让“近邻”变成“在强势维度上近”。PCA白化把各维度方差拉到同一量级距离度量更均衡码书结构也会更稳定。实现方式很简单在收集完训练特征X_train后% X_train: N x 128nsample个SIFT描述子 [coeff, score, latent] pca(X_train); % 白化主成分得分除以标准差 whitenScale 1 ./ sqrt(latent 1e-6); X_white score .* whitenScale; % 用X_white聚类码书 [centers, ~] vl_kmeans(single(X_white), 1024, NumInit, 3); D centers;测试特征也必须用同一个PCA投影和白化参数变换不能单独对测试集重新算PCA否则分布完全不一致。白化后维度仍然是128因为pca默认保留全部分量只是做了正交旋转和缩放。latent是各主成分的方差加1e-6是防止某些主成分方差接近零导致除零。需要注意的是这样做之后编码系数直接对白化特征做重建池化和分类流程完全不变因此对现有代码侵入极小。让我建议一个验证习惯不要在单次随机划分上比较两个方案的准确率而是把数据集随机打乱三次分别跑一版特征处理方案记录平均准确率加减标准差。只有两组结果在三种划分下的均值差超过标准差之和才说明PCA白化真的带来了稳定提升。我在森林图像分类这类纹理较强的数据上白化大约能带来1到2个点的提升在Caltech101上提升稍小。目前最新的图像分类模型确实都转向了Transformer和预训练范式但手头没有GPU、又想快速验证算法机理时LLC加白化这套组合仍然值得保留在工具箱里。跑实验时我也会把码书前几个码字对应的图像patch可视化出来看一眼再输出混淆矩阵确认整个流程是通的最后才关心准确率数字本身。希望帮到你。本文还有配套的精品资源点击获取
返回列表