【火爐煉AI】機器學習051-視覺詞袋模型+極端隨機森林建立影像分類器
(本文所使用的Python庫和版本號: Python 3.6, Numpy 1.14, scikit-learn 0.19, matplotlib 2.2 )
視覺詞袋模型(Bag Of Visual Words,BOVW)來源於自然語言處理中的詞袋模型(Bag Of Words, BOW),關於詞袋模型,可以參考我的博文【火爐煉AI】機器學習038-NLP建立詞袋模型.在NLP中,BOW的核心思想是將一個文件當做一個袋子,裡面裝著各種各樣的單詞,根據單詞出現的頻次或權重來衡量某個單詞的重要性。BOW的一個重要特性是不考慮單詞出現的順序,句子語法等因素。
視覺詞袋模型BOVW是將BOW的核心思想應用於影像處理領域的一種方法,為了表示一幅影像,我們可以將影像看做文件,即若干個“視覺單詞”的集合,和BOW一樣,不考慮這些視覺單詞出現的順序,故而BOVW的一個缺點是忽視了畫素之間的空間位置資訊(當然,針對這個缺點有很多改進版本)。BOVW的核心思想可以從下圖中看出一二。
有人要問了,提取影像的特徵方法有很多,比如SIFT特徵提取器,Star特徵提取器等,為什麼還要使用BOVW模型來表徵影像了?因為SIFT,Star這些特徵提取器得到的特徵向量是多維的,比如SIFT向量是128維,而且一幅影像通常會包含成百上千個SIFT向量,在進行下游機器學習計算時,這個計算量非常大,效率很低,故而通常的做法是用聚類演算法對這些特徵向量進行聚類,然後用聚類中的一個簇代表BOVW中的一個視覺單詞,將同一幅影像的SIFT向量對映到視覺視覺單詞序列,生成視覺碼本,這樣,每一幅影像都可以用一個視覺碼本向量來描述,在後續的計算中,效率大大提高,有助於大規模的影像檢索。
關於BOVW的更詳細描述,可以參考博文:視覺詞袋模型BOW學習筆記及matlab程式設計實現
1. 使用BOVW建立影像資料集
BOVW主要包括三個關鍵步驟:
1,提取影像特徵:提取演算法可以使用SIFT,Star,HOG等方法,比如使用SIFT特徵提取器,對資料集中的每一幅影像都使用SIFT後,每一個SIFT特徵用一個128維描述特徵向量表示,假如有M幅影像,一共提取出N個SIFT特徵向量。
2,聚類得到視覺單詞:最常用的是K-means,當然可以用其他聚類演算法,使用聚類對N個SIFT特徵向量進行聚類,K-means會將N個特徵向量分成K個簇,使得每個簇內部的特徵向量都具有非常高的相似度,而簇間的相似度較低,聚類後會得到K個聚類中心(在BOVW中,聚類中心被稱為視覺單詞)。計算每一幅影像的每一個SIFT特徵到這K個視覺單詞的距離,並將其對映到距離最近的一個簇中(即該視覺單詞的對應詞頻+1)。這樣,每一幅影像都變成了一個與視覺單詞相對應的詞頻向量。
3,構建視覺碼本:因為每一幅影像的SIFT特徵個數不相等,所以需要對這些詞頻向量進行歸一化,將每幅影像的SIFT特徵個數變為頻數,這樣就得到視覺碼本。
整個流程可以簡單地用下圖描述:
下面開始準備資料集,首先從Caltech256影像抽取3類,每一類隨機抽取20張圖片,組成一個小型資料集,每一個類別放在一個資料夾中,且資料夾的命名以數字和“-”開頭,數字就表示類別名稱。這個小資料集純粹是驗證演算法是否能跑通。如下為準備的資料集:
首先來看第一步的程式碼:提取影像特徵的程式碼:
def __img_sift_features(self,image):
```
提取圖片image中的Star特徵的關鍵點,然後用SIFT特徵提取器進行計算,
得到N行128列的矩陣,每幅圖中提取的Star特徵個數不一樣,故而N不一樣,
但是經過SIFT計算之後,特徵的維度都變成128維。
返回該N行128列的矩陣
```
keypoints=xfeatures2d.StarDetector_create().detect(image)
gray=cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_,feature_vectors=xfeatures2d.SIFT_create().compute(gray,keypoints)
return feature_vectors
複製程式碼
然後將得到的所有圖片的N行128列特徵集合起來,組成M行128列特徵,構建一個聚類演算法,用這個演算法來對映得到含有32個聚類中心(視覺單詞)的模型,將這128列特徵對映到32個視覺單詞中(由於此處Kmeans我使用32個簇,故而得到32個視覺單詞,越複雜的專案,這個值要調整的越大,從幾百到幾千不等。),在統計每一個特徵出現的頻次,組成一個詞袋模型,如下程式碼:
def __map_feature_to_cluster(self,img_path):
```從單張圖片中提取Star特徵矩陣(N行128列),
再將該特徵矩陣通過K-means聚類演算法對映到K個類別中,每一行特徵對映到一個簇中,得到N個簇標號的向量,
統計每一個簇中出現的特徵向量的個數,相當於統計詞袋中某個單詞出現的頻次。
```
img_feature_vectors=self.__img_sift_features(self.__get_image(img_path)) # N 行128列
cluster_labels=self.cluster_model.predict(img_feature_vectors)
# 計算這些特徵在K個簇中的類別,得到N個數字,每個數字是0-31中的某一個,代表該Star特徵屬於哪一個簇
# eg [30 30 30 6 30 30 23 25 23 23 30 30 16 17 31 30 30 30 4 25]
# 統計每個簇中特徵的個數
vector_nums=np.zeros(self.clusters_num) # 32個元素
for num in cluster_labels:
vector_nums[num]+=1
# 將特徵個數歸一化處理:得到百分比而非個數
sum_=sum(vector_nums)
return [vector_nums/sum_] if sum_>0 else [vector_nums] # 一行32列,32 個元素組成的list
複製程式碼
上面僅僅是用一部分圖片來得到聚類中心,沒有用全部的影像,因為部分影像完全可以代表全部影像。
第三步:獲取多張圖片的視覺碼本,將這些視覺碼本組成一個P行32列的矩陣。
def __calc_imgs_clusters(self,img_path_list):
```獲取多張圖片的視覺碼本,將這些視覺碼本組成一個P行32列的矩陣,P是圖片張數,32是聚類的類別數。
返回該P行32列的矩陣```
img_paths=list(itertools.chain(*img_path_list)) # 將多層list展開
code_books=[]
[code_books.extend(self.__map_feature_to_cluster(img_path)) for img_path in img_paths]
return code_books
複製程式碼
完整的準備資料集的程式碼比較長,如下:
# 準備資料集
import cv2,itertools,pickle,os
from cv2 import xfeatures2d
from glob import glob
class DataSet:
def __init__(self,img_folder,cluster_model_path,img_ext=`jpg`,max_samples=12,clusters_num=32):
self.img_folder=img_folder
self.cluster_model_path=cluster_model_path
self.img_ext=img_ext
self.max_samples=max_samples
self.clusters_num=clusters_num
self.img_paths=self.__get_img_paths()
self.all_img_paths=[list(item.values())[0] for item in self.img_paths]
self.cluster_model=self.__load_cluster_model()
def __get_img_paths(self):
folders=glob(self.img_folder+`/*-*`) # 由於圖片資料夾的名稱是數字+‘-’開頭,故而可以用這個來獲取
img_paths=[]
for folder in folders:
class_label=folder.split(`\`)[-1]
img_paths.append({class_label:glob(folder+`/*.`+self.img_ext)})
# 每一個元素都是一個dict,key為資料夾名稱,value為該資料夾下所有圖片的路徑組成的list
return img_paths
def __get_image(self,img_path,new_size=200):
def resize_img(image,new_size):
```將image的長或寬中的最小值調整到new_size```
h,w=image.shape[:2]
ratio=new_size/min(h,w)
return cv2.resize(image,(int(w*ratio),int(h*ratio)))
image=cv2.imread(img_path)
return resize_img(image,new_size)
def __img_sift_features(self,image):
```
提取圖片image中的Star特徵的關鍵點,然後用SIFT特徵提取器進行計算,
得到N行128列的矩陣,每幅圖中提取的Star特徵個數不一樣,故而N不一樣,
但是經過SIFT計算之後,特徵的維度都變成128維。
返回該N行128列的矩陣
```
keypoints=xfeatures2d.StarDetector_create().detect(image)
gray=cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_,feature_vectors=xfeatures2d.SIFT_create().compute(gray,keypoints)
return feature_vectors
def __calc_imgs_features(self,img_path_list):
```獲取多張圖片的特徵向量,這些特徵向量是合併到一起的,最終組成M行128列的矩陣,返回該矩陣.
此處的M是每張圖片的特徵向量個數之和,即N1+N2+N3....```
img_paths=list(itertools.chain(*img_path_list)) # 將多層list展開
feature_vectors=[]
[feature_vectors.extend(self.__img_sift_features(self.__get_image(img_path))) for img_path in img_paths]
return feature_vectors
def __create_save_Cluster(self):
```由於folders中含有大量圖片,故而取一小部分(max_samples)圖片來做K-means聚類。
```
# 獲取要進行聚類的小部分圖片的路徑
cluster_img_paths=[list(item.values())[0][:self.max_samples] for item in self.img_paths]
feature_vectors=self.__calc_imgs_features(cluster_img_paths)
cluster_model = KMeans(self.clusters_num, # 建立聚類模型
n_init=10,
max_iter=10, tol=1.0)
cluster_model.fit(feature_vectors) # 對聚類模型進行訓練
# 將聚類模型儲存,以後就不需要再訓練了。
with open(self.cluster_model_path,`wb+`) as file:
pickle.dump(cluster_model,file)
print(`cluster model is saved to {}.`.format(self.cluster_model_path))
return cluster_model
def __map_feature_to_cluster(self,img_path):
```從單張圖片中提取Star特徵矩陣(N行128列),
再將該特徵矩陣通過K-means聚類演算法對映到K個類別中,每一行特徵對映到一個簇中,得到N個簇標號的向量,
統計每一個簇中出現的特徵向量的個數,相當於統計詞袋中某個單詞出現的頻次。
```
img_feature_vectors=self.__img_sift_features(self.__get_image(img_path)) # N 行128列
cluster_labels=self.cluster_model.predict(img_feature_vectors)
# 計算這些特徵在K個簇中的類別,得到N個數字,每個數字是0-31中的某一個,代表該Star特徵屬於哪一個簇
# eg [30 30 30 6 30 30 23 25 23 23 30 30 16 17 31 30 30 30 4 25]
# 統計每個簇中特徵的個數
vector_nums=np.zeros(self.clusters_num) # 32個元素
for num in cluster_labels:
vector_nums[num]+=1
# 將特徵個數歸一化處理:得到百分比而非個數
sum_=sum(vector_nums)
return [vector_nums/sum_] if sum_>0 else [vector_nums] # 一行32列,32 個元素組成的list
def __calc_imgs_clusters(self,img_path_list):
```獲取多張圖片的視覺碼本,將這些視覺碼本組成一個P行32列的矩陣,P是圖片張數,32是聚類的類別數。
返回該P行32列的矩陣```
img_paths=list(itertools.chain(*img_path_list)) # 將多層list展開
code_books=[]
[code_books.extend(self.__map_feature_to_cluster(img_path)) for img_path in img_paths]
return code_books
def __load_cluster_model(self):
```從cluster_model_path中載入聚類模型,返回該模型,如果不存在或出錯,則呼叫函式準備聚類模型```
cluster_model=None
if os.path.exists(self.cluster_model_path):
try:
with open(self.cluster_model_path, `rb`) as f:
cluster_model = pickle.load(f)
except:
pass
if cluster_model is None:
print(`No valid model found, start to prepare model...`)
cluster_model=self.__create_save_Cluster()
return cluster_model
def get_img_code_book(self,img_path):
```獲取單張圖片的視覺碼本,即一行32列的list,每個元素都是對應特徵出現的頻率```
return self.__map_feature_to_cluster(img_path)
def get_imgs_code_books(self,img_path_list):
```獲取多張圖片的視覺碼本,即P行32列的list,每個元素都是對應特徵出現的頻率```
return self.__calc_imgs_clusters(img_path_list)
def get_all_img_code_books(self):
```獲取img_folder中所有圖片的視覺碼本```
return self.__calc_imgs_clusters(self.all_img_paths)
def get_img_labels(self):
```獲取img_folder中所有圖片對應的label,可以從資料夾名稱中獲取```
img_paths=list(itertools.chain(*self.all_img_paths))
return [img_path.rpartition(`-`)[0].rpartition(`\`)[2] for img_path in img_paths]
def prepare_dataset(self):
```獲取img_folder中所有圖片的視覺碼本和label,構成資料集```
features=self.get_all_img_code_books()
labels=self.get_img_labels()
return np.c_[features,labels]
複製程式碼
2. 使用極端隨機森林建立模型
極端隨機森林是隨機森林演算法的一個提升版本,可以參考我以前的文章【火爐煉AI】機器學習007-用隨機森林構建共享單車需求預測模型.使用方法和隨機森林幾乎一樣。
# 極端隨機森林分類器
from sklearn.ensemble import ExtraTreesClassifier
class CLF_Model:
def __init__(self,n_estimators=100,max_depth=16):
self.model=ExtraTreesClassifier(n_estimators=n_estimators,
max_depth=max_depth, random_state=12)
def fit(self,train_X,train_y):
self.model.fit(train_X,train_y)
def predict(self,newSample_X):
return self.model.predict(newSample_X)
複製程式碼
其實,這個分類器很簡單,沒必要寫成類的形式。
對該分類器進行訓練:
dataset_df=pd.read_csv(`./prepared_set.txt`,index_col=[0])
dataset_X,dataset_y=dataset_df.iloc[:,:-1].values,dataset_df.iloc[:,-1].values
model=CLF_Model()
model.fit(dataset_X,dataset_y)
複製程式碼
3. 使用訓練後模型預測新樣本
如下,我隨機測試三張圖片,均得到了比較好的結果。
# 用訓練好的model預測新圖片,看看它屬於哪一類
new_img1=`E:PyProjectsDataSetFireAI/test0.jpg`
img_code_book=dataset.get_img_code_book(new_img1)
predicted=model.predict(img_code_book)
print(predicted)
new_img2=`E:PyProjectsDataSetFireAI/test1.jpg`
img_code_book=dataset.get_img_code_book(new_img2)
predicted=model.predict(img_code_book)
print(predicted)
new_img3=`E:PyProjectsDataSetFireAI/test2.jpg`
img_code_book=dataset.get_img_code_book(new_img3)
predicted=model.predict(img_code_book)
print(predicted)
複製程式碼
————————————-輸———出——————————–
[0]
[1]
[2]
——————————————–完————————————-
########################小**********結###############################
1,這個專案的難點在於視覺詞袋模型的理解和資料集準備,所以我將其寫成了類的形式,這個類具有一定的通用性,可以用於其他專案資料集的製備。
2,從這個專案可以看出視覺詞袋模型相對於原始的Star特徵的優勢:如果使用原來的Star特徵,一張圖片會得到N行128列的特徵數,而使用了BOVW模型,我們將N行128列的特徵資料對映到1行32列的空間中,所以極大的降低了特徵數,使得模型簡化,訓練和預測效率提高。
3,一旦準備好了資料集,就可以用各種常規的機器學習分類器進行分類,也可以用各種方法評估該分類器的優劣,比如效能報告,準確率,召回率等,由於這部分我在前面的文章中已經講過多次,故而此處省略。
#################################################################
注:本部分程式碼已經全部上傳到(我的github)上,歡迎下載。
參考資料:
1, Python機器學習經典例項,Prateek Joshi著,陶俊傑,陳小莉譯