dlib人臉關鍵點檢測的模型分析與壓縮


本文系原創,轉載請注明出處~

小喵的博客:https://www.miaoerduo.com

博客原文(排版更精美):https://www.miaoerduo.com/c/dlib人臉關鍵點檢測的模型分析與壓縮.html

github項目:https://github.com/miaoerduo/dlib-face-landmark-compression

 

人臉關鍵點檢測的技術在很多領域上都有應用,首先是人臉識別,常見的人臉算法其實都會有一步,就是把人臉的圖像進行對齊,而這個對齊就是通過關鍵點實現的,因此關於人臉關鍵點檢測的論文也常叫face alignment,也就是人臉對齊。另一方面,對於美顏,2D/3D建模等等也需要一來人臉的關鍵點技術,而且通常也要求有盡可能多的人臉關鍵點。

Dlib is a modern C++ toolkit containing machine learning algorithms and tools for creating complex software in C++ to solve real world problems. It is used in both industry and academia in a wide range of domains including robotics, embedded devices, mobile phones, and large high performance computing environments. Dlib's open source licensing allows you to use it in any application, free of charge.

Dlib是一個包含了大量的機器學習和復雜軟件開發工具的現代C++工具箱,被廣泛的用於軟件開發等領域。

本篇博客主要研究的就是Dlib中的人臉關鍵點檢測的工具。該工具的方法依據是 One Millisecond Face Alignment with an Ensemble of Regression Trees by Vahid Kazemi and Josephine Sullivan, CVPR 2014 這篇論文,在速度和精度上均達到了極好的效果。

本文的側重點在於人臉關鍵點模型的存儲結構的分析和模型的壓縮策略分析,最終在性能幾乎不變的情況下,得到模型的至少10倍的壓縮比。項目最終的github地址為:https://github.com/miaoerduo/dlib-face-landmark-compression 歡迎fork、star和pr。

注意:

  1. 本文假定了讀者對該論文有一定的了解,可以使用Dlib完成人臉關鍵點的訓練和部署,因此不做論文的相關方法的解釋。、
  2. 本文中分析的數據都是Dlib的shape_predictor類的私有成員,這里不得不把他們的修飾符從private改成了public,但文中並沒有專門指出。
  3. 本文中所有的代碼均在本地的64位操作系統上運行,在變量數據存儲的大小描述的時候也均以64位來說明,即使是不同的編譯器也會對數據大小造成影響,但這不是本文的重點。
  4. 本文中的數據類型如果不在C++中見到的數據類型,則為下面的typedef的數據類型
typedef char        int8;
typedef short       int16;
typedef int         int32;
typedef long long   int64;
typedef float       float32;
typedef double      float64;
typedef unsigned char       uint8;
typedef unsigned short      uint16;
typedef unsigned int        uint32;
typedef unsigned long long  uint64;

Dlib中人臉關鍵點實現的類是dlib::shape_predictor,源碼為:https://github.com/davisking/dlib/blob/master/dlib/image_processing/shape_predictor.h

這里簡單的抽取了數據相關的接口定義:

namespace dlib
{
// ----------------------------------------------------------------------------------------
    namespace impl
    {
        struct split_feature
        {
            unsigned long idx1;
            unsigned long idx2;
            float thresh;
        };

        struct regression_tree
        {
            std::vector<split_feature> splits;
            std::vector<matrix<float,0,1> > leaf_values;
        };
    } // end namespace impl

// ----------------------------------------------------------------------------------------
    class shape_predictor
    {
    private:
        matrix<float,0,1> initial_shape;
        std::vector<std::vector<impl::regression_tree> > forests;
        std::vector<std::vector<unsigned long> > anchor_idx; 
        std::vector<std::vector<dlib::vector<float,2> > > deltas;
    };
}

 

下面,我們逐一對每個部分的參數進行分析。

Dlib內置了很多的數據類型,像vector、metrix等等,每種數據類型又可以單獨序列化成二進制的數據。對於shape_predictor的序列化,本質上就是不斷的調用成員變量數據的序列化方法,由此極大地簡化代碼,提高了代碼的復用率。

inline void serialize (const shape_predictor& item, std::ostream& out)
    {
        int version = 1;
        dlib::serialize(version, out);
        dlib::serialize(item.initial_shape, out);
        dlib::serialize(item.forests, out);
        dlib::serialize(item.anchor_idx, out);
        dlib::serialize(item.deltas, out);
    }

但,對於移動端等應用場景,需要模型占用盡可能少的存儲空間,這樣一來,這些標准的存儲方式就會造成數據的很大程度的冗余。我們的任務就是一點點的減少這些冗余,只存有用的數據。

一、常量部分

首先,我們需要知道一些常量的數據。這些數據完成了對模型的描述。

變量名 數據類型 作用
version uint64 記錄模型版本號
cascade_depth uint64 回歸樹的級數
num_trees_per_cascade_level uint64 每一級中的樹的個數
tree_depth uint64 樹的深度
feature_pool_size uint64 特征池的大小
landmark_num uint64 特征點的數目
quantization_num uint64 量化的級數
prune_thresh float32 剪枝的閾值

 

二、初始形狀 initial_shape

matrix<float,0,1> initial_shape; 表示的是初始化人臉關鍵點的坐標,存儲類型是float型,個數為 landmark_num * 2 (不要忘了一個點是兩個數組成 :P)。

三、錨點 anchor_idx

std::vector<std::vector<unsigned long> > anchor_idx; 是一個二維的數組,存放的是landmark點的下標。在常見的68點和192點的任務中,使用一個uint8就可以存放下標,而這里使用的是unsigned long,顯然過於冗余,這里可以簡化成uint8存儲。這個二維數組的大小為 cascade_depth * feature_pool_size 。每一級回歸樹使用一套錨點。

四、deltas

std::vector<std::vector<dlib::vector<float,2> > > deltas;和anchor_idx類似,是一個二維數組,不同的是,數組的每個值都是dlib::vector<float,2>的結構。這個數組的大小為 cascade_depth * feature_pool_size * 2 ,存放的內容是float數值。考慮到這里的參數量很少,沒有壓縮的必要,這里我們直接存儲原數據。

五、森林 forests

這部分是模型參數量最大的部分,一個模型大概2/3的存儲都耗在了這個地方。這里才是我們壓縮的重點!

std::vector<std::vector<impl::regression_tree> > forests;一個shape_predictor中,有cascade_depth級,每一級有num_trees_per_cascade_level棵樹。對於每棵樹,它主要存放了兩個部分的數據:分割的閾值splits和葉子的值leaf_values。為了便於閱讀,再把數據結構的定義附上。

namespace dlib
{
    namespace impl
    {
        struct split_feature
        {
            unsigned long idx1;
            unsigned long idx2;
            float thresh;
        };

        struct regression_tree
        {
            std::vector<split_feature> splits;
            std::vector<matrix<float,0,1> > leaf_values;
        };
    } // end namespace impl
}

5.1 splits

splits存放的數據是閾值和特征像素值的下標,這個下標的范圍是[0, feature_pool_size),在通常情況下,feature_pool_size不會太大,論文中最大也就設到了2000。這里我們可以使用一個uint16來存儲。thresh就直接存儲。對於一棵樹,樹的深度為tree_depth,則有 2^tree_depth - 1 個split_node。(這里認為只有根節點的樹深度為0)。

5.2 leaf_values

std::vector<matrix<float,0,1> > leaf_values;對於深度為tree_depth的樹,有 2^tree_depth 個葉子節點。對於每個葉子節點,需要存儲整個關鍵點的偏移量,也就是說每個節點存放了 landmark_num * 2 個float的數值。那么這部分的參數量到底有多大呢?

舉個例子,在cascade_num為10,num_trees_per_cascade_level為500,tree_depth為5,landmark_num為68的時候。leaf_values的值有cascade_num * num_trees_per_cascade_level * (2 ^ tree_depth) * landmark_num * 2 = 21760000 = 20.8M 的參數量,由於使用float存儲,通常一個float是4個字節,因此總的存儲量達到了逆天的80MB!遠大於其他的參數的總和。

那么如何才能有效的降低這部分的存儲量呢?

這就要要用到傳說中的模型壓縮三件套:剪枝,量化編碼

5.2.1 參數分布分析

首先筆者統計了參數的分布,大致的情況是這樣的,(具體的結果找不到了)。

葉子節點里的參數的范圍在[-0.11, 0.11]之間,其中[-0.0001, 0.0001]的參數占了50%以上。說明模型中有大量的十分接近0的數字。

5.2.2 剪枝

剪枝的策略十分粗暴,選擇一個剪枝的閾值prune_thresh,將模小於閾值的數全部置0。

5.2.3 量化

量化的過程,首先獲取數據中的最小值和最大值,記為:leaf_min_value 和 leaf_max_value。之后根據量化的級數 quantization_num,計算出每一級的步長:quantization_precision = (leaf_max_value - leaf_min_value) / quantization_num。之后對於任意數值x,那么它最終為 x / quantization_precision 進行四舍五入的結果。這樣就可以把float的數字轉換成整形來表示。量化級數越高,則量化之后的值損失就越小。

5.3.3 編碼

如果我們不做任何的編碼操作,直接存儲量化之后的結果,也是可以一定程度上進行模型的壓縮的。比如使用256級量化,則量化的結果使用一個uint8就可以存儲,從而把存儲量降為原來的1/4。但是這樣有兩個問題:1,依賴量化的級數;2,存儲量減少不大。

在信息論中有個信息熵的概念。為了驗證存儲上的可以再優化,這里選擇了一個68點的模型,經過256級量化之后,計算出信息熵(信息熵的計算請查閱其他的資料),其數值為1.53313,也就是說,理想情況下,一個數值只需要不到2 bits就可以存儲了。如果不編碼則需要8 bits。壓縮比為 1.53313 / 8 = 19.2 %,前者僅為后者的1/5不到!

這里,我采用的是經典的huffman編碼,使用了github上的 https://github.com/ningke/huffman-codes 項目中的代碼,感謝作者的貢獻!

原項目中只能對char類型的數據進行編碼,因此這里也做了相應的修改,以適應於int類型的編碼,同時刪除了一些用不到的函數。

使用huffman對上述的256級的數值進行編碼,最終的每個數字的平均長度為1.75313,已經很接近理想情況。

使用huffman編碼時,同時需要將碼表進行儲存,這部分細節較為繁瑣,讀者可以自行閱讀源碼。

 

至此,Dlib的模型的分析和壓縮就全部介紹完了。對代碼感興趣的同學可以在:https://github.com/miaoerduo/dlib-face-landmark-compression ,也就是我的github上clone到最新的代碼,代碼我目前也在不斷的測試,如果有問題,也會及時更新的。

在本地的實驗中,原模型的大小為127M,壓縮之后只有5.9M,且性能幾乎不變(這里prune_thresh設為0.0001, quantization_num設為256,quantization_num設置越大,則精度越接近原模型,同時prune_thresh的大小很多時候是沒有用的)。

 

馬上就要畢業了,希望寫博客的習慣能夠一直保持下去。

最后,再一次,希望小喵能和大家一起學習和進步~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM