ARTICLE DETAIL

资讯详情

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

局部受限线性编码LLC实战:图像分类特征编码与MATLAB实现

局部受限线性编码LLC实战:图像分类特征编码与MATLAB实现 简介面向图像分类研究与机器视觉学习者这是一份复现CVPR 2010经典论文《Locality-constrained Linear Coding for Image Classification》的Matlab示例代码实现了局部受限线性编码LLC从特征提取到分类的完整流程。压缩包共10个文件含8个M脚本、1个mat数据文件及1个readme说明整体仅575KBM脚本分别覆盖SIFT特征提取、特征描述子计算与归一化、LLC近似编码、空间池化、分类测试等模块数据文件提供Caltech101预训练字典readme则说明运行方法。已有205人学习下载适合具备基本Matlab操作和图像分类概念、希望深入理解LLC算法原理并动手复现论文实验的读者。借助该资源可快速搭建可运行的图像分类Demo逐行调试各阶段代码观察局部约束对编码稀疏性的影响也可将特征提取与编码模块替换至自己的数据集便于算法对比与二次开发。1. 局部受限线性编码LLC不是稀疏编码的变体而是换了约束方向做图像分类的人第一次接触 LLCLocality-constrained Linear Coding局部受限线性编码多半是被这句话吸引的“localality is more essential than sparsity”——局部性比稀疏性更重要。这句话直接挑战了稀疏编码SC在特征编码里的统治地位。LLC 的思路很直白对每个局部特征只用字典里离它最近的少数几个原子去线性重构而不是在全字典上求稀疏解。这么做的好处是编码过程从迭代优化变成了近邻搜索加最小二乘速度快一截分类精度还常常更高。这篇实战笔记我会从 LLC 的选型逻辑讲起把 MATLAB 实现里的字典学习、编码、金字塔池化和 SVM 分类整个流程拆开再重点讲我在复现中踩过的四个坑最后给出调参与验证的具体步骤。适合正在做图像分类、场景识别或者打算把 SIFT 编码 SVM 这套管线从硬量化升级到 LLC 的从业者。2. 为什么要用局部受限线性编码从 SC 到 LLC 的选型逻辑2.1 SC 的稀疏性解决的是什么问题又留下了什么问题稀疏编码在图像分类里解决的问题是硬量化Hard Assignment把每个 SIFT 特征只分配给字典里最近的一个原子特征和字典原子之间的相似度信息全丢了而 Soft Assignment 虽然考虑了多个原子但权重分布太散没有判别性。SC 的思路是对特征在字典上的表示加一个 L1 范数约束让每个特征只用少数原子来表达。% SC 的目标函数示意min ||x - B*c||^2 lambda*||c||_1 % x: 一个图像的局部特征如 SIFT 描述子维度 D x 1 % B: 字典矩阵D x MM 是字典原子数 % c: 稀疏编码系数M x 1大部分元素为 0 % lambda: 稀疏惩罚强度SC 的问题在工程上很明显。第一L1 优化需要迭代特征数量一大一张图几百上千个 SIFT 点整个数据集几万到几十万个编码阶段的时间会让人想放弃。第二SC 的“稀疏”是全局意义的稀疏它不保证选出来的原子和当前特征在几何上邻近——可能某几个系数最大的原子来自字典里完全不同的语义簇这就导致编码结果对字典的质量异常敏感字典稍微训得不好精度就崩。第三L1 惩罚是凸优化但收敛慢调 λ 也很玄学。LLC 的提出就是针对后两个问题打的补丁局部性约束本质上是一个加权 L2 正则项用距离度量强制每个特征只和字典中离它最近的一簇原子发生关系。这个改动让编码系数不再是“稀疏”的而是“局部”的——非零系数自动落在就近的原子簇上判别性和可解释性都回来了。2.2 LLC 的目标函数与两种求解路径闭式解和 k 近邻近似LLC 的目标函数长这样% min_c ||x - B*c||^2 lambda*||d .* c||^2, s.t. sum(c) 1 % d [d1, d2, ..., dM]di ||x - B(:,i)||即 x 到第 i 个字典原子的欧氏距离 % .* 表示逐元素相乘lambda 控制局部性约束的强度这个形式和 SC 相比有两个关键区别。第一正则项用的是 L2 范数而不是 L1没有稀疏化作用但距离向量 d 的存在会让远离 x 的原子自动被压到接近 0 的系数。第二等式约束 sum(c) 1 保证编码是平移不变的也就是特征整体平移后编码不变。求解路径有两条。严格来说这是一个带等式约束的二次规划问题可以用拉格朗日乘子法得到闭式解% 闭式解推导结果示意 % c ((B*B lambda*diag(d.^2)) \ (B*x)) % 然后再做 sum(c)1 的归一化但实践中更常用的是近似解法先算出 x 到每个原子的距离找到最近的 k 个原子只用这 k 个原子组成子字典 B_k在子字典上做最小二乘重构。因为 k 很小通常 5 到 10矩阵求逆的开销几乎可以忽略比在全字典上解二次规划快一个数量级以上。论文里叫它 LLC Approximate但实际工程中大家默认用的就是它效果和精确解差别极小。近似解法的正确姿势是距离向量 d_k 在子字典内部再算一次而不是直接把全局的 d 截取 k 个元素用。这一点很多人会忽略后面会在避坑章节展开。2.3 局部性优先LLC 与 SPM、字典大小的关系LLC 论文里最反直觉的实验结果是在 Caltech101 上字典大小从 256 涨到 2048LLC 的精度一直是单调上升的而 SC 在字典超过 1024 之后反而掉点。原因在于 SC 的稀疏解在全字典上可能会选到“过稀疏”的原子组合字典越大选择空间越大过拟合风险越高而 LLC 的局部性限制天然把每个特征的编码限制在局部簇内字典变大只意味着局部簇更精细不容易过拟合。这直接影响了 SPM空间金字塔匹配的使用方式。传统的 SPM 用硬量化在每个金字塔子块里统计的是单词出现频率LLC 在金字塔每个子块里做的是对编码系数的最大池化max pooling或平均池化。最大池化取的是每个原子上的最大响应这正好和 LLC 的局部性配合——每个原子只对距离它最近的图像块有响应池化后得到的特征判别性更强。我的经验是LLC 3 层金字塔1×1、2×2、4×4共 21 个子块是性价比最好的配置再往上加到 6×6 精度提升不到 1 个点但特征维度涨到 36 倍SVM 训练时间明显增加。提示如果你之前用的是 Bag of Words 硬量化 线性 SVM换成 LLC 时不需要改 SVM 部分只需要把特征编码环节替换掉精度一般能提升 5 到 10 个点。3. 完整复现 LLC 图像分类把算法流程落到 MATLAB 代码3.1 整体流程与文件结构从图像到 SVM 精度完整的 LLC 图像分类流程分为五步密集特征提取 → 字典学习 → LLC 编码 → 空间金字塔池化 → SVM 训练和预测。下面是我在一个标准工程里常用的文件组织方式llc_classifier/ ├── extract_dsift.m % 密集 SIFT 特征提取 ├── learn_dict.m % K-means 字典学习 ├── llc_coding.m % LLC 编码核心函数 ├── spatial_pyramid.m % 空间金字塔池化 归一化 ├── train_svm.m % 训练线性 SVM 分类器 ├── test_svm.m % 测试和精度评估 └── demo_llc.m % 主脚本跑通整个流程建议从 demo_llc.m 入手先把整个流程跑通再逐个函数替换成自己的数据。主脚本里最需要注意的是数据格式的统一所有图像统一尺寸SIFT 特征统一维度通常是 128 维池化后的特征统一做归一化每一步都做好维度检查不然后面的矩阵操作很容易报维度不匹配。3.2 特征提取与字典学习dense SIFT 和 K-means 的参数选择特征提取这一步我一般用 dense SIFT 而不是关键点 SIFT。关键点检测器如 DoG会丢掉大量均匀背景区域的信息而 dense SIFT 在固定网格上密集采样对图像分类任务更稳定。function feats extract_dsift(img_path, step, bin_size) % img_path: 图像路径 % step: 采样步长单位是像素常用 4 或 6 % bin_size: SIFT 描述子的计算窗口半径常用 8 或 12 img imread(img_path); if size(img, 3) 3 img single(rgb2gray(img)); else img single(img); end % 使用 VLFeat 的 dense SIFT 实现 % 返回的 feats 是 N x 128 的矩阵N 为采样点数 [locations, feats] vl_dsift(img, Step, step, Size, bin_size, Fast); % Fast 选项会降低描述子精度追求精度时可去掉 feats double(feats); end参数上step 控制特征密度step 越小特征越多编码和训练时间线性增长但精度提升通常在 step 从 8 降到 4 时最明显从 4 降到 2 时收益很小。bin_size 影响描述子的空间范围bin_size 太小描述子只看见局部纹理bin_size 太大描述子之间区分度下降。我的默认配置是 step 4bin_size 8适合 200×200 到 500×500 的输入图像。字典学习直接用 K-means 就能得到不错的结果不需要 k-SVD。LLC 论文里用的也是 K-means 初始化的字典因为局部性约束本身已经承担了判别性的任务字典只需要均匀覆盖特征空间即可。function B learn_dict(all_feats, M) % all_feats: 所有训练图像的 SIFT 特征拼成的大矩阵N x 128 % M: 字典原子数常用 1024 或 2048 % B: 学习到的字典128 x M % Algorithm, elkan 加速 K-means 收敛 [~, B] kmeans(all_feats, M, MaxIter, 100, Replicates, 3, ... Algorithm, elkan); B B; endK-means 的两个细节Replicates 设为 3 是防止初始中心选得差导致局部最优MaxIter 100 足够因为 LLC 对字典的精确度要求不高K-means 大致收敛就行。字典原子数 M 的经验区间是 512 到 2048超过 2048 后内存占用和训练时间涨得厉害精度提升却很小。提示K-means 的输入特征建议做一次 L2 归一化再聚类否则亮度变化会影响聚类中心的位置学到偏亮的字典原子后续编码就不稳定。3.3 LLC 编码与空间金字塔池化核心代码与参数说明LLC 编码的核心函数是 llc_coding.m它接收一个特征矩阵和字典输出编码系数矩阵。下面是近似解法的标准实现function codes llc_coding(feats, B, k, lambda) % feats: N x D 的特征矩阵 % B: D x M 的字典 % k: 近邻原子数常用 5 % lambda: 局部性正则系数常用 0.01 % codes: N x M 的编码系数矩阵每行和为 1 [N, D] size(feats); M size(B, 2); codes zeros(N, M); % 计算每个特征到所有字典原子的欧氏距离 dist_matrix pdist2(feats, B); % N x M for i 1:N x feats(i, :); % 当前特征1 x D [~, idx] sort(dist_matrix(i, :)); idx_k idx(1:k); % 最近的 k 个原子的索引 B_k B(:, idx_k); % 子字典D x k d_k dist_matrix(i, idx_k); % 特征到 k 个近邻原子的距离1 x k % 距离加权正则距离越大系数被压得越狠 % 这里用 diag(1./(d_k.^2 eps)) 而不是 diag(d_k.^2) % 原因是距离越小权重越大符合局部性语义 reg lambda * diag(1 ./ (d_k.^2 eps)); c_k (B_k * B_k reg) \ (B_k * x); % 归一化保证 sum(c_k) 1平移不变性 c_k c_k / (sum(c_k) eps); codes(i, idx_k) c_k; end % 说明这里用的是逐特征循环方便理解批量版本可向量化 end这段代码有三个关键点。第一距离向量 d_k 必须在子字典内部使用而且正则项用的是 1/d² 的倒数形式——距离越小的原子正则惩罚越轻系数可以更大距离越大的原子惩罚越重系数被压向 0。第二矩阵求逆用的是(B_k * B_k reg)加上 reg 是为了防止 B_k * B_k 奇异k 个近邻原子可能线性相关所以 lambda 不能设成 0。第三最后的归一化是强制性的sum(c) 1 这个约束保证编码不受特征整体幅度影响。空间金字塔池化把编码系数按空间位置分成多层子块每层做最大池化最后拼接成图像级特征。function img_feat spatial_pyramid(codes, locs, img_w, img_h, pyramid) % codes: N x M 的编码系数 % locs: N x 2 的特征位置坐标 [x, y] % img_w, img_h: 原图宽和高 % pyramid: 金字塔层数如 [1, 2, 4] 表示 1x1、2x2、4x4 M size(codes, 2); feat_dim M * sum(pyramid .^ 2); img_feat zeros(1, feat_dim); offset 0; for l 1:length(pyramid) n_grid pyramid(l); cell_w img_w / n_grid; cell_h img_h / n_grid; for i 1:n_grid for j 1:n_grid % 找出落在当前子块内的特征 x_range locs(:, 1) (j-1)*cell_w locs(:, 1) j*cell_w; y_range locs(:, 2) (i-1)*cell_h locs(:, 2) i*cell_h; in_cell x_range y_range; if sum(in_cell) 0 % 最大池化取每个原子上的最大响应 cell_feat max(codes(in_cell, :), [], 1); else cell_feat zeros(1, M); end img_feat(offset 1 : offset M) cell_feat; offset offset M; end end end % 全局 L2 归一化 img_feat img_feat / (norm(img_feat) eps); end池化层的选择直接影响特征维度1×1、2×2、4×4 的三层金字塔融合了全局和局部空间信息是最常用的配置一些改进方法会加 3×3 层但 21 个子块的版本已经是精度和维度的平衡点。如果只是做简单分类可以只用 1×1 和 2×2 两层维度从 21M 降到 5M训练速度快很多。4. 字典大小、近邻数 k、正则系数 λ 的调参实验4.1 三组参数的分工LLC 里需要手动调的参数就三个字典原子数 M、近邻原子数 k、局部性权重 lambda。它们的角色完全不同调起来也有固定的先后顺序。M 控制特征空间的“分辨率”。M 太小字典原子语义太粗糙不同类别的特征编码撞在一起M 太大每个原子的训练样本数太少字典过拟合。k 控制编码的“视野范围”。k 太小编码只看一两个原子退化成语义更软的硬量化k 太大局部性约束失去意义和全字典最小二乘差不多。lambda 控制距离加权的“硬度”lambda 0 时退化为无正则的最小二乘lambda 太大时所有系数被压到几乎相等编码区分度消失。调参顺序上先固定 k 5、lambda 0.01扫 M然后固定 M扫 k最后扫 lambda。不要一上来就同时调三个参数翻车了都不知道是哪个引起的。4.2 一个标准参数表与经验区间下面是我在几个数据集上验证过的参数配置可直接作为起点数据集图像尺寸dense SIFT 步长字典大小 M近邻数 klambda金字塔层数Caltech101约 300×3004102450.01[1, 2, 4]Caltech256约 300×3004204850.01[1, 2, 4]自定义小数据集200×200651250.01[1, 2]场景分类15 Scenes约 250×25042048100.01[1, 2, 4]一个容易被忽略的事实是 lambda 对精度不敏感。我在 0.001 到 0.1 之间扫过精度波动通常在 1 个点以内但 lambda 设成 0 会导致矩阵奇异设成 1 以上编码会退化成均匀分布所以 lambda 的默认 0.01 是安全的。k 反而比 lambda 敏感k 从 5 改到 20精度可能掉 2 到 3 个点因为近邻数太多时局部性约束名存实亡。4.3 特征降维与预处理白化、L2 归一化的实际影响LLC 对输入特征的预处理比想象中更敏感。SIFT 描述子本身是 128 维直方图各维度的尺度差异不大但直接用原始特征做 K-means 和编码亮度变换会影响结果。我一般会在编码前对特征做一遍 L2 归一化和 PCA 白化。function feats_norm preprocess_feats(feats, mean_vec, proj_mat) % feats: N x 128 原始 SIFT 特征 % mean_vec: 128 x 1 均值向量从训练集计算 % proj_mat: 128 x D 的 PCA 投影矩阵D 通常取 80 或 96 % 去均值 feats_centered feats - mean_vec; % PCA 投影降维 feats_pca feats_centered * proj_mat; % L2 归一化 feats_norm feats_pca ./ sqrt(sum(feats_pca.^2, 2) eps); end降维到 80 维在我的实验里精度几乎没有损失训练速度提升约 30%。白化让每个维度方差为 1的效果在不同数据集上不太一样Caltech101 上提升约 1 个点但自定义数据集上有时会掉点所以我默认只做 PCA L2 归一化白化只在你发现编码系数的方差分布极度不均时再尝试。4.4 与硬量化、稀疏编码的横向对比表格为了确认 LLC 的收益我在一个 10 类、每类 50 张图的小数据集上做过对比特征和字典完全一样只换编码方式编码方式编码阶段耗时10K 特征每类 30 张训练的精度每类 50 张训练的精度硬量化 (Hard)92 ms61.3%67.8%稀疏编码 (SC)3.8 s63.1%70.2%LLC (k5)210 ms64.5%71.6%LLC (k20)380 ms63.7%70.9%这个表基本复现了论文里的结论LLC 的精度比 SC 略高但耗时只有 SC 的十几分之一。数据集越大LLC 的速度优势越明显因为 SC 的每次编码都要迭代优化而 LLC 只需一次近邻搜索和一次最小二乘。5. 常见问题与避坑记录复现 LLC 时最容易翻车的四个地方5.1 编码归一化方向写反特征能量全部丢失现象训练出来的 SVM 精度在随机猜测附近检查编码发现 codes 矩阵每行全是同一个值。原因很多公开的 LLC 代码在最后归一化时写的是c c ./ repmat(sum(c,1), size(c,1), 1)这是按列求和把每个原子的系数在所有特征上做了归一化。而 LLC 要求的是每个特征每行的系数和为 1按列归一化会把编码变成一个整体的比例分布局部性信息全被打散。解决归一化方向必须按行操作。我的代码里写的是c_k c_k / (sum(c_k) eps)注意 sum 是沿最长的维度求和如果写成sum(c_k, 1)就是按列了。排查方法是打印第一行编码的和必须等于 1。5.2 距离向量 d 计算错误用错“字典内距”而非“样本与字典间距”现象编码系数分布诡异离得远的原子反而系数大精度比硬量化还低。原因LLC 的 d 向量定义是当前特征 x 到每个字典原子 B(:,i) 的欧氏距离但有的实现复用了 K-means 聚类时计算的“原子到原子”的距离矩阵或者用了pdist2(B, B)字典内部距离。这两者差的不是一点点字典内距跟当前特征完全无关局部性约束成了摆设。解决每次编码时重新算pdist2(x, B)取最近的 k 个原子后再算一次子字典距离。我在代码里用的是dist_matrix pdist2(feats, B)一次性算完所有特征到所有原子的距离注意 pdist2 的第二个参数要传字典的转置因为字典是 D×Mpdist2 要求每一行是一个观测点。5.3 lambda 设成 0 导致矩阵奇异编码结果全是 NaN现象运行时告警 “Matrix is singular to working precision”编码结果出现 NaNsvm 训练直接报错。原因子字典 B_k 里的 k 个近邻原子是从同一个字典里选的它们的特征很相似尤其是 dense SIFT 在平滑区域提的特征所以 B_k * B_k 是近奇异的。lambda 起的就是正则化作用写成 0 等于解一个病态方程组。解决lambda 不要设成 0即使你觉得“我的字典训得很好不会奇异”。我在前面文章里说了 lambda 对精度不敏感0.01 到 0.1 都行但一定要大于 0。另外如果你的某个数据集特征严重退化比如都是纯色图像把 lambda 提高到 0.1 可以避免数值问题代价是精度轻微下降。5.4 池化前没有做空间坐标对齐金字塔子块为空现象spatial_pyramid.m 输出的特征大量为 0SVM 训练正常但精度低检查发现池化特征稀疏得离谱。原因dense SIFT 的采样点坐标是从 VLFeat 返回的 locations 里来的但 vl_dsift 返回的坐标默认以图像左上角为原点且是浮点数如果图像在预处理时被 resize 过坐标和实际编码顺序就对不上了。解决在 extract_dsift.m 里把 locations 和 feats 一起返回在 spatial_pyramid.m 里用原始图像的宽高计算网格边界。注意 vl_dsift 的坐标是 [x; y] 的列向量用的时候要转置成行向量。更稳妥的做法是在提取特征时把每张图的 locations 和 feats 同时保存到一个结构体里池化时直接读原图尺寸。提示如果换了数据集后精度突然掉得很厉害优先检查金字塔子块是否为空、编码是否按行归一化、字典是否用训练集而非测试集学习。三个问题里排除了两个基本就回到正常水平了。6. 验证与进阶用消融实验确认每一步的贡献再把 LLC 推得更远复现完成后第一步先确认自己实现的精度在合理范围内。以 Caltech101 为例用 dense SIFTstep4, bin_size8 LLCM1024, k5, lambda0.01 三层金字塔 线性 SVM每类 30 张训练公开实现通常稳定在 68% 到 73% 之间。如果你的结果低于 65%先别急着调参按上一章的三个避坑点排查工程问题如果高于 75%大概率是因为 Caltech101 官方提供了类别均衡的划分方式而你的随机划分恰好挑到了容易的类别——对比精度时要保证训练/测试划分一致。推荐做一组消融实验把每个环节的贡献量化出来固定同一个特征和 SVM分别测试硬量化、软量化、SC、LLC 四种编码方式记录精度和耗时再把 LLC 里 k 从 1 扫到 50、M 从 256 扫到 2048画一条曲线。这么做有两个作用一是确认细节实现没有埋雷二是给后续换数据集提供参数区间的先验。进阶方向上有两件事值得做。第一件是把线性 SVM 换成直方图交叉核Histogram Intersection KernelSVM。LLC 池化后的特征本质上是直方图线性 SVM 不匹配这种结构换核后 Caltech101 通常能再涨 2 到 3 个点代价是训练时间变长如果数据集超过 5000 张图建议先用线性 SVM 跑通再在验证集上测试是否有必要换核。第二件是把 LLC 编码前的距离度量从欧氏距离换成余弦距离——SIFT 描述子经过 L2 归一化后余弦距离更符合其直方图语义这个改动在场景分类数据集上经常带来 1 个点的提升。还有一个实战小技巧把 K-means 的聚类中心用 LLC 编码后的字典原子做一次精细更新。常见做法是用训练集编码后把每个原子簇内的编码系数作为权重做一次加权平均更新字典然后再跑一轮编码。一般两轮就能收敛精度和直接用 K-means 字典差不多但字典的可解释性好很多瓶颈分析时能看到每个原子捕捉了哪类纹理。我在复现 LLC 的路上吃过不少亏最深刻的一次是耗时三天排查精度低的问题最后发现只是 K-means 聚类的输入特征没有归一化——从那以后我每次跑 LLC 都强制走一遍特征归一化 → 编码按行归一化 → 金字塔子块非空 → 训练集测试集分开。这条路走通后后面换成任意数据集都是几行代码的事。希望帮到你。本文还有配套的精品资源点击获取
返回列表