【火爐煉AI】機器學習051-視覺詞袋模型+極端隨機森林建立影象分類器

weixin_34166847發表於2018-10-23

【火爐煉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:\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著,陶俊傑,陳小莉譯

相關文章