クイックノート

ちょっとした発見・アイデアから知識の発掘を

【R】手書き文字 omniglot で遊んでみる 【keras】

ディープラーニングのライブラリ keras では、
MNIST という数字の手書き文字のデータセット
用意されています。

ところが、数字なので、10種類しかなく、
沢山の種類のデータを使いたいという場合には不便です。

手書き文字のデータセットとして、
MNISTの他に、omniglot と呼ばれるデータセットがあります。

omniglot では、各国の文字の手書きデータが提供されており、
ひらがなやカタカナ等も含めた 50 種類の言語のデータがあります。

データセットは、下のリンクから得ることができます。

github.com

ここでは、この omniglot のデータを使って、
手書き画像の特徴量変換を行ってみましょう。

omniglot とは

omniglot とは、手書きの文字の画像データをまとめたデータセットです。

50種類の言語(アルファベット)について、
各文字の手書き画像が20枚ずつ用意されています。

データセットは下のリンクから取得可能です。

github.com

ここでは、python用のデータを使って、
R上でkerasに学習させます。

images_background が学習用に用意された30種類の言語データで、
images_evaluationがテスト用に用意された20種類の言語データです。

データの読み込み

データは、言語ごと、そして、文字ごとにディレクトリ分けされています。

この画像データをR上に読み込みましょう。

画像の読み込み

ディレクトリを辿って行って、画像データを読み込みます。
この時、言語と文字のラベルも一緒に取っておきましょう。

下のコードで、訓練データとテストデータを分けて読み込みます。
ただし、データの保存先に応じてtrain_dirtest_dirの指定が必要です。

library(png)
library(stringi)
library(abind)
library(rasterImage)

omniglot.read_dir = function(dir){
  names = list.files(dir)
  
  imgs = list()
  alfname = list()
  char_id = list()
  
  for(name in names){
    chars = list.files(file.path(dir,name),full.names = T)
    for(char in chars){
      img_files = list.files(char,full.names=T)
      for(imgf in img_files){
        imgs[[length(imgs)+1]] = readPNG(imgf)
        alfname[[length(alfname)+1]] = name
        char_id[[length(char_id)+1]] = stri_match(imgf,regex="([0-9]+)_[0-9]+\\.png")[,2]
      }
    }
  }
  
  x = abind(imgs,along=3)
  x = aperm(x,c(3,1:2))
  
  return(list(x=x,alfname=unlist(alfname),char_id=unlist(char_id)))
}

omniglot.load = function(train_dir="~/Downloads/images_background/",
                         test_dir ="~/Downloads/images_evaluation/"){
  train = omniglot.read_dir(train_dir)
  test = omniglot.read_dir(test_dir)
  
  return(list(train=train,test=test))
}

omniglot = omniglot.load()
train = omniglot$train
test = omniglot$test

読み込んだ画像を表示してみる

読み込みが正しく行えていることを確認するため、
画像を表示してみましょう。

下のコードでは、ひらがなの最初の20文字をプロットしています。

omniglot.plot_multi = function(imgs){
  N = dim(imgs)[1]
  n = ceiling(sqrt(N))
  plot(0,0,xlim=c(0,105*n),ylim=c(0,105*n),type="n",yaxt="n",xaxt="n",xlab="",ylab="")
  
  for(i in 1:N){
    x = (i-1) %% n 
    y = (i-1) %/% n 
    rasterImage(imgs[i,,],xleft=105*x,ybottom = 105*y,
                xright = 105*(x+1),ytop = 105*(y+1))
  }
}
omniglot.plot_multi(train$x[train$alfname=="Japanese_(hiragana)",,][1:400,,])

f:id:u874072e:20181204151214p:plain

正しく読み込めていそうですね。

ディープラーニングによる特徴量変換

データが読み込めたので、ディープラーニングに突っ込んで、
特徴量変換してみます。

今回は、Siamese Network を使って、
特徴量変換を学習します。

Siamese Network とは

Siamese Network とは、
特徴量変換のネットワークと、
距離ベースの判別器のネットワークの組み合わせによって、
特徴量変換を学習するものです。

入力としては、2種類の画像を入力して、
それぞれを特徴量変換にかけて、
その後、判別器が2種類が同じものか違うものかを判断します。

判別器は距離が近いものを同じものとして判別するので、
学習を進めていくと、特徴量変換は、
同じ文字を近くに、違う文字を遠くに置くように、
特徴量変換をするようになります。

モデルの構成

下のコードで、特徴量変換のモデルと、
判別器と連結したネットワークを構成しています。

特徴量変換や、判別器をどのようなモデルにするかに自由度がありますが、
ここでは、特徴量変換に簡素なCNN、
判別器は、二つの特徴量の絶対差をベースに判別するものとしています。

siamese.model_feat_map = function(reg_par=0,input_shape){
  map = keras_model_sequential() %>%
    layer_conv_2d(filters = 32, kernel_size = c(3,3), activation = 'linear',
                  input_shape = input_shape,kernel_regularizer = regularizer_l2(reg_par)) %>%
    layer_batch_normalization() %>%
    layer_activation_parametric_relu() %>%
    layer_max_pooling_2d(pool_size=c(3,3)) %>%
    layer_conv_2d(filters = 64, kernel_size = c(3,3), activation = 'linear',kernel_regularizer = regularizer_l2(reg_par)) %>%
    layer_batch_normalization() %>%
    layer_activation_parametric_relu() %>%
    layer_max_pooling_2d(pool_size=c(3,3)) %>%
    layer_flatten() %>%
    layer_dense(units = 64, activation = 'linear',kernel_regularizer = regularizer_l2(reg_par)) %>%
    layer_activation_parametric_relu() %>%
    layer_dense(units = 32, activation = "linear",kernel_regularizer = regularizer_l2(reg_par)) 

  return(map)
}

siamese.model_for_train = function(map,input_shape){
  p_input =layer_input(input_shape)
  v_input =layer_input(input_shape)
  
  
  p_map = p_input %>% map
  v_map = v_input %>% map
 
  
  d_nn = layer_subtract(list(p_map,v_map)) %>%
    layer_lambda(backend()$abs) %>%
    layer_dense(units=2,activation="softmax")
  
  model = keras_model(inputs=list(p_input,v_input),outputs=d_nn)
  
  model %>% compile(loss="categorical_crossentropy",
                    optimizer = optimizer_adam(),
                    metrics="accuracy")  
  
  return(model)
}

学習用データペアの生成

Siamese Network では、二つの画像をセットで入力して、
それが同じか違うかを判断します。

そのため、同じ画像のペアと違う画像のペアを
学習用データとして与える必要があります。

下では、ランダムに同じ画像のペアと
違う画像のペアを生成しています。

siamese.make_pairs = function(X,Y,n=10){
  p_X = list()
  v_X = list()
  y = NULL
  
  for(char in unique(Y)){
    print(char)
    for(j in 1:n){
      # same
      p_X[[length(p_X)+1]] = X[sample(which(Y==char),1),,]
      v_X[[length(v_X)+1]] = X[sample(which(Y==char),1),,]
      y = c(y,0)
      # diff
      p_X[[length(p_X)+1]] = X[sample(which(Y==char),1),,]
      v_X[[length(v_X)+1]] = X[sample(which(Y!=char),1),,]
      y = c(y,1)
    }
  }
  
  p_X = abind(p_X,along = 3)
  v_X = abind(v_X,along = 3)
  
  p_X = aperm(p_X,c(3,1,2))
  v_X = aperm(v_X,c(3,1,2))
  
  return(list(p_X,v_X,y))
}

モデルの学習

生成したデータのペアを与えて、
ニューラルネットワークの学習を行います。

  map = siamese.model_feat_map(input_shape = c(105,105,1))
  model = siamese.model_for_train(map,c(105,105,1))
  
  model %>% fit(x=list(array_reshape(train[[1]],c(dim(train[[1]]),1)),
                       array_reshape(train[[2]],c(dim(train[[2]]),1))),
                       y=to_categorical(train[[3]],2),
                       epochs = 2,validation_split = 0.1)

学習の結果

これで、画像から特徴量への変換写像mapが学習されているはずなので、
ここに、画像を入力して、特徴量への変換をしてみましょう。

下の関数では、画像を特徴量に変換して、
さらにその特徴量を二次元に写像した上で、
画像をプロットしています。

これで、似た画像・違う画像の位置関係が一目で分かるはずです。

plot.raster = function(map,x,y,ratio=1/sqrt(length(y))){
  feat_v = map %>% predict(array_reshape(x,c(dim(x)[1:3],1)))
  
  d = dist(feat_v,upper = T)
  feat2d = cmdscale(d)
  
  xmax = max(feat2d[,1])
  xmin = min(feat2d[,1])
  ymax = max(feat2d[,2])
  ymin = min(feat2d[,2])
  
  sx = (xmax-xmin) * ratio
  sy = (ymax-ymin) * ratio
  
  plot(0,0,xlim=c(xmin,xmax),ylim=c(ymin,ymax),type="n",xlab="",ylab="",yaxt="n",xaxt="n")
  
  for(i in 1:length(y)){
    rasterImage(x[i,,],
                xleft = feat2d[i,1] - sx/2,xright = feat2d[i,1] + sx/2,
                ybottom = feat2d[i,2] - sy/2,ytop=feat2d[i,2]+sy/2)
  }
  
}

訓練データの変換

まずは、学習データに含まれていた、
ひらがなの特徴量をプロットしてみます。

ids = which(omniglot$train$alfname=="Japanese_(hiragana)")[1:200]
x = omniglot$train$x[ids,,]
y = omniglot$train$char_id[ids]
  
plot.raster(map,x,y)

f:id:u874072e:20181204154653p:plain

同じ文字が近くにまとまっている様子が分かりますね。
また「い」と「け」のように形が近いものが近いようにも見えます。

テストデータの変換

次は、学習に用いられていない、
テストデータの特徴量をプロットしてみます。

x = omniglot$test$x[1:200,,]
y = omniglot$test$char_id[1:200]
  
plot.raster(map,x,y)

f:id:u874072e:20181204155034p:plain

こちらも同じ文字がまとまっている様子がわかります。

また、右上では、似たような文字が近くにまとまっている様子も見えます。

まとめ

omniglot のデータを使って、
特徴量変換を試してみました。

画像をそのまま特徴量空間(2次元に射影)にマッピングすることで、
学習結果が分かりやすくなりましたね。

プライバシーポリシー