機械学習の学習用に画像情報抽出とラベル付けが自動でできる前処理アプリをRailsで作ってみた

# はじめに

[![logo_horizontal.png](https://qiita-image-store.s3.amazonaws.com/0/130181/be63b003-0dfa-e1c1-9282-ca12b02f00a4.png)](http://www.kikagaku.co.jp/)


機械学習とは非常に便利なもので、データさえそろっていれば、**医療用のMRIデータから癌（がん）の発見**ができたり、**株価の予測**ができたりと、応用範囲は多岐に渡ります。

しかし、ここでひとつ問題があります。
そうです。

**「データさえそろっていれば」**です。

そして、声を大にして言いたいところです。

**「まずデータがそろっていない！」**

結局のところ、機械学習の**数学**や**プログラミング**ができるようになったとしても、このデータを作り出す力が身につかない限り、現場のデータに応用できないもので、お困りの方も多いのではないでしょうか。

特にデータ解析では、はじめに**「ざっくりとした解析で傾向を素早く見られるか」**が重要です。
実際にデータ解析を行う際は、**データの前処理** → **プログラミング** → **モデルの学習** が主な開発の工程であり、なんと**「データの前処理が開発時間全体の80%を占める」**とも言われています[*](http://japan.zdnet.com/article/35063353/)。
この**前処理**の時間をいかに削減できるかが、**成功の鍵**といえるのではないのでしょうか。

そこで、今回は**機械学習向けに使用できる前処理用アプリ**を作ってみたので、**使い方**から**作り方**まで全てをご紹介します。


#### ご協力お願いします！

こちらの記事が参考になった方は、こちらの記事に『**いいね**』をしていただけると嬉しいです。

## 著者紹介

![02.jpg](https://qiita-image-store.s3.amazonaws.com/0/130181/32a1e616-fe1c-6ffa-fa7f-430138c52b3d.jpeg)

私は[**株式会社キカガク**](http://www.kikagaku.co.jp/)代表取締役の[**吉崎亮介**](http://blog.kikagaku.co.jp/)と申します。
現在は『[**機械学習・人工知能 脱ブラックボックスセミナー**](http://www.kikagaku.co.jp/services/seminar/)』や『[**機械学習のオンライン家庭教師**](http://www.kikagaku.co.jp/services/online-tutor/)』を運営しております。

#### 略歴

|所属|学科・部署|研究内容|賞罰|
|:--|:--|:--|:--|
|舞鶴高専|電子制御工学科|画像処理（AR）を研究||
|舞鶴高専|電気・制御システム工学専攻|ロボット工学・システム制御・最適化を研究|
|京都大学大学院|情報学研究科（[加納研](http://human.sys.i.kyoto-u.ac.jp/)）|製造業に向けた機械学習の応用研究|ADCHEM2016最優秀論文賞、[化学工学会技術賞](http://www2.scej.org/sis/awards/)|
|株式会社SHIFT|社長室|人工知能によるソフトウェアテスト自動化の研究|[CEDEC2016登壇](http://cedec.cesa.or.jp/2016/session/PRD/12366.html)|
|株式会社Carat|取締役兼COO|最適な旅程提案アプリ（自然言語処理・最適化）||
|[株式会社キカガク](http://www.kikagaku.co.jp/)|代表取締役社長|[機械学習・人工知能セミナー](http://www.kikagaku.co.jp/services/seminar/)や[オンライン家庭教師](http://www.kikagaku.co.jp/services/online-tutor/)||

#### 株式会社キカガク

[![logo_horizontal.png](https://qiita-image-store.s3.amazonaws.com/0/130181/be63b003-0dfa-e1c1-9282-ca12b02f00a4.png)](http://www.kikagaku.co.jp/)

機械学習や人工知能の教育サービスを提供


#### フォローお待ちしております

ビジネス目線の機械学習・人工知能の情報やオススメの参考書について発信しています。

代表取締役社長　吉崎 亮介
Twitter：[@yoshizaki_kkgk](https://twitter.com/yoshizaki_kkgk)
Facebook：[@ryosuke.yoshizaki](https://www.facebook.com/ryosuke.yoshizaki)
Blog：[キカガク代表のブログ](http://blog.kikagaku.co.jp/)


# 完成イメージ

今回は、**「画像からテキストを認識する」**といった解析を前提とします。
ただし、これは一例であり、ご紹介するプログラムをうまくカスタマイズしていただければ、色々なパターンに適用可能です。

**「百聞は一見にしかず」**
まずは完成イメージを御覧ください。

## 手作業によるラベル付け

まずは、最終的に出来上がる動作サンプルをご覧ください。

![tegaki01.gif](https://qiita-image-store.s3.amazonaws.com/0/130181/ff14ab65-f076-4936-6adf-47954b6d8b3f.gif)

**前処理**として行うことは以下の通りです。
１．切り取りたい範囲の座標をクリックで選択
２．対応するテキストを書き込みラベル付け

## ラベル付けしたデータのダウンロード

ラベル付けを行ったデータを**CSVファイル**として簡単にダウンロードできるようにしています。

![tegaki02.gif](https://qiita-image-store.s3.amazonaws.com/0/130181/c21c8df8-c14b-40d7-7a6e-6de1be62cf9c.gif)


いかがでしょうか。

もちろん「座標の値を抽出して、ラベル付けを行い、CSVファイルに入力していく」こういった作業は手作業でできるのですが、この前処理用のアプリケーションを使うだけで遥かに**作業スピードを向上させる**ことができます。


# 前処理用アプリの使い方

## 読者の前提知識

**Ruby on Rails** をある程度理解している方を対象にしております。
Ruby on Railsについては [Rails Tutorial](http://railstutorial.jp/) や [ドットインストール](http://dotinstall.com/) で勉強されると良いと思います。

## 開発環境

* OS：macOS Sierra (10.12.2)
* プログラミング言語：Ruby 2.3.1
* Webフレームワーク：Rails 5.0.0.1
* DB：Sqlite
* テンプレートエンジン：Slim　（インストールは[こちら](http://qiita.com/carat_yoshizaki/items/b37c656956766e6a7122)を参照）
* CSSフレームワーク：BootStrap　（インストールは[こちら](http://qiita.com/carat_yoshizaki/items/27a0be0830135a9db91c)を参照）

## ソースコード

[GitHub](https://github.com/ryo813/tegaki_recog) でソースコードを公開しているので、こちらから fork→clone などをして、お手元の環境にダウンロードします。
そして、ダウンロードしてきたディレクトリに作業先として移動します。

## ライブラリ群のインストール

```bash:必要なgemのインストール
$ bundle install
```

こちらで必要なライブラリ群は入ると思います。

## データベースの構築

このあたりは、すでに設定済みのため、以下のコマンドひとつで大丈夫だと思います。

```bash:データベースの構築
$ rake db:migrate
```

## サーバーの立ち上げ

あとは、ローカル環境でサーバーを立ち上げれば完了です。

```bash:ローカルサーバーの立ち上げ
$ rails s
```

あとは、http://localhost:3000/ へアクセスすると、使用できるのではないでしょうか。


## （必要であれば）データの初期化処理

ただし、最初は画像の初期化ができていない可能性があるので、もしエラーが出た場合は、http://localhost:3000/image/reset にアクセスをしてみてください。
こちらにアクセスしてもらうと、画像を含めた設定の初期化が完了します。

![tegaki03.gif](https://qiita-image-store.s3.amazonaws.com/0/130181/1ca91791-93fe-70cc-728e-0445991f199b.gif)


## データの入力作業

トップページがラベル付けする用の作業画面となっています。
http://localhost:3000/

![tegaki01.gif](https://qiita-image-store.s3.amazonaws.com/0/130181/ff14ab65-f076-4936-6adf-47954b6d8b3f.gif)

冒頭でご紹介した通り、画像中の**「左上」**と**「右下」**をクリックすると、枠で囲まれるため、そこに対応したテキストを入力します。
間違って選択した場合は、最下部の**「リセット」**により、やり直しが可能です。
また、前の操作のみ取り消しが可能になっており、左上の**「前の登録を取り消し」**ボタンから操作できます。

すべてのラベル付けが完了すれば、**「次の画像へ」**により次の画像へ移動できます。

画像は `app/assets/images/` の中に入っている `.jpg`、`.png`、`.jpeg` のファイルが順番に読み込まれるようになっていますので、ラベル付けしたい画像ファイルは予め `app/assets/images/` に格納しておいてください。
※ 最初は、サンプル用の画像 `app/assets/images/image_sample_01.png` と `app/assets/images/image_sample_02.png` が入っています。


## ラベル付けしたデータのダウンロード

ラベル付けを行ったデータをCSVファイルとして簡単にダウンロードできるようにしています。
ダウンロードファイルへのアクセス先はこちらです。
http://localhost:3000/image/download

![tegaki02.gif](https://qiita-image-store.s3.amazonaws.com/0/130181/c21c8df8-c14b-40d7-7a6e-6de1be62cf9c.gif)

# Railsアプリのソースコードを解説

それでは、ソースコードの中身を解説していきます。
Ruby on Rails は MVC (Model View Controller) デザインパターンのため、それぞれについて説明していきます。

## ルーティング

まず、MVCの前に、どのようにWebアプリが構成されているか全体感を掴むために、ルーティング部の紹介です。
基本的にはトップページのみの構成のため、非常にシンプルですが、画像を操作するためのメソッドをいくつか紐付けています。
こちらのそれぞれの機能に関しては、後述する Contoller の説明をご覧ください。

```rb:app/config/routes.rb
Rails.application.routes.draw do
  root 'image/register#index'

  namespace :image do
    post 'register' => 'register#register'
    post 'next'     => 'register#next'
    get  'reset'    => 'register#reset'
    get  'download' => 'register#download'
    delete 'delete' => 'register#delete'
  end
end
```

## Model

今回は `Image` `TextBlock` `TmpImage` と名付けた３つのモデルを使用しています。
詳細は以下のとおりです。

```rb:３つのモデル
# *** 画像情報 ***
Image
  id: integer
  filename: string
  is_complete: boolean  # すべてのラベル付けが完了するとtrue
  is_none: boolean
  created_at: datetime
  updated_at: datetime

# *** ラベル情報  ***
TextBlock
  id: integer
  image_id: integer
  text: string  # テキストの情報
  x1: integer   # 座標値
  x2: integer   # 座標値
  y1: integer   # 座標値
  y2: integer   # 座標値
  created_at: datetime
  updated_at: datetime

# *** ラベル編集中の画像を一時的に保存 ***
TmpImage
  id: integer
  filename: string
  image_id: integer
  created_at: datetime
  updated_at: datetime
```

こちらを確認したい場合は、

```bash:pryによるモデルの確認
$ rails c
[1] pry(main)> show-models
```
と入力してもらえれば、表示されるはずです。

そして、`TextBlock`モデルの中でひとつメソッドを定義しています。

```rb:app/models/text_block.rb
class TextBlock < ApplicationRecord
  validates :text, :x1, :x2, :y1, :y2, :image_id, presence: true

  # *** View側ですでにラベル付けを行った箇所の座標を返す ***
  def self.view_css(image_id)
    texts = self.where(image_id: image_id)
    texts.map do |t|
      {
        x: t.x1,
        y: t.y1,
        width:  t.x2 - t.x1,
        height: t.y2 - t.y1
      }
    end
  end
end
```

こちらでは、「空入力を防止するためのバリデーション」と「View側にラベル付けした箇所の情報を返すためのメソッドの定義」を行っています。

## Controller

それぞれの機能毎にメソッドを分けております。
各機能はシンプルなため、それぞれに割り振ったコメントで大体は理解してもらえるかと思います。

```rb:app/controllers/image/register_controller.rb
class Image::RegisterController < ApplicationController

  # *** トップページ ***
  def index
    tmp = TmpImage.first
    @image = tmp[:filename]
    @blocks = TextBlock.view_css(tmp[:image_id])
  end


  # *** ラベル登録用のメソッド ***
  def register
    pos = params[:pos]
    text = TextBlock.create(
      image_id: TmpImage.first[:image_id],
      text: pos[:text],
      x1: pos[:pos1_x1],  y1: pos[:pos1_y1],
      x2: pos[:pos2_x2],  y2: pos[:pos2_y2]
    )
    redirect_to root_path
  end


  # *** 前の操作を取り消すメソッド ***
  def delete
    TextBlock.last.delete
    redirect_to root_path
  end


  # *** 次の画像に移動するメソッド ***
  def next
    # 現状の画像を完了に
    tmp = TmpImage.first
    image = Image.find_by(filename: tmp[:filename])
    image.update(is_complete: true)
    # 新しい画像に切り替える
    files = Dir::entries("app/assets/images/")
    files.each do |file|
      next unless %w(.jpg .png .jpeg).include?(File.extname(file))
      # ファイル名が既に書き込まれていないか確認
      db_files = Image.where(filename: file)
      if db_files.empty?
        image = Image.create(filename: file)
        TmpImage.first.update(filename: file, image_id: image.id)
        break
      end
    end
    # トップページへ遷移
    redirect_to root_path
  end


  # *** ダウンロードページ ***
  def download
    respond_to do |format|
      format.html
      format.csv do
        filename = 'recognition_result'
        headers['Content-Disposition'] = "attachment; filename=\"#{filename}.csv\""
      end
    end
  end


  # *** リセット（初期化）用のページ ***
  def reset
    # データベースの中身を削除する
    Image.delete_all
    TmpImage.delete_all
    TextBlock.delete_all
    # 最初の画像を指定する ※ サンプル画像以外の場合はこちらを書き換え
    image = Image.create(filename:"image_sample_01.png")
    TmpImage.create(filename:image[:filename], image_id: image.id)
    redirect_to root_path
  end

end
```

## View

### トップページ

トップページのViewはこのようにslimを使用して記載しています。
Form要素はRails側の `form_for` を使用し、クリックなどの動的な動作は JavaScript (jQuery) で実現しています。
※ slim用のシンタックスハイライトが用意されていないため、読みにくいことをお許し下さい。

```rb:app/views/image/register/index.html.slim
div.top-title
  div.left-button
    = link_to "前の登録を取り消し", image_delete_path, method: :delete, data: {confirm: "取り消しますか？"}, class: "btn btn-danger"
  div.right-button align="right"
    = link_to "次の画像へ", image_next_path, method: :post, class: "btn btn-warning"

div.float2
  div#main-image
    = image_tag @image, id:"image"
    div.child id="text-pos"
    - @blocks.each do |blk|
      div.child style="top: #{blk[:y]}px; left:#{blk[:x]}px; width:#{blk[:width]}px; height:#{blk[:height]}px;"
    
div.float2
  div.form
    = form_for(:pos, url: image_register_path, method: :post) do |f|
      div.form-group
        = f.label :pos1, "１．左上の座標"
      div.form-group
        div.form-inline
          = f.label :pos1, "x :", style: "margin-right:10px;"
          = f.number_field :pos1_x1, id: 'x1', class: "form-control", style: "width:100px; margin-right:30px;"
          = f.label :pos1, "y :", style: "margin-right:10px;"
          = f.number_field :pos1_y1, id: 'y1', class: "form-control", style: "width:100px; margin-right:30px;"
      div.form-group
        = f.label :pos2, "２．右下の座標"
      div.form-group
        div.form-inline
          = f.label :pos2, "x :", style: "margin-right:10px;"
          = f.number_field :pos2_x2, id: 'x2', class: "form-control", style: "width:100px; margin-right:30px;"
          = f.label :pos2, "y :", style: "margin-right:10px;"
          = f.number_field :pos2_y2, id: 'y2', class: "form-control", style: "width:100px; margin-right:30px;"
      div.form-group
        = f.label :text, "３．テキスト"
        = f.text_area :text, class:"form-control", style:"height: 200px;"
      div.form-group
        = f.submit "登録", class:"btn btn-primary"
  
    div
      = link_to "リセット", root_path

javascript:

  count = 1  // ボタンで使うカウンター

  document.getElementById( "main-image" ).addEventListener( "click", function( e ) {
  	// マウス位置を取得する
  	var mouseX = e.pageX ;	// X座標
  	var mouseY = e.pageY ;	// Y座標
    
    // 要素の位置を取得
    var element = document.getElementById( "main-image" ) ;
    var rect = element.getBoundingClientRect();
    
    // 要素の位置座標を計算
    var positionX = rect.left + window.pageXOffset ;	// 要素のX座標
    var positionY = rect.top  + window.pageYOffset ;	// 要素のY座標
    
    // 要素の左上からの距離を計算
    var X = mouseX - positionX;
    var Y = mouseY - positionY;
    
    // formの要素のvalueに代入
    $('#x'+count).val(X);
    $('#y'+count).val(Y);
    
    console.log([count, X, Y]);
    
    // count up
    count += 1
    
    if(count > 1) {
       var element = document.getElementById("text-pos"); 
       var x1 = $('#x1').val();
       var y1 = $('#y1').val();
       var x2 = $('#x2').val();
       var y2 = $('#y2').val();
       var width  = x2 - x1;
       var height = y2 - y1;
       element.style.width  = width  + 'px'; 
       element.style.height = height + 'px'; 
       element.style.top  = y1 + 'px'; 
       element.style.left = x1 + 'px'; 
    }
  } ) ;
```

```scss:app/assets/stylesheets/image/register.scss
div.top-title {
  padding: 30px;
}

div.float2{
  float: left;
  width: 50%;
  height: auto;
  padding: 30px;
}

div.left-button {
  float: left;
  width: 50%;
}

div.right-button {
  float: right;
  width: 50%;
}

#image {
  width: 500px;
  height: auto;
}

#main-image {
  position: relative;
}

.child {
  border: thin solid rgba(255, 0, 255, 0.5);
  background-color: rgba(255, 0, 255, 0.2);
  position: absolute;
}
```

### ダウンロードページ

まず、http://localhost:3000/image/download にアクセスした際に表示される View は以下のように記述しています。

```rb:app/views/image/register/download.html.slim
div.container
  h1 ダウンロードページ
  
  = link_to 'csv download', image_download_path(format: :csv), class: "btn btn-primary"
```

そして、リンクボタンを押下後に、CSVファイルをダウンロードするため、CSV用の View も別途準備しております。

```rb:app/views/image/register/download.csv.ruby
require 'csv'
CSV.generate do |csv|
  field_names = %w(id filename text x1 y1 x2 y2)
  csv << field_names
  TextBlock.all.each do |t|
    record = field_names.map do |field_name|
      case field_name
      when "filename"
        Image.find(t.image_id).filename
      else
        t[field_name]
      end
    end
    csv << record
  end
end.encode('CP932')
```

このように書くことで、CSVファイルとしてダウンロードできるようになります。
Railsは非常に簡単に実現できるので良いですね。


# おわりに

**「たかが前処理、されど前処理」**
機械学習では、そのアルゴリズムの実装に焦点が当たりがちですが、こういった前処理を効率的に行えるアプリケーションを持っていると非常に重宝します。
今回のアプリケーションは [GitHub](https://github.com/ryo813/tegaki_recog) にアップロードしているので、ご自身の状況にカスタマイズしてお使いください。

少しでも機械学習を勉強しようと励まれている方のお役に立てれば光栄です。
この記事が役に立った方は、記事に『**いいね**』をしていただけると幸いです。

また、機械学習を学ばれる際に、『**独学ではあの難解な数式やプログラミングがやっぱり難しい**』と感じた方は、ぜひ弊社のセミナーもしくはオンライン家庭教師でお待ちしております。


#### フォローお待ちしております

ビジネス目線の機械学習・人工知能の情報やオススメの参考書について発信しています。

代表取締役社長　吉崎 亮介
Twitter：[@yoshizaki_kkgk](https://twitter.com/yoshizaki_kkgk)
Facebook：[@ryosuke.yoshizaki](https://www.facebook.com/ryosuke.yoshizaki)
Blog：[キカガク代表のブログ](http://blog.kikagaku.co.jp/)


最後までお読みいただき、ありがとうございました。