news 2026/10/7 1:37:14

基于VGG-16的图像检索系统实战:特征提取、余弦相似度与Top3检索

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于VGG-16的图像检索系统实战:特征提取、余弦相似度与Top3检索

简介:这份资源面向人工智能与信息检索方向的学习者与开发者,提供一套基于VGG-16的图像检索系统完整项目实践。项目以深度学习特征替代传统颜色、形状、纹理等手工特征,借助Keras预训练模型完成图像特征提取,并将图像库特征统一存入h5文件构建索引;检索阶段采用夹角余弦度量相似度,对测试图片特征与库中向量逐一计算距离并排序,返回相似度Top3结果,帮助读者理解从特征抽取到相似度匹配的完整检索链路。资源包共253个文件,以241张jpg图像样本为主,另含5个xml标注、3个py脚本、2个pyc编译文件、1个h5特征库及1个iml工程配置,压缩包约41.13MB,目录结构便于按模块查阅。目前已有197人学习下载,适合希望动手复现图像检索流程、掌握特征索引与相似度排序思路的读者参考。

1. 从一堆散图到可检索图库:这套 VGG-16 图像检索系统到底能干什么

你手里有一批图片,命名是 430453.jpg、430448.jpg、430374.jpg 这种纯数字,散在一个目录里,想找一张“和这张差不多”的图,靠文件名根本对不上,靠肉眼一张张翻又太慢。这套基于 VGG-16 的图像检索系统,解决的就是这件事:把图片库里的每张图抽成一个特征向量,存进 h5 文件,检索时把查询图也抽成向量,逐一算夹角余弦,排序取 Top3。它属于人工智能里信息检索方向的一个典型项目实践,技术栈是 Python + Keras 预训练模型,核心文件就一个 vgg_featureCNN.h5 加一个 search-picture.iml 工程描述。适合谁?正在做人工智能大作业、课程设计、毕设选题,或者想搞明白“深度学习特征提取 + 相似度检索”这条链路怎么跑通的人。它不追求工业级召回率,追求的是链路完整、代码能跑、参数能改、坑能看见。下面我按“资源是什么 → 怎么用 → 坑在哪”的顺序,把这份东西拆开讲。

2. 特征提取为什么选 VGG-16:三种预训练模型的取舍与落地

2.1 传统特征和深度特征的差别在哪

传统图像检索常用颜色直方图、形状矩、纹理的灰度共生矩阵、能量这些手工特征。它们的问题是:你定义什么,它就只认什么。换一批光照、换一个拍摄角度,颜色直方图可能就崩了。深度特征不一样,VGG-16 在 ImageNet 上预训练过,卷积层学到的是一层层从边缘、纹理到语义部件的表达,最后全连接层之前的输出是一个 4096 维(或全局池化后的定长)向量,这个向量对语义相似更敏感。

摘要里写得很清楚:为了简便,直接用 Keras 的预训练模型,可选 vgg16、resnet50、densenet121。这三个不是随便列的。VGG-16 结构规整、层数适中、特征稳定,适合当 baseline;ResNet50 有残差连接,深但不容易退化;DenseNet121 特征复用强,参数少。选 VGG-16 的理由通常是:它的特征在中小规模图库上表现稳,Keras 里include_top=False一设就能拿到卷积特征,不用自己搭网络。代价是它的全连接层参数量大,推理比 ResNet 慢一些,特征维度也偏高。

2.2 用 Keras 抽特征的完整代码

下面这段是抽特征入库的核心逻辑,我按常见做法补全了输入尺寸和池化方式,你照着改路径就能跑。

import os import numpy as np from keras.applications.vgg16 import VGG16, preprocess_input from keras.preprocessing import image from keras.models import Model # 加载 VGG-16,去掉顶部分类层,只要卷积特征 base_model = VGG16(weights='imagenet', include_top=False, input_shape=(224, 224, 3)) # 加全局平均池化,把 (7,7,512) 压成 512 维,省内存也好算余弦 x = base_model.output x = keras.layers.GlobalAveragePooling2D()(x) model = Model(inputs=base_model.input, outputs=x) def extract_feature(img_path): img = image.load_img(img_path, target_size=(224, 224)) arr = image.img_to_array(img) arr = np.expand_dims(arr, axis=0) arr = preprocess_input(arr) # VGG 专用的减均值处理 feat = model.predict(arr) return feat.flatten() # 变成一维向量

逻辑说明:include_top=False是关键,去掉 ImageNet 的 1000 类分类头,保留卷积基。GlobalAveragePooling2D把空间维度压掉,得到 512 维向量,比直接 flatten 的 25088 维省 50 倍存储,检索时余弦计算也快得多。preprocess_input不能省,VGG 训练时做了特定的通道均值减法,你不做,特征分布就偏了,检索结果会明显变差。

参数说明:input_shape=(224,224,3)是 VGG-16 的标准输入,改小可以提速但会损失细节;weights='imagenet'表示用预训练权重,第一次运行会下载,之后走本地缓存。如果你换成 resnet50 或 densenet121,只需改 import 和模型名,池化那步逻辑通用。

2.3 索引化数据集:把特征写进 h5

摘要说“为了简便,存到 h5 文件中”。h5 的好处是单文件、带键、能存多维数组,读的时候按 key 取,不用自己解析二进制。下面是把整个图库抽完存 h5 的写法。

import h5py img_dir = './images' img_list = [f for f in os.listdir(img_dir) if f.lower().endswith(('.jpg', '.png'))] with h5py.File('vgg_featureCNN.h5', 'w') as h5f: for name in img_list: feat = extract_feature(os.path.join(img_dir, name)) # 用文件名当 key,检索时能直接映射回原图 h5f.create_dataset(name, data=feat)

逻辑说明:遍历图库目录,每张图抽一个向量,用文件名做 dataset 的 key。这样检索出 Top3 之后,直接拿 key 就能找到原图,不需要额外维护一张 id 映射表。参数说明:create_dataset默认不压缩,图库大时可以加compression='gzip',但读取会慢一点;文件名里如果有斜杠或特殊字符,key 会出问题,入库前最好统一重命名。

提示:h5 文件是单写多读的结构,抽特征时别多个进程同时写同一个文件,会损坏。要并行就每个进程写各自的 h5,最后合并。

3. 相似度计算与检索排序:夹角余弦怎么落到代码里

3.1 为什么用夹角余弦而不是欧氏距离

摘要列了一串:欧氏距离、曼哈顿距离、闵可夫斯基距离、切比雪夫距离、夹角余弦。这里选夹角余弦是有道理的。深度特征向量的绝对长度受图像亮度、对比度影响,但方向更能反映语义内容。余弦只看两个向量的夹角,对长度不敏感,所以同一类图即使明暗不同,余弦相似度依然高。欧氏距离会把长度差也算进去,容易把“亮一点的同类图”排到后面。

代价是:余弦对零向量无定义,如果某张图抽出来全是零(极少见但可能),要单独处理。另外余弦相似度范围是 [-1,1],实际深度特征基本落在 [0,1],排序时从大到小取 Top3 即可。

3.2 检索 Top3 的完整实现

import h5py import numpy as np def cosine_sim(a, b): # 分子点积,分母模长乘积,加 1e-8 防除零 return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8) def search(query_path, h5_path='vgg_featureCNN.h5', topk=3): q_feat = extract_feature(query_path) scores = [] with h5py.File(h5_path, 'r') as h5f: for key in h5f.keys(): db_feat = h5f[key][:] sim = cosine_sim(q_feat, db_feat) scores.append((key, sim)) # 按相似度降序,取前 topk scores.sort(key=lambda x: x[1], reverse=True) return scores[:topk] if __name__ == '__main__': result = search('430453.jpg') for name, s in result: print(name, round(s, 4))

逻辑说明:先抽查询图特征,再遍历 h5 里所有 key,逐个算余弦,存进列表后排序取前 3。参数说明:topk默认 3,对应摘要里的 Top3,想多看几张改成 5 或 10 都行;1e-8是数值稳定项,防止某张图特征全零导致除零报错。如果图库上万张,这个 for 循环会慢,常见做法是把所有特征读成一个矩阵,用矩阵乘法一次算完,但那是优化阶段的事,先跑通再说。

3.3 检索结果怎么验证对不对

跑完 search,输出的是文件名和相似度分数。验证方法很直接:拿一张已知类别的图当查询,看 Top3 里有没有同类图。比如 430453.jpg 和 430448.jpg 如果肉眼看着像,检索结果里它俩应该互相排前面。如果 Top3 全是无关图,先别怀疑算法,按顺序查三件事:特征提取时preprocess_input有没有漏、h5 里的特征是不是同一模型抽的、查询图和库图的尺寸处理是否一致。这三步能排掉八成“检索不准”的问题。

注意:h5 里的 key 顺序和文件系统遍历顺序不一定一致,别依赖顺序做映射,永远用 key 回查原图。

4. 避坑与排查:这套检索系统最容易翻车的五个地方

4.1 检索结果全是同一张图或分数异常接近

现象:Top3 返回的相似度都在 0.99 以上,或者反复出现同一张图。原因:查询图本身就在图库里,余弦相似度自然接近 1,这是正常的;但如果查询图不在库里还这样,多半是特征向量没做归一化,或者 h5 里存的是未池化的高维向量,数值尺度失控。解决:确认抽特征时走了GlobalAveragePooling2D,并在入库前对向量做 L2 归一化,检索时余弦就退化成点积,分数分布更合理。

4.2 h5 文件读取报 “Unable to open file”

现象:h5py.File打开时报错,或者读出来的 dataset 是空的。原因:上一次写入进程没正常关闭文件,h5 结构损坏;或者路径写成了相对路径,工作目录变了。解决:写入时用with语句保证关闭;损坏的 h5 基本救不回来,重新抽一遍特征比修复快。路径统一用绝对路径或基于os.path.dirname(__file__)拼。

4.3 换模型后检索效果反而变差

现象:把 VGG-16 换成 ResNet50 或 DenseNet121,Top3 准确率下降。原因:不同模型的preprocess_input不一样,ResNet 用的是 caffe 模式的通道减法,DenseNet 又是另一套。你只改了模型名没改预处理,特征分布就错了。解决:每个模型配对应的预处理函数,from keras.applications.resnet50 import preprocess_input这样分开导入,别混用。

4.4 图库稍大就内存爆掉

现象:图库到几千张时,抽特征或检索阶段内存占用飙升。原因:一次性把所有图读进内存,或者 h5 读取时把整个 dataset 复制成 numpy 数组没释放。解决:抽特征时一张一张读、抽完即写 h5,不要攒列表;检索时如果图库大,分批读 h5 算相似度,或者预先算好归一化矩阵存成 npy,用矩阵乘法替代循环。

4.5 文件名带中文或空格导致 key 错乱

现象:h5 里存进去的 key 和实际文件名对不上,检索出来找不到原图。原因:h5py 的 key 对特殊字符敏感,中文和空格在某些版本下会出问题。解决:入库前统一重命名成数字或拼音,保留一张映射表;或者用str(i)当 key,另存一个id2name.json。这一步看着土,但能省掉后面大量排查时间。

5. 进阶技巧:把检索从“能跑”推到“好用”的几个参数

先说一个具体技巧:特征归一化 + 矩阵化检索。前面检索是 for 循环逐个算余弦,图库上千张就明显卡。做法是入库时就把每个向量 L2 归一化,检索时把所有特征读成一个(N, 512)的矩阵,查询向量(512,)直接和矩阵做点积,一次得到 N 个相似度。代码上就是把 h5 遍历换成np.stack,然后scores = db_matrix @ q_feat。这一步能把检索从秒级压到毫秒级,而且不改变排序结果,因为归一化后余弦就是点积。

再说验证方法。想确认检索链路没退化,固定一组查询图,每次改完参数跑一遍,记录 Top3 命中率。命中率的定义可以简单点:Top3 里只要有一张和查询图同目录或同前缀,就算命中。这个指标不严谨,但足够发现“改了预处理之后效果突然掉一半”这种问题。我一般会留一个eval.py,把查询图列表和预期命中写死,改完代码跑一下,比肉眼翻结果靠谱。

参数上还有两个可调点。一是输入尺寸,224 是 VGG-16 的标准,改成 256 或 299 会改变特征粒度,但要注意模型对输入尺寸有下限,太小会丢细节。二是池化方式,全局平均池化得到 512 维,全局最大池化对显著特征更敏感,两者可以都试,看你的图库偏纹理还是偏物体。没有绝对优劣,只有适不适合你这批图。

最后说个血泪经验:这套系统最容易被忽视的不是模型,是数据一致性。查询图和库图必须走完全相同的预处理、相同的模型、相同的池化。任何一环不一致,检索结果就是玄学。从那以后我每次改完抽特征代码,都强制拿同一张图分别走查询和入库两条路径,确认两次抽出的向量余弦是 1.0,才继续往下跑。这个自检习惯帮我省掉了无数次“明明代码没改为什么结果变了”的排查。希望帮到你。

本文还有配套的精品资源,点击获取

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

FP7195升降压恒流驱动IC实战设计指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:34:45

嵌入式开发者如何借助AI优雅管理Git版本与固件协作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:34:21

YOLOv5扑克牌检测实战:从数据集格式到训练避坑全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:33:06

随机森林鸢尾花分类实战:从原理到调参完整教程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:32:50

JavaWeb个人网上银行系统:MVC架构、数据库设计与部署避坑全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:32:47

C++飞机大战源码解析:从版本演进学EasyX游戏开发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华