:畢業(yè)設(shè)計從特征提取到可復(fù)現(xiàn)落地)
簡介這是一套面向高校學(xué)生與深度學(xué)習(xí)入門者的圖像檢索畢業(yè)設(shè)計完整項目基于VGG16卷積神經(jīng)網(wǎng)絡(luò)實現(xiàn)以圖搜圖功能。項目使用Python與Keras搭建將圖像轉(zhuǎn)換為高維特征向量再通過余弦相似度或歐氏距離完成相似圖像匹配覆蓋圖像預(yù)處理、特征提取、相似度計算與檢索全流程可直接運行并根據(jù)自有數(shù)據(jù)集調(diào)整。資源包共30個文件約47.96MB包含8個Python腳本模型構(gòu)建、訓(xùn)練與檢索邏輯、5個gif與5個png演示圖、3個js與2個html及2個css前端頁面、1個db數(shù)據(jù)庫、1個md說明文檔等目錄結(jié)構(gòu)清晰便于按模塊閱讀與二次開發(fā)。目前已有430人學(xué)習(xí)下載。讀者可借此掌握VGG16特征提取、Keras預(yù)訓(xùn)練模型調(diào)用、圖像檢索算法與完整項目組織方式是理解深度學(xué)習(xí)落地應(yīng)用、完成畢業(yè)設(shè)計的實用參考。1. 基于 VGG16 的圖像檢索系統(tǒng)從特征提取到可復(fù)現(xiàn)的畢業(yè)設(shè)計落地做畢業(yè)設(shè)計最怕兩件事一是選題聽起來高大上真動手發(fā)現(xiàn)無從下手二是代碼跑不通數(shù)據(jù)找不到最后只能“借鑒”別人的倉庫改個名字?;?VGG16 的圖像檢索系統(tǒng)恰好卡在中間——它足夠經(jīng)典經(jīng)典到任何計算機畢業(yè)設(shè)計答辯老師都認(rèn)可這個方向又足夠具體具體到你可以用一份完整代碼和數(shù)據(jù)在本地跑出可演示的結(jié)果。這個系統(tǒng)的核心邏輯并不復(fù)雜用預(yù)訓(xùn)練的 VGG16 卷積網(wǎng)絡(luò)把每張圖片變成一個高維特征向量再通過計算向量之間的余弦相似度或歐氏距離從圖庫里找出與查詢圖最接近的 Top-K 張。它解決的是“以圖搜圖”這個真實需求適合計算機視覺入門、信息檢索課程設(shè)計以及需要快速搭建可演示系統(tǒng)的畢業(yè)設(shè)計場景。下面我會把選型理由、代碼實現(xiàn)、參數(shù)調(diào)優(yōu)和踩坑記錄一層層拆開讓你拿到就能跑跑完能講清楚為什么這么做。2. VGG16 做圖像特征提取為什么選它而不是 ResNet 或 CLIP2.1 VGG16 作為特征提取器的結(jié)構(gòu)優(yōu)勢與局限VGG16 是牛津大學(xué)視覺幾何組在 2014 年提出的卷積神經(jīng)網(wǎng)絡(luò)拿過 ImageNet 挑戰(zhàn)賽的亞軍。它的結(jié)構(gòu)極其規(guī)整13 個卷積層全部使用 3×3 小卷積核5 個最大池化層穿插其中最后接 3 個全連接層。這種“堆疊小卷積核”的設(shè)計讓網(wǎng)絡(luò)在保持感受野的同時減少了參數(shù)量也讓它提取的特征具有很好的層次性——淺層卷積響應(yīng)邊緣和紋理深層卷積響應(yīng)語義部件。在圖像檢索任務(wù)里我們通常不會用 VGG16 最后的 softmax 分類輸出而是取全連接層之前的特征。常見做法是取block5_pool之后的 512 維特征或者取fc2層的 4096 維特征。512 維特征計算快、存儲省適合圖庫規(guī)模在幾千到幾萬張的場景4096 維特征表達能力更強但檢索時的距離計算開銷會明顯上升。我一般會先用 512 維跑通全流程如果發(fā)現(xiàn)相似圖片排不到前面再切到 4096 維對比效果。VGG16 的局限也很明顯參數(shù)量約 1.38 億其中全連接層占了絕大部分導(dǎo)致模型文件超過 500MB推理速度比 MobileNet 慢不少。如果你的畢業(yè)設(shè)計需要部署到邊緣設(shè)備或者要求實時響應(yīng)VGG16 可能不是最優(yōu)選。但如果你追求的是“特征穩(wěn)定、代碼簡單、答辯好講”VGG16 依然是性價比很高的選擇。ResNet 的殘差連接雖然訓(xùn)練更深網(wǎng)絡(luò)更容易但作為固定特征提取器時VGG16 在中小規(guī)模圖庫上的檢索精度并不遜色。CLIP 這類多模態(tài)模型效果更好但依賴大規(guī)模預(yù)訓(xùn)練權(quán)重和更復(fù)雜的推理流程對畢業(yè)設(shè)計的算力要求偏高。2.2 用 PyTorch 加載 VGG16 并導(dǎo)出特征向量的最小代碼下面這段代碼展示了如何加載預(yù)訓(xùn)練 VGG16、去掉分類頭、對單張圖片做預(yù)處理并輸出 512 維特征向量。代碼可以直接復(fù)制運行前提是你已經(jīng)安裝了 PyTorch 和 torchvision。import torch import torch.nn as nn from torchvision import models, transforms from PIL import Image # 加載預(yù)訓(xùn)練 VGG16weights 參數(shù)指定使用 ImageNet 預(yù)訓(xùn)練權(quán)重 vgg16 models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) # 只保留 features 部分卷積層去掉 avgpool 和 classifier # features 的輸出是 (batch, 512, 7, 7)需要進一步處理 feature_extractor vgg16.features # 設(shè)置為評估模式關(guān)閉 dropout 和 batchnorm 的訓(xùn)練行為 feature_extractor.eval() # 定義圖像預(yù)處理縮放到 224x224轉(zhuǎn)張量按 ImageNet 統(tǒng)計量歸一化 preprocess transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def extract_feature(img_path): img Image.open(img_path).convert(RGB) input_tensor preprocess(img).unsqueeze(0) # 增加 batch 維度 with torch.no_grad(): features feature_extractor(input_tensor) # 形狀 (1, 512, 7, 7) # 全局平均池化把 7x7 空間維度壓成 1x1得到 512 維向量 features torch.mean(features, dim[2, 3]) # L2 歸一化方便后續(xù)用余弦相似度 features nn.functional.normalize(features, p2, dim1) return features.squeeze().numpy() if __name__ __main__: vec extract_feature(test.jpg) print(vec.shape) # 輸出 (512,)這段代碼的關(guān)鍵點有三個。第一vgg16.features只包含卷積層輸出是四維張量(batch, 512, 7, 7)其中 7×7 是空間尺寸。第二全局平均池化把每個通道的 7×7 特征圖取平均得到 512 維向量這比直接展平更魯棒也避免了全連接層的巨大參數(shù)量。第三L2 歸一化讓向量落在單位球面上之后用余弦相似度檢索時只需做點積計算效率更高。參數(shù)方面Resize(256)和CenterCrop(224)是 ImageNet 的標(biāo)準(zhǔn)預(yù)處理流程不要隨意改成 224 直接縮放否則會改變圖像長寬比影響特征質(zhì)量。歸一化的均值和標(biāo)準(zhǔn)差必須和預(yù)訓(xùn)練時一致否則特征分布會偏移。如果你用的是自己訓(xùn)練的 VGG16 權(quán)重歸一化參數(shù)要換成訓(xùn)練時用的統(tǒng)計量。2.3 批量提取圖庫特征并保存為可檢索的索引文件單張?zhí)崛≈皇茄菔緦嶋H系統(tǒng)需要對整個圖庫做批量處理。下面代碼遍歷指定目錄下所有圖片提取特征并保存為.npy文件同時保存文件名列表方便后續(xù)根據(jù)索引找回原圖。import os import numpy as np from tqdm import tqdm from extract_feature import extract_feature # 假設(shè)上面的函數(shù)保存在 extract_feature.py def build_index(image_dir, output_dir): os.makedirs(output_dir, exist_okTrue) image_paths [] features [] # 支持的圖片格式 valid_ext {.jpg, .jpeg, .png, .bmp, .webp} for fname in tqdm(os.listdir(image_dir)): ext os.path.splitext(fname)[1].lower() if ext not in valid_ext: continue fpath os.path.join(image_dir, fname) try: vec extract_feature(fpath) features.append(vec) image_paths.append(fpath) except Exception as e: print(f跳過 {fpath}: {e}) features np.array(features, dtypenp.float32) np.save(os.path.join(output_dir, features.npy), features) with open(os.path.join(output_dir, paths.txt), w, encodingutf-8) as f: f.write(\n.join(image_paths)) print(f索引完成{len(image_paths)} 張圖片特征維度 {features.shape[1]}) if __name__ __main__: build_index(gallery_images, index_output)這段代碼做了幾件事遍歷目錄、過濾非圖片文件、逐張?zhí)崛√卣鳌⒉东@異常防止單張損壞圖片中斷整個流程、保存特征矩陣和路徑列表。tqdm用來顯示進度條圖庫大時很有用。保存為.npy格式比 CSV 或 JSON 快得多加載時直接np.load即可。參數(shù)上image_dir是你的圖庫根目錄output_dir是索引輸出目錄。如果圖庫有子目錄結(jié)構(gòu)需要改成os.walk遞歸遍歷。特征矩陣的形狀是(N, 512)N 是圖片數(shù)量。路徑列表的順序必須和特征矩陣的行順序嚴(yán)格對應(yīng)否則檢索結(jié)果會張冠李戴。我一般會在保存前打印前 5 個路徑和對應(yīng)的特征范數(shù)確認(rèn)沒有全零向量或異常值。3. 相似度計算與檢索排序余弦距離、歐氏距離怎么選3.1 余弦相似度與歐氏距離在歸一化特征下的等價性特征向量做完 L2 歸一化之后余弦相似度和歐氏距離之間存在單調(diào)關(guān)系。具體來說兩個單位向量之間的歐氏距離平方等于 2 減去 2 倍的余弦相似度。這意味著用余弦相似度排序和用歐氏距離排序得到的 Top-K 結(jié)果完全一致。既然等價為什么還要區(qū)分因為計算效率不同。余弦相似度只需要做點積而歐氏距離需要計算差值平方和再開方。在圖庫規(guī)模達到十萬級時點積的矩陣運算優(yōu)勢會體現(xiàn)出來。實際寫代碼時我通常把圖庫特征矩陣轉(zhuǎn)置后與查詢向量做矩陣乘法一次性算出所有相似度而不是寫循環(huán)逐個計算。NumPy 的dot底層調(diào)用 BLAS 庫速度比 Python 循環(huán)快兩個數(shù)量級。如果你用 FAISS 或 Annoy 這類近似最近鄰庫它們默認(rèn)也是用內(nèi)積或歐氏距離歸一化后兩者可以互換。3.2 用 NumPy 實現(xiàn) Top-K 檢索并返回圖片路徑下面代碼加載之前保存的特征索引對一張查詢圖做檢索返回最相似的 K 張圖片路徑和相似度分?jǐn)?shù)。import numpy as np from extract_feature import extract_feature def search(query_path, index_dir, top_k10): # 加載圖庫特征和路徑 features np.load(f{index_dir}/features.npy) # (N, 512) with open(f{index_dir}/paths.txt, r, encodingutf-8) as f: paths f.read().strip().split(\n) # 提取查詢圖特征已經(jīng)是 L2 歸一化的 query_vec extract_feature(query_path) # (512,) # 矩陣乘法計算余弦相似度features (N,512) dot query (512,) - (N,) similarities features.dot(query_vec) # 取 Top-K 索引argsort 默認(rèn)升序取最后 K 個再反轉(zhuǎn) top_indices np.argsort(similarities)[-top_k:][::-1] results [] for idx in top_indices: results.append({ path: paths[idx], score: float(similarities[idx]) }) return results if __name__ __main__: res search(query.jpg, index_output, top_k5) for r in res: print(f{r[score]:.4f} {r[path]})這段代碼的核心是features.dot(query_vec)這一行。features形狀是(N, 512)query_vec形狀是(512,)點積結(jié)果是(N,)的相似度數(shù)組。np.argsort返回升序排列的索引取最后 K 個再反轉(zhuǎn)就得到降序的 Top-K。相似度分?jǐn)?shù)越接近 1 表示越相似因為向量已經(jīng)歸一化點積就是余弦值。參數(shù)top_k根據(jù)你的演示需求調(diào)整畢業(yè)設(shè)計答辯通常展示 5 到 10 張就夠了。如果圖庫里有重復(fù)圖片或近似重復(fù)圖片可能會占據(jù)多個 Top-K 位置可以考慮做非極大值抑制但一般畢業(yè)設(shè)計不需要這么復(fù)雜。注意paths列表的長度必須和features的行數(shù)一致如果之前構(gòu)建索引時跳過了損壞圖片這里不會出問題因為保存時就是對應(yīng)的。3.3 檢索結(jié)果的可視化與評價指標(biāo)計算檢索系統(tǒng)不能只輸出路徑和分?jǐn)?shù)答辯時需要直觀展示。用 Matplotlib 把查詢圖和 Top-K 結(jié)果拼成一張圖是成本最低的可視化方案。import matplotlib.pyplot as plt from PIL import Image def visualize(query_path, results, save_pathresult.png): k len(results) fig, axes plt.subplots(1, k 1, figsize(3 * (k 1), 4)) # 第一張顯示查詢圖 axes[0].imshow(Image.open(query_path)) axes[0].set_title(Query) axes[0].axis(off) # 后續(xù)顯示檢索結(jié)果 for i, r in enumerate(results): axes[i 1].imshow(Image.open(r[path])) axes[i 1].set_title(f{r[score]:.3f}) axes[i 1].axis(off) plt.tight_layout() plt.savefig(save_path, dpi150) plt.show()評價指標(biāo)方面畢業(yè)設(shè)計至少應(yīng)該報告 Top-1 準(zhǔn)確率和 Top-5 準(zhǔn)確率。做法是準(zhǔn)備一個帶標(biāo)注的測試集每張查詢圖有已知的同類圖片。如果 Top-K 結(jié)果中包含同類圖片就算命中。計算代碼很簡單但標(biāo)注數(shù)據(jù)需要你自己整理。我建議從圖庫中每類隨機抽 5 張作為查詢圖剩下的作為圖庫這樣既保證有同類圖片可檢索又不會讓查詢圖同時出現(xiàn)在圖庫中導(dǎo)致“自己搜自己”的虛高分?jǐn)?shù)。4. 避坑與排查VGG16 圖像檢索系統(tǒng)最常見的 5 個翻車點4.1 現(xiàn)象檢索結(jié)果全是同一張圖或相似度全部接近 1.0原因通常是特征沒有做 L2 歸一化或者歸一化維度搞錯了。如果特征向量沒有歸一化點積結(jié)果會受向量模長影響模長大的向量與所有查詢向量的點積都偏大導(dǎo)致它排到前面。另一種可能是構(gòu)建索引時把查詢圖也放進了圖庫查詢圖與自身的相似度必然是 1.0如果圖庫里有重復(fù)圖片也會出現(xiàn)類似情況。解決方法是檢查extract_feature函數(shù)里是否調(diào)用了nn.functional.normalize并確認(rèn)dim1而不是dim0。構(gòu)建索引前先對圖庫去重可以用文件哈希或感知哈希做快速去重。如果已經(jīng)構(gòu)建了索引加載后打印特征矩陣每行的 L2 范數(shù)正常應(yīng)該全部接近 1.0如果偏差超過 0.01 就說明歸一化有問題。4.2 現(xiàn)象GPU 顯存溢出報錯 CUDA out of memoryVGG16 參數(shù)量大如果批量提取特征時一次性把整個圖庫讀進 GPU顯存很容易爆。尤其是圖庫超過 5000 張、每張圖片預(yù)處理后是(1, 3, 224, 224)時累積的中間激活值會占用大量顯存。解決方法是分批處理每批 16 或 32 張?zhí)幚硗暌慌桶烟卣鬓D(zhuǎn)到 CPU 并釋放 GPU 緩存。代碼上可以用torch.cuda.empty_cache()手動清理但更根本的是控制 batch size。如果顯卡顯存小于 6GB建議 batch size 設(shè)為 8 或 16。另外提取特征時務(wù)必用torch.no_grad()上下文否則 PyTorch 會保留計算圖顯存占用會翻好幾倍。4.3 現(xiàn)象檢索速度極慢單次查詢超過 5 秒如果圖庫有 10 萬張圖片特征矩陣是(100000, 512)用 NumPy 做點積大約需要幾十毫秒不應(yīng)該超過 1 秒。如果慢到幾秒通常是兩個原因一是每次查詢都重新加載.npy文件磁盤 I/O 成了瓶頸二是用了 Python 循環(huán)逐張計算相似度沒有用矩陣運算。解決方法是把特征矩陣和路徑列表在系統(tǒng)啟動時加載到內(nèi)存查詢時直接復(fù)用。如果內(nèi)存放不下考慮用 FAISS 建立索引它支持近似最近鄰搜索在億級向量上也能做到毫秒級響應(yīng)。對于畢業(yè)設(shè)計的圖庫規(guī)模NumPy 矩陣運算完全夠用關(guān)鍵是把加載和計算分開。4.4 現(xiàn)象不同圖片提取的特征幾乎一樣區(qū)分度低這通常是因為預(yù)處理出了問題。比如把圖片直接縮放到 224×224 而沒有保持長寬比導(dǎo)致圖像內(nèi)容變形或者歸一化時用了錯誤的均值和標(biāo)準(zhǔn)差讓輸入分布偏離預(yù)訓(xùn)練時的分布。還有一種可能是取錯了特征層如果取了block1_pool之后的淺層特征語義信息太弱不同類別的圖片特征差異不明顯。解決方法是嚴(yán)格按 ImageNet 標(biāo)準(zhǔn)做預(yù)處理先 Resize 到 256再 CenterCrop 到 224歸一化參數(shù)用mean[0.485, 0.456, 0.406]和std[0.229, 0.224, 0.225]。特征層選擇上block5_pool之后的 512 維特征在語義性和計算量之間平衡得最好。如果還是區(qū)分度低可以嘗試取fc2層的 4096 維特征或者對特征做 PCA 降維后再檢索。4.5 現(xiàn)象換一臺電腦或重新安裝環(huán)境后代碼跑不通畢業(yè)設(shè)計代碼需要在不同機器上演示環(huán)境依賴是常見翻車點。PyTorch 版本、torchvision 版本、NumPy 版本不匹配都可能導(dǎo)致 API 變化或權(quán)重加載失敗。比如models.vgg16(pretrainedTrue)在舊版本可用新版本推薦用weightsmodels.VGG16_Weights.IMAGENET1K_V1。解決方法是在項目根目錄放一個requirements.txt固定主要依賴的版本號。我一般會寫torch1.12、torchvision0.13、numpy1.21、Pillow9.0、matplotlib3.5、tqdm4.60。如果答辯現(xiàn)場沒有網(wǎng)絡(luò)提前把預(yù)訓(xùn)練權(quán)重文件下載到本地加載時用weights參數(shù)指定本地路徑避免現(xiàn)場下載超時。另外路徑分隔符在 Windows 和 Linux 上不同代碼里統(tǒng)一用os.path.join而不是硬編碼斜杠。5. 進階技巧用 PCA 降維和查詢擴展把檢索精度再提一檔5.1 用 PCA 把 512 維特征壓到 128 維檢索速度翻倍512 維特征在幾萬張圖庫上做檢索已經(jīng)夠快但如果你想把系統(tǒng)部署到內(nèi)存受限的環(huán)境或者想進一步加速PCA 降維是性價比很高的選擇。PCA 的原理是找到特征方差最大的方向把原始特征投影到低維空間同時保留大部分信息量。對于 VGG16 的 512 維特征通常降到 128 維就能保留 95% 以上的方差檢索精度損失很小。實現(xiàn)上用 scikit-learn 的PCA類對圖庫特征矩陣做擬合然后把查詢特征也投影到同樣的主成分空間。注意 PCA 的均值中心化步驟擬合時用圖庫特征計算均值和主成分查詢時用同樣的均值做中心化不能重新計算。下面代碼展示了完整流程。from sklearn.decomposition import PCA import numpy as np # 加載圖庫特征 features np.load(index_output/features.npy) # (N, 512) # 擬合 PCA保留 128 維 pca PCA(n_components128, whitenTrue) features_pca pca.fit_transform(features) # (N, 128) # 保存 PCA 模型和降維后的特征 import joblib joblib.dump(pca, index_output/pca_model.pkl) np.save(index_output/features_pca.npy, features_pca) # 查詢時先提取 512 維特征再用同樣的 PCA 投影 query_vec extract_feature(query.jpg) # (512,) query_pca pca.transform(query_vec.reshape(1, -1)) # (1, 128) query_pca query_pca / np.linalg.norm(query_pca, axis1, keepdimsTrue) # 重新歸一化 # 檢索 similarities features_pca.dot(query_pca.squeeze()) top_indices np.argsort(similarities)[-10:][::-1]whitenTrue會讓降維后的特征每個維度方差為 1有助于提升余弦相似度的區(qū)分度。降維后需要重新做 L2 歸一化因為 PCA 變換會改變向量模長。PCA 模型必須保存下來查詢時用同一個模型投影否則查詢特征和圖庫特征不在同一空間檢索結(jié)果會完全錯誤。5.2 查詢擴展用 Top-K 結(jié)果的均值特征做二次檢索查詢擴展是一種經(jīng)典的檢索優(yōu)化技巧。它的思路是第一次檢索得到 Top-K 結(jié)果后把這些結(jié)果的特征向量取平均得到一個“擴展查詢向量”再用這個新向量做第二次檢索。這樣做的好處是如果第一次檢索中有幾張同類圖片它們的平均特征會更接近該類別的中心從而把更多同類圖片拉進 Top-K。實現(xiàn)上第一次檢索用原始查詢特征取 Top-5 結(jié)果的特征計算加權(quán)平均相似度越高的權(quán)重越大然后歸一化得到擴展查詢向量。第二次檢索用擴展向量替換原始查詢向量。代碼改動很小但效果在部分?jǐn)?shù)據(jù)集上能提升 3 到 5 個百分點的 Top-5 準(zhǔn)確率。def query_expansion(query_vec, features, top_k5, alpha0.5): # 第一次檢索 sims features.dot(query_vec) top_idx np.argsort(sims)[-top_k:][::-1] # 加權(quán)平均相似度作為權(quán)重 weights sims[top_idx] weights weights / weights.sum() expanded np.average(features[top_idx], axis0, weightsweights) # 與原始查詢向量混合 expanded alpha * expanded (1 - alpha) * query_vec expanded expanded / np.linalg.norm(expanded) return expandedalpha控制擴展向量和原始向量的混合比例通常設(shè)在 0.3 到 0.7 之間。如果圖庫噪聲大alpha 取小一點避免被錯誤結(jié)果帶偏。這個方法在畢業(yè)設(shè)計答辯時是一個很好的加分項因為它體現(xiàn)了你對檢索算法的理解不止于“提取特征算距離”。5.3 一個我反復(fù)驗證過的習(xí)慣先跑通 100 張圖的小閉環(huán)做了這么多版圖像檢索系統(tǒng)我最大的血淚經(jīng)驗是不要一上來就把整個圖庫跑完。先挑 100 張圖片構(gòu)建索引用 5 張查詢圖測試確認(rèn) Top-5 結(jié)果肉眼看著合理再逐步擴大圖庫規(guī)模。這個習(xí)慣幫我省下了大量等待批量提取的時間也讓我在早期就發(fā)現(xiàn)了預(yù)處理錯誤、歸一化遺漏、路徑對應(yīng)錯位等問題。另一個習(xí)慣是每次修改特征提取邏輯后重新計算圖庫特征并覆蓋舊索引絕不混用不同版本的特征文件。特征文件和路徑列表必須成對出現(xiàn)版本一致這是系統(tǒng)可復(fù)現(xiàn)的底線。希望幫到你。本文還有配套的精品資源點擊獲取