【火爐煉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),關於詞袋模型,可以參考我的博文 ofollow,noindex">【火爐煉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 306 30 30 23 25 23 23 30 30 16 17 31 30 30 304 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 306 30 30 23 25 23 23 30 30 16 17 31 30 30 304 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:\PyProjects\DataSet\FireAI/test0.jpg' img_code_book=dataset.get_img_code_book(new_img1) predicted=model.predict(img_code_book) print(predicted) new_img2='E:\PyProjects\DataSet\FireAI/test1.jpg' img_code_book=dataset.get_img_code_book(new_img2) predicted=model.predict(img_code_book) print(predicted) new_img3='E:\PyProjects\DataSet\FireAI/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著,陶俊傑,陳小莉譯