1. 程式人生 > >MINIST | 基於樸素貝葉斯分類器的0-9數字手寫體識別

MINIST | 基於樸素貝葉斯分類器的0-9數字手寫體識別

概要: 本實驗基於MINIST資料集,採用樸素貝葉斯分類器,實現了0-9數字手寫體的識別。本文將簡要介紹這一方法的原理、程式碼實現以及在程式設計過程中需要注意的若干問題,程式碼仍然是用MATLAB寫成的。 關鍵字: MATLAB; 影象處理; 數字手寫體識別; 樸素貝葉斯分類器  

1 背景說明

   我的上一篇部落格介紹了基於樸素貝葉斯分類器的語音性別識別,用的也是樸素貝葉斯分類器。我在那篇部落格中說“之前曾做過用樸素貝葉斯分類器進行數字手寫體識別”並且“之後也將整理到此部落格上來”——看來這一篇文章是跑不掉了,那還不如儘早寫完——好吧,我今天這是履行諾言來了。不過同樣地我也不打算介紹“樸素貝葉斯分類器”到底是什麼、可以用來幹什麼了,因為首先它的原理的確too simple,其次網上這方面的資料非常多,而且講解的水平比我不知道高到哪裡去了。大家可以去我的

上一篇部落格中的“背景說明”中找我推薦的資料連結。

2 關於資料集

2.1 什麼是MINIST

   這是我在部落格中首次提到MINIST,因此有必要向讀者作簡要介紹。而且我也打算做一個MINIST的專欄,把自己學過的各種各樣的方法——包括傳統的方法和學習的方法——都在這個資料集上用一遍,也相當於是學以致用了。

   MINIST是一個開源資料庫,它來自美國國家標準與技術研究所(National Institute of Standards and Technology, NIST)。 其中一共包含了60000條訓練集資料和10000條測試集資料。其中訓練集由來自 250 個不同人手寫的0-9數字構成,其中50%是高中學生,50% 來自人口普查局的工作人員;而測試集的手寫數字資料也擁有同樣的來源及比例。

2.2 資料集處理

   這個資料集顯然要比我上一篇部落格中提到的kaggle中的資料集更難處理一些。因為資料集的製作者為了方便資料的上傳和下載,首先用自己的編碼方式將這些影象以位元組儲存從而變成了4個特殊格式的檔案,從而壓縮整個資料集的大小。根據MINIST網站的描述,這四個檔案分別是:

   train-images-idx3-ubyte.gz: 包含60000個訓練集影象    train-labels-idx1-ubyte.gz: 包含60000個訓練集影象的標記(label)    t10k-images-idx3-ubyte.gz : 包含10000個測試集影象    t10k-labels-idx1-ubyte.gz : 包含60000個訓練集影象的標記(label)

   這些檔案的編碼方式在網頁上也有詳細的介紹,讀者只需要根據編碼方式反過來操作就可以解碼得到影象檔案了,至於儲存成.png還是.jpg還是其他的什麼格式當然任憑讀者自己決定了,在MATLAB中也就是一行程式碼的事兒。

   我上傳的程式碼包裡有一個名叫“FILE FORMATS FOR THE MNIST DATABASE.txt”的檔案,裡面就是編解碼規則,對上不了外網的讀者可能會比較有幫助。

   資料集解碼的程式碼MINIST網頁上沒有提供,不過也不是什麼難事。我根據自己的習慣把原始檔案解碼到了20個.mat資料包內,按照數字和類別命名:前10個是train0.mat-train9.mat,代表數字0-9的訓練集;後10個是test0.mat-test9.mat,代表數字0-9的測試集。

   最後一步就是將這些.mat資料包中的資料條目一個個轉換成影象。每個.mat都是一個 若干行x784 的二維矩陣,其行數代表資料條數,列數的784是28的平方,也就是一張數字手寫體影象的畫素個數(每張數字手寫體是 28x28 的灰度圖)。

   說道灰度圖,那就還有一個問題值得考慮:需不需要轉換為二值圖呢?請注意二值化本質上是量化階為2的量化,而灰度圖的取值範圍是0-255,那也就是量化階為256的量化。它們的效果誰好誰壞呢,口說無憑,到了後面我們會拿實驗結果表態的。

3 程式碼實現

3.1 檔案目錄

   現在來介紹一下程式碼的檔案目錄以及各個檔案之間的聯絡。本實驗用到的全部程式如圖1所示:

圖1 所需檔案列表

     看上去程式碼檔案很多,但是有一些程式碼是準備材料時所用到的,還有一些是輔助性程式碼,還有一些是為了對比實驗結果而另外寫的,所以實際上並沒有那麼誇張。

   詳細介紹如下:

   第一個資料夾matdata裡的內容有MINIST原始的4個位元組檔案、解碼所需的.m原始碼、1個說明性的.txt檔案以及解碼得到的20個.mat資料包。    第二個資料夾models裡面的內容是在多種引數下訓練得到的模型,一共有16個模型。這個資料夾裡的內容是可以刪除的,只要每次在訓練程式碼Cpic2mat.m或是Dpic2mat.m中改變引數和產生資料包的檔名就可以在這個資料夾下生成新的模型,注意模型的格式也是.mat資料包。    第三個資料夾othertestpic裡面的內容是若干張其他來源的0-9數字手寫體圖片,用以測試訓練產生的額模型的識別效果。    第四個資料夾test-images裡面的內容是10000張測試圖片。    第五個資料夾test-images-smaller是第四個資料夾的子集。    第六個資料夾train-images裡面的內容是60000張訓練圖片。    第七個資料夾train-images-smaller是第六個資料夾的子集。    檔案Cpic2mat.m和Dpic2mat.m是訓練程式碼,區別在於前者的訓練影象是0-255灰度圖,後者訓練的是0-1浮點灰度圖。    檔案CBayesTesting.m和DBayesTesting.m是驗證程式碼。    檔案CBTSinglePic.m和DBTSinglePic.m是針對單個目標影象的測試程式碼。    檔案im_pre_here.m是影象預處理程式碼,功能是將灰度圖/黑白圖反色。    檔案mat2pic.m是輔助程式碼,功能是將第一個資料夾的處理結果——資料包——轉換成影象。    最後2個.m檔案都是輔助性程式碼,功能分別是計算某個取值在某個正態分佈下的概率以及對整數矩陣進行資料統計。    最後的.xls檔案是實驗記錄表單,我會直接在後文截圖並附上說明的。

   需要說明的是:

   由於我已經完成了大部分預處理和準備工作,所以讀者只需要先執行Cpic2mat.m或Dpic2mat.m得到訓練模型(模型以.mat格式儲存在資料夾models中),再執行CBayesTesting.m或DBayesTesting.m即可得到每個數字手寫體的識別正確率。另外,讀者也可以繼續執行CBTSinglePic.m或DBayesTesting.m來對單個影象進行檢測,包括現場書寫的數字(當然別忘了做適當的預處理)。

3.2 核心程式碼

   核心程式碼其實很簡單,就以C字打頭的檔案為例進行說明。

   資料訓練步驟的核心程式碼如下:

% 對於0-9共10個數字
for A1=1:10
    % 每個數字有多少個樣本
    len = length(img_list{A1,1});
    % 確定樣本訓練(學習)樣本數目
    for A2=1:min(len,train_num)
        img_name = img_list{A1,1}(A2).name;
        im = imread([prefix,img_name]);
%         figure;imshow(im);
        % 例如:拆分成7*7=49個小塊(Piece),每個小塊有4*4=16個元素
        Piece = 1;
        for A3=1:lPiece:29-lPiece
            for A4=1:lPiece:29-lPiece
                temp = im(A3:A3+lPiece-1,A4:A4+lPiece-1);
                % 例如:只要16箇中有超過3個是白色,就標記為1(255*4=1020)
                if sum(sum(temp))>= 255*nthres
                    Pattern(A1).feature(Piece,A2) = 1;
                end
                Piece = Piece+1;
            end
        end
    end
    fprintf('Calss %1d:%5d images are written to mat.\n',A1-1,train_num);
end
% ---------至此,結構體陣列Pattern寫入完畢,即資料集模板已建立-------------

   資料驗證步驟的核心程式碼如下:

% 對於每一類數字(0-9)
correct_num_all = 0;
for A1=1:10
    % 每個數字的每一個測試樣本數
    len = length(img_list{A1});
    % 對0-9每一個數字設定一個正確判斷量
    correct_num = 0;
    % 對於一個數字的每一個測試樣本
    for A2=1:len
        img_name=img_list{A1}(A2).name;
        im=imbinarize(im2double(imread([prefix,img_name])));
%         figure;imshow(im);
        % 對每一個im分割處理
        fe = zeros(nPiece^2,1);
        piece=1;
        for A3=1:lPiece:29-lPiece
            for A4=1:lPiece:29-lPiece
                temp=im(A3:A3+lPiece-1,A4:A4+lPiece-1);
                fe(piece)=sum(sum(temp));
                piece = piece+1;
            end
        end
        
        % 至此,得到了一個測試樣本的特徵序列fe,接下來識別該樣本
        % 用 Bayes 分類器的方法
        % 計算10個條件概率,即P(X|wi),i=0,1,...,9
        % 而 P(X|wi)=∏(k=1,49)P(xk|wi)
        % 可以理解為下面的 P(A2|A1)
       
        % 條件概率
        cond_prob = 2*ones(10,1);
        % 後驗概率
        post_prob = zeros(10,1);
        % 對於10個數字
        for A5=1:10
            % 對於待確定數字的每一位 1:49
            for A6=1:nPiece^2
                cond_prob(A5)=cond_prob(A5)*CPattern(A5).feature_prob(A6,fe(A6)+1)*sqrt(2);
            end
            % 後驗概率
            post_prob(A5) = cond_prob(A5)*CPattern(A5).prob;
        end
        % 取概率最大的
        [~,I]=max(post_prob);
        % 數字 I-1 就是識別結果,需要和真實數字 A1-1 進行對比
        if I==A1
            correct_num = correct_num+1;
        end
    end
    correct_rate = correct_num/len;
    correct_num_all = correct_num_all+correct_num;
    fprintf('Judgment rate of Class %1d is %.2f%%\n',A1-1,100*correct_rate);
end

   這就得到了每一條待驗證的資料被判斷為0-9的概率,通過比較這10個概率值的大小即可得出最終判斷。

3.3 注意點

   讀者可能注意到這一段的架構和我上一篇部落格的架構幾乎一模一樣,解說文字也一樣,這並不是我偷懶,而是這兩個實驗的確非常相似。同樣地,這裡的注意點也包括了上一篇部落格中第3.3章節所提到的那三點需要注意的地方,在此我就不再贅述了。

   但是,它們兩者之間也有幾點重要的不同,而正是這些不同使得這個小專案實際上比上一篇部落格中的語音性別識別更難了一些。不同點主要有3個,下面進行簡要介紹。

   第一、語音性別識別是一個二分類問題,而這是一個10分類問題。

   第二、語音性別識別需要考慮的特徵有20個,也就是說後面需要計算20個概率連乘;而這裡需要考慮的特徵最多可以達到 28x28 ,即784個,也就是說最多需要計算784個概率連乘。而這就使得必須進行常數補償,因為MATLAB也無法表示10的負784次方這種變態數字。

   第三、語音性別識別的量化階可以設定為10或20或更高,但是設定更高已經沒有意義;而這裡影象的量化階有可能需要設定成256,因為0-255的灰度圖我們經常使用到,不能說沒有意義。這一點會直接擴大需要查詢的概率表,著實讓程式設計師(也就是我了)痛苦了一翻,尤其是在檢查bug的時候。當然了,如果是bw二值圖就方便多了,此時每一個畫素點非0即1,概率表直接降級為一個一維陣列。

   第四點是一個說明:為什麼上面第二點說“最多”784個,也就是還有可能小於784個。對於一個 28x28 的影象,我們既可以逐畫素地處理,也可以“逐組”地處理——每多個畫素劃分成一組就OK了。例如我可以每 2x2 個畫素劃為一組,這樣一張圖就有 14x14 個組,這樣就只需要計算 14x14=196 個概率連乘了。那麼也可以每 4x4 個畫素劃為一組,計算 7x7=49 個概率連乘……這樣做的好處是可以減少特徵數,但是壞處有2個:第一,處理的精細程度下降了,這就相當於把 28x28 的影象resize 變小了,最後的識別效果是會下降的;第二,擴大了需要查詢的概率表,對於bw二值影象來說,原來一個畫素非0即1,而現在一個 NxN 的“組”的畫素取值範圍變成了 0-(N^2) 。這之間如何取捨,還得靠實驗驗證,最後權衡之後選一個最合適的值。

4 實驗與結果分析

   影響實驗結果的因素包含但不限於以下因素:

   1. 訓練集數量;    2. “組”大小的劃分,即下表中的“幀尺寸”;    3. 影象格式,是二值圖還是灰度圖,實質上是量化階數量的選取;    4. 每“組”內的畫素劃分,即下表中的“分類法”,相當於是把每“組”作為一個整體視為二值圖還是4/16值圖等等;    5. 閾值,即若要二值化處理,怎樣判定每“組”是黑還是白。

   在進行了若干次有效試驗之後,得到如圖4所示的實驗結果:

圖4 實驗結果

   從上圖可以得出以下幾個結論:

   1. 在一定範圍內,幀尺寸越小,識別效果越好。當幀尺寸為 1x1 ,即逐畫素處理時,識別正確率最高,可以達到84%左右;    2. 在一定範圍內,訓練集數目越多,識別效果越好。    3. 隨著幀尺寸的逐漸減小,識別正確率的提升越來越緩慢。而這也是可以預料的,某單一因素的改善對最終結果的改善總有上限,並且改善效果越來越低;    4. 比較Lab12的兩組資料,前者在概率連乘時沒有常數補償,而後者加了乘以sqrt(2)的補償,最終效果有明顯差異,這提醒我們在使用貝葉斯樸素分類器時一定要注意數值計算——尤其是概率連乘——可能帶來的問題;

   讀者可以自行設計更多的實驗,可能會發現更多的結果。

5 後記

   這是我在18年上半年為《數字影象處理》做的小練習,年代不算久遠,而且程式碼完全是自己寫的,所以還記得不少當時的想法。因此,如有讀者對程式碼任何細節有疑問,歡迎隨時來與我聯絡。

   程式碼最終的識別效果84.59%,看上去似乎還不錯,但是如果用自己手寫的影象一個一個地去測試就會發現,正確率完全達不到這個資料。什麼原因呢?我想很可能是因為訓練資料太多了,使得訓練出來的模型擁有了太多的“個性”,以至於掩蓋了我們所希望它所應當具備的“共性”,或者說是“抽象性”。我有一個同學是用CNN來做的MINIST,最後驗證集的識別正確率可以達到97%還多,但是最後測試的時候也免不了會遇到這個問題。

   由於影象實在太多了(7萬張),壓縮很耗時,所以影象我就不傳了,但是影象解碼的程式碼我會上傳的,所謂“授人以魚不如授人以漁”嘛;另外,models資料夾我也清空了,給讀者自己設計實驗的空間。

   轉載時務必註明來源及作者。尊重智慧財產權從我做起。

   程式碼與其他資料已上傳至網路,歡迎下載,密碼是hpyx