2016年8月3日水曜日

ニューラルネットで雑な手書き文字認識

以前にもニューラルネットについて言及しましたが,かなり原初的な内容ばかりだったので今回は簡単な応用について.

C言語でニューラルネット(関数近似)

C言語でニューラルネット(2次元平面での分離)

といっても,修士の授業で作った内容そのままなので,いざ実装してみると動かないかも.

ニューラルネットの応用先として,真っ先に上がるのがパターン認識の分野でしょう.

とくに文字認識の分野ではすでにかなりの数,実用化されていると聞きます.

ここでは,最も単純な文字の認識について述べていきたいと思います.

具体的には以下の2点.

  • ニューラルネット(誤差逆伝播法)の基本的な部分
  • 特徴量(ニューラルネットへの入力)

まず,ニューラルネット自体についてですが,ニューラルネットワークという手法では,脳の電気的な回路をプログラムによって模擬することで,生物の柔軟な判断力を手に入れようという試みです.

よく教科書で以下のような図を見ると思います.
neuron

これは,脳のある細胞から,別のある細胞への繋がりを表した図です.

細胞体が外部/他の細胞体からの刺激を受けると,刺激に応じた大きさの電気信号が発生し,発生した電気信号は軸索およびシナプスを通って,別の接続されている細胞体に伝達されます.

これを模擬したものが以下の図になります.
neuron2

この時,が細胞体を,が細胞体からまでの結合強度を表します.

細胞体が外部の刺激に反応し,電気信号を発生した際,その電気信号と結合強度の積がある一定値を超えると,細胞体が励起(反応)し電気信号を発生します.

からの電気信号を入力,が励起されることによって発生する電気信号を出力を出力とすると,以下の様なグラフが得られます(青の破線).

sigmoid

この例では,励起するためのある一定値が0の場合です.

しかし,一般的には赤線のようなシグモイド関数が使用されることが多いようです.

当初は階段状の関数で定義されていたようなのですが,ニューラルネットの学習のための強力な手法である誤差逆伝播法(バックプロパゲーション法とも呼ぶ)を実装するためには,関数の微分が連続であるほうが都合が良かったことから,シグモイド関数が使われるようになったとのことです.

ステップ状の関数では出力は,

ですが,シグモイド関数では出力は,

で表されます.(はシグモイド関数のゲイン)


通常,文字認識では,学習の過程で教師信号を得ることができます.

これは例えば,手書きで書いた’A’という文字を学習させる際,”この手書き文字は’A’という文字ですよ“という情報(教師信号)をあらかじめ与えることが多いためです.

これによってニューラルネットは,送られてきた手書き文字(画像データ)が,’A’の文字であると知ることができます.

私たちは,以下ような文字が送られてきたとき,’D’なのか’O’なのかわからない場合もあるでしょう.

O

しかし,この人が’D’を以下のように書くと知っている場合,上の文字は’O’であると判断できるということと少し似ています.

D

文字の教師信号を得られる場合,ニューラルネットの出力と,答えの文字との誤差というものを得ることができます.

ニューラルネットの出力は,学習前はほとんどランダムに近い値ですが,適当(ランダム)に出力された信号と解答(教師信号)を照らしあわせて,その誤差が小さくなるように結合係数を調整していけば,いずれは正しい値に近い値を出すようになります.

この誤差が小さくなるように計算する手法の最も単純なものに,誤差逆伝播法(Back Propagation Neural Network; BPNN) があります.

今回,誤差逆伝播法では3層のニューラルネットを使います.

Neuralnet

I,H,O層はそれぞれ,入力層,隠れ層,出力層と呼ばれます.

入力層には個の入力が与えられ,隠れ層を通って出力層に達し,個の値(図では)を出力します.

未学習の時(結合係数がランダム),文字画像データを入力層に与えると,隠れ層,出力層の結合係数によって出力の値がランダムに決まります.

この出力と,教師信号の誤差は以下のようになります.

この誤差は二乗なため,のような下向きに凸で,全範囲にわたって正となる関数となります.
これを微分したものがになる点を考えると,その点が誤差が最小となる点になります.

上記の誤差の式を上手く計算すると,以下の結合係数の更新式が得られます.

ここで,は学習係数で,以上の値です.
この結合係数の更新は,出力層から入力層に向かって行われるため,誤差”逆”伝播法と呼ばれるそうです.


次に文字認識について.

手書き文字認識では,手書きの文字の画像を入力データとして,”その文字が何であるか”という情報を出力します.

例えば,[px]の文字画像があったとして,この4096個をそのままデータとしてニューラルネットに入力した場合,上手く学習することはできません

そればかりか,4096個のデータというのは学習するのに膨大な時間を要します.

これは人間でも同じことで,4096個のデータを覚えるのはとても大変です.
それでも,人は文字の認識をとても迅速に行うことができます.

私たちは通常,文字の書かれている範囲を一点ずつ見ていくのではなく,線の流れ,向き,本数,線の密度(文字の細かさ)などの”文字のもつ特徴“を見て文字を判断しています.

この特徴のことをそのまま,”特徴量“と呼びます.

パターン認識の分野では,適切な特徴量を見つけることがとても大切です.

2016年の最近では,ディープラーニングという,特徴量の抽出から判別(識別)までの全てを学習でやってしまう手法もあるようです.

こちらはgoogleがソフトウェアを公開しているようなので,使ってみるとよいのかもしれません.

Tensor Flow - Google

しかし,ここでは従来同様に特徴量を手動で抽出し,判別するという方針で行きます.
(行きます,というよりは数年前のプログラムがそうなっている,というだけ)


具体的な手法としては,まず,大きさが[px]に正規化された以下の様な文字画像が得られるとします.

character

  • まずはじめに,この画像の左上から[px]の正方形の画像(フレーム)を切り出します.

  • 次に,フレームの中に,以下の様な[px]の成分が,それぞれいくつ入っているかを成分ごとにカウントします.
    (このとき,真横に1pxの線が引かれていた場合には,縦成分,横成分,右上成分,右下成分のカウントはそれぞれ,0,15,0,0のようになります.)

component

  • 次に,先ほどの[px]のフレームの切り出しを,右に8pxだけずらして行います.
    (画像の破線部分)

  • 新たに得たフレーム内に,各成分がいくつ入っているかカウントします.

  • この流れを画像の左上から右下まで行います.
    このとき,フレームは個切り出せ,フレームごとに成分が4つあるので,全体として特徴量は196個得られます.

このようにして得られた196要素のベクトルをニューラルネットの入力として与えることで文字認識を行うことができます.

これは非常に単純な方法ですが,各フレームの位置が全体の文字の配置(バランス)を,各成分のカウント数が文字の密度と方向を表しているため,文字の全体の雰囲気や線の流れを掴むことができるようです.

これを基にC言語で実装し,学習/判別してみました.

以下は,学習に使用したデータです.
すべて私の文字ですが,本来はさまざまな人のデータを使用します.

learning

学習では次のパラメータ設定をしています.

隠れ層の次数 :
シグモイド関数のゲイン :
学習係数 :
結合係数の初期値 : のランダムな値
学習の反復回数 : 回 

学習後,このデータを入力した際には,全ての文字で正しい認識がされました.
既学習のデータについては,間違いなく識別できています.

次に,以下の未学習の文字について,識別を行いました.

unknown

上の行は私の字,下の字は他人(母親ですが)の字です.

これの識別結果が以下.

A B C D E F G
T T T T T T T
T T T F (C) F (C) T F (C)

結果として,私自身の字はよく識別できているのですが,他人の字では間違いが発生しています.
括弧内は,何と間違えて出力されたのかです.

上手く識別できなかった原因はいくつか考えられます.

  • 自分の字でしか学習していない(過学習)

  • 画像の大きさが正規化されていない

  • 特徴量が世の中で利用されているものと比べて非常に単純

特に,今回の手法は読んだ論文の手法を簡略化して使っているので,上の行の未学習データが識別できているのが奇跡なくらいです.

手書き文字認識における特徴量の次元数と変数変換に関する考察

また,本来は画像の正規化を行うのですが,時間がなかったため,最初から[px]の画像に文字を書いて,正規化せずに使っているので画像全体に対する文字の大きさが,下の行のものだとまちまちになってしまっています.

このあたりの影響が結果に大きく出ているのではないかと思います.

授業のレポートとしては充分な結果だったので,このあたりで検証は終わっていますが,単純ながらなかなか良好な結果だったと思います.


以下リスト.
アップロード前に動作確認をしていないので動くかどうか笑

ファイル構造

Dir
 |- character
 |   |- A.pbm
 |   |- A2.pbm
 |   |- :
 |   |- A5.pbm
 |   |- B.pbm
 |   |- :
 |   +- G5.pbm
 |- main.c
 +- char_name.dat

char_name.dat

A
A2
:
A5
B
:
G5

main.c

/*  ./characterディレクトリ内のGimp Image Editorにより作成されたA~Zのアルファベットの64x64[px]の二値画像(.pbm)を入力とし学習を行う.    */
/*  学習イメージ名はA.pbm,A2.pbm,A3.pbm,...,Z.pbm,Z2.pbm,... のような先頭にアルファベットを付与した形式とし,           */
/*  プログラムと同一ディレクトリ内にA,A2,...,Z,Z2,... のようなイメージ名から拡張子を取り除いたものをchar_name.datファイル内に縦に列挙する. */
/*  プログラム内の#define Input_sizeを学習データの数に設定する.                                 */
/*  引数なしで実行すると学習無しでの実行,何らかの引数ありで実行すると学習ありでの実行を行う.                   */

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
#include<string.h>

#define Input_size 35
#define Pic_size 64
#define Vect_size (((Pic_size-16)/8+1)*((Pic_size-16)/8+1)*4)
#define Input_dim Vect_size
#define Hidden_dim 50
#define alpha 0.1
#define eta 0.8
#define gain 100
#define test_size 1
#define th 0.5
#define RAND_GAIN 0.2

#define CHARACTER_FILE "./character/"
#define INPUT_NAME "learning.dat"
#define RHO_NAME "teacher.dat"
#define TEST_NAME "test.dat"

double a[Input_size][Input_dim]={0},rho[Input_size]={0};
double v[Hidden_dim][Input_dim+1]={0},w[Hidden_dim+1]={0};
double I[Input_dim+1]={0},H[Hidden_dim+1]={0};
double E[Input_size]={0};
double O=0;
double O_out[Input_size]={0},test_out[test_size];
double dk,dj[Hidden_dim]={0};

void Feature(int buff[Pic_size][Pic_size], int vec[Pic_size/16-1]);
void Convert(char inname[20],char outname[20]);
void Loading();
void Init();
void Display();
char Check(double num);
void Test();
void Save();

int main(int argc,char *argv[])
{
    int i,j,k,t;
    double temp,E_max=0;

    Loading();
    Init();

    //main loop
    k=0;

    while(argv[1]!=NULL){
        k++;

        for (t=0;t<Input_size;t++){
            for (i=0;i<Input_dim;i++){
                I[i+1]=a[t][i];
            }

            for (i=0;i<Hidden_dim;i++){
                temp=0;
                for (j=0;j<Input_dim+1;j++){
                    temp+=I[j]*v[i][j];
                }
                H[i+1]=1/(1+exp(-alpha*temp));
            }

            temp=0;
            for (i=0;i<Hidden_dim+1;i++){
                temp+=H[i]*w[i];
            }

            O=1/(1+exp(-alpha*temp));

            E[t]=1/2.0*pow(gain*(rho[t]-O),2);

            dk=-(rho[t]-O)*O*(1-O);

            for (i=0;i<Hidden_dim;i++){
                dj[i]=dk*w[i+1]*H[i+1]*(1-H[i+1]);
            }

            for (i=0;i<Hidden_dim+1;i++){
                w[i]-=eta*dk*H[i];
            }

            for (i=0;i<Hidden_dim;i++){
                for(j=0;j<Input_dim+1;j++){
                    v[i][j]=v[i][j]-eta*dj[i]*I[j];
                }
            }

            O_out[t]=O*gain;
        }

        if (k>=1e4){
            printf("\nk=%d\n\nE=\n",k);
            for (i=0;i<Input_size;i++){
                printf("%f\n",E[i]);
            }
            printf("\nO_out=\n");
            for (i=0;i<Input_size;i++){
                printf("%f\t",O_out[i]);
                printf("%c\n",Check(O_out[i]));
            }
            break;
        }
    }

    Save(); 
    Test();

    return 0;
}

void Feature(int buff[Pic_size][Pic_size], int vec[Vect_size]){
    int i,j,k,l,n,temp[4]={0};

    n=0;
    for (k=0;k<7;k++){
        for (l=0;l<7;l++){
            for (i=0;i<4;i++){
                temp[i]=0;
            }
            for(i=0;i<16-1;i++){
                for(j=0;j<16-1;j++){
                    if(buff[i+8*k][j+8*l]==1 && buff[i+8*k+1][j+8*l]==1 && buff[i+8*k][j+8*l+1]==0 && buff[i+8*k+1][j+8*l+1]==0){
                        temp[0]++;
                    }
                    if (buff[i+8*k][j+8*l]==1 && buff[i+8*k+1][j+8*l]==0 && buff[i+8*k][j+8*l+1]==1 && buff[i+8*k+1][j+8*l+1]==0){
                        temp[1]++;
                    }
                    if ((buff[i+8*k][j+8*l]==1 && buff[i+8*k+1][j+8*l]==1 && buff[i+8*k][j+8*l+1]==0 && buff[i+8*k+1][j+8*l+1]==1) || (buff[i+8*k][j+8*l]==1 && buff[i+8*k+1][j+8*l]==0 && buff[i+8*k][j+8*l+1]==1 && buff[i+8*k+1][j+8*l+1]==1)){
                        temp[2]++;
                    }
                    if ((buff[i+8*k][j+8*l]==1 && buff[i+8*k+1][j+8*l]==1 && buff[i+8*k][j+8*l+1]==1 && buff[i+8*k+1][j+8*l+1]==0)||(buff[i+8*k][j+8*l]==0 && buff[i+8*k+1][j+8*l]==1 && buff[i+8*k][j+8*l+1]==1 && buff[i+8*k+1][j+8*l+1]==1)){
                        temp[3]++;
                    }
                    if (buff[i+8*k][j+8*l]==1 && buff[i+8*k+1][j+8*l]==1 && buff[i+8*k][j+8*l+1]==1 && buff[i+8*k+1][j+8*l+1]==1){
                        temp[0]++;
                        temp[1]++;
                        temp[2]++;
                        temp[3]++;
                    }
                }
            }
            for(i=0;i<4;i++){
                vec[n]=temp[i];
                n++;
            }
        }
    }
}

void Convert(char inname[20],char outname[20]){
    int i,j,k,buff[Pic_size][Pic_size],f[4][Pic_size][Pic_size]={0},temp[Vect_size]={0};
    FILE *fi,*fo;
    char s[Pic_size*Pic_size],c;

    if ((fo=fopen(outname,"a"))==NULL){
        printf("OUTPUT LEARNING FILE OPEN ERROR !\n");
        exit(-1);
    }

    if ((fi=fopen(inname,"r"))==NULL){
        printf("INPUT CHARACTER FILE %s OPEN ERROR !\n",inname);
        exit(-1);
    }

    for (i=0;i<3;i++){
        fgets(s,Pic_size*Pic_size,fi);
    }

    i=0;
    while ((c=getc(fi))!=EOF){
        if(c == '1' || c == '0'){
            s[i]=c;
            i++;
        }
    }

    for(i=0;i<Pic_size;i++){
        for(j=0;j<Pic_size;j++){
            buff[i][j]=(int)(s[Pic_size*i+j]-'0');
        }
    }

    Feature(buff,temp);

    for (k=0;k<Vect_size;k++){
        fprintf(fo,"%d ",temp[k]);
    }

    fprintf(fo,"\n");

    fclose(fo);
    fclose(fi);

}

void Loading(){
    FILE *fp;
    int i,j;
    char c[Input_size][20];
    char inname[Input_size][20];


    /* pbm data file input */
    if ((fp=fopen("char_name.dat","r"))==NULL){
        printf("char_name.dat open error !\n");
        exit(-1);
    }

    for(i=0;i<Input_size;i++){
        fgets(c[i],20,fp);
    }
    fclose(fp);

    for (i=0;i<Input_size;i++){
        strcpy(inname[i],CHARACTER_FILE);
        strcat(inname[i],c[i]);
        for(j=0;j<20;j++){
            if(inname[i][j]=='\n'){
                inname[i][j]='\0';
            }
        }
        strcat(inname[i],".pbm");
    }

    if ((fp=fopen(INPUT_NAME,"w"))==NULL){
        printf("learning.dat OPEN ERROR\n");
        exit(-1);
    }
    fclose(fp);

    for (i=0;i<Input_size;i++){
        Convert(inname[i],"learning.dat");
    }

    if ((fp=fopen(RHO_NAME,"w"))==NULL){
        printf("OUTPUT TEACHER FILE OPEN ERROR !\n");
        exit(-1);
    }

    for (i=0;i<Input_size;i++){
        fprintf(fp,"%d\n",(int)(c[i][0]-'A'+1));
    }
    fclose(fp);


    //Input a
    if ((fp=fopen(INPUT_NAME,"r"))==NULL){
        printf("INPUT FILE open error!\n");
        exit(-1);
    }

    for (i=0;i<Input_size;i++){
        for (j=0;j<Input_dim;j++){
            fscanf(fp,"%lf ",&a[i][j]);
        }
    }   
    fclose(fp);

    //Input rho 
    if ((fp=fopen(RHO_NAME,"r"))==NULL){
        printf("RHO FILE open error!\n");
        exit(-1);
    }

    for (i=0;i<Input_size;i++){
        fscanf(fp,"%lf ",&rho[i]);
    }
    fclose(fp);
}

void Init(){
    int i,j;
    FILE *fp;

    srand((unsigned int)time(NULL));

    for(i=0;i<Input_size;i++){
        rho[i]=rho[i]/gain;
    }

    I[0]=1;
    H[0]=1;

    if((fp=fopen("v.dat","r"))==NULL){
        printf("v.dat not found\n");
        for (i=0;i<Hidden_dim;i++){
            for (j=0;j<Input_dim+1;j++){
                v[i][j]=RAND_GAIN*rand()/(double)RAND_MAX-0.1;
            }
        }
    } else {
        for (i=0;i<Hidden_dim;i++){
            for(j=0;j<Input_dim+1;j++){
                fscanf(fp,"%lf",&v[i][j]);
            }
        }
        fclose(fp);
    }

    if((fp=fopen("w.dat","r"))==NULL){
        printf("w.dat not found\n");
        for (i=0;i<Hidden_dim+1;i++){
            w[i]=RAND_GAIN*rand()/(double)RAND_MAX-0.1;
        }
    } else {
        for (i=0;i<Hidden_dim+1;i++){
            fscanf(fp,"%lf",&w[i]);
        }
        fclose(fp);
    }
}

void Display(){
    int i,j;

    printf("a=\n");
    for(i=0;i<Input_size;i++){
        for(j=0;j<Input_dim;j++){
            printf("%f\t",a[i][j]);
        }
        printf("\n");
    }

    printf("\nrho=\n");
    for(i=0;i<Input_size;i++){
            printf("%f\n",rho[i]);
    }

    printf("\nv=\n");
    for (i=0;i<Hidden_dim;i++){
        for (j=0;j<Input_dim+1;j++){
            printf("%f\t",v[i][j]);
        }
        printf("\n");
    }
}

char Check(double num){
    char c;

    c=(int)(num+0.5); //num=0~25
    if (c>=0 && c <26){
        return 'A'+c-1;
    } else {
        return '?';
    }
}

void Test(){
    int i,j,t;
    double max[Input_dim]={0},min[Input_dim]={0};
    double p;
    double test[test_size+1][Input_dim];
    double temp;
    char inname[20];
    FILE *fp;

    if ((fp=fopen(TEST_NAME,"w"))==NULL){
        printf("test.dat OPEN ERROR\n");
    }
    fclose(fp);

    printf("Input  64 x 64 pixel character file name\n");
    scanf("%s",inname);

    for (i=0;i<test_size;i++){
        Convert(inname,TEST_NAME);
    }

    if ((fp=fopen(TEST_NAME,"r"))==NULL){
        printf("INPUT FILE open error!\n");
        exit(-1);
    }

    for (i=0;i<test_size;i++){
        for (j=0;j<Input_dim;j++){
            fscanf(fp,"%lf ",&test[i][j]);
        }
    }   
    fclose(fp);

    for (t=0;t<test_size;t++){
        for (i=0;i<Input_dim;i++){
            I[i+1]=test[t][i];
        }

        for (i=0;i<Hidden_dim;i++){
            temp=0;
            for (j=0;j<Input_dim+1;j++){
                temp+=I[j]*v[i][j];
            }
            H[i+1]=1/(1+exp(-alpha*temp));
        }

        temp=0;
        for (i=0;i<Hidden_dim+1;i++){
            temp+=H[i]*w[i];
        }

        O=1/(1+exp(-alpha*temp));

        /*test*/
        printf("\nO=%f\n",O*gain);

        test_out[t]=O*gain;
    }
    printf("\nClassification=\n");
    for (i=0;i<test_size;i++){
        printf("%c\n",Check(test_out[i]));
    }
}

void Save(){
    int i,j;
    FILE *fp;

    if ((fp=fopen("v.dat","w"))==NULL){
        printf("v.dat write error!\n");
    }

    for(i=0;i<Hidden_dim;i++){
        for(j=0;j<Input_dim+1;j++){
            fprintf(fp,"%f ",v[i][j]);
        }
    }

    fclose(fp);

    if ((fp=fopen("w.dat","w"))==NULL){
        printf("w.dat write error!\n");
    }

    for(i=0;i<Hidden_dim+1;i++){
        fprintf(fp,"%f ",w[i]);
    }

    fclose(fp);

    if ((fp=fopen("./O_out.dat","w"))==NULL){
        printf("O_out.dat open Error!\n");  
        exit(-1);
    }

    for (i=0;i<Input_size;i++){
        fprintf(fp,"%c\n",Check(O_out[i]));
    }

    fclose(fp);

    if ((fp=fopen("./test_out.dat","w"))==NULL){
        exit(-1);
    }

    for (i=0;i<test_size;i++){
        fprintf(fp,"%c\n",Check(test_out[i]));
    }
    fclose(fp); 
}

0 件のコメント:

コメントを投稿