【Ruby on rails6】画像認識と自然言語処理の実装

【Ruby on rails6】画像認識と自然言語処理の実装

2024年2月12日

はじめに

今回はRuby on rails6にて画像認識で自動的にタグをつけてくれる処理と自然言語処理をすることにより、投稿がネガティブなものなのか、ポジティブなものなのかを判断するAIを実装していこうと思います。
今回実装していく機能では、Google Cloud Platformが提供している「Cloud Vision API」と「Cloud Natural Language API」の二つを使用していきます。

Cloud Vision APIとは

Cloud Vision API は、 Google が提供する画像認識サービスで、 Google 独自の機械学習モデルを採用しています。これによって効率的に画像を分析し、様々なことを実現することが可能になります。
例えば、

・画像に何が移っているかを判別する
・手書きの文字を読み取る
・画像から顔を検出する

といったようなことができるようになります。
Cloud Vision APIの機能をざっくりと以下の表にまとめます。

機能説明
LABEL_DETECTION画像全体に対して画像コンテンツ分析を実行し、結果を返します。
TEXT_DETECTIONtext画像内のテキストに対して光学式文字認識(ocr)を実行します(文字数制限あり)。
DOCUMENT_TEXT_DETECTION高密度テキスト画像に対して光学式文字認識(ocr)を実行します(プレミアム機能では文字数制限なし)。
FACE_DETECTION画像内の顔を検出します。
LANDMARK_DETECTION画像内の地理的ランドマークを検出します。
LOGO_DETECTION画像内の企業ロゴを検出します。
SAFE_SEARCH_DETECTION画像の画像セーフサーチ プロパティを判別します。
IMAGE_PROPERTIES画像の一連のプロパティ(画像のドミナント カラーなど)
を計算します。
図1. Cloud Vision API 機能表

Cloud Natural Language APIとは

Cloud Natural Language APIは、文章をAPIをリクエストとして、その内容をGoogleの機械学習アルゴリズムが分析してその文字列の感情を数値化したり、構造、意味を返してくれるというAPIとなっています。
これによってできることは、

・ニュース記事やブログ記事で言及されている人、場所、イベントなどの情報を抽出する
・文章がポジティブなものかネガティブなものかという感情認識を行う
・テキストがどんなカテゴリに属していそうかを分類する

といったことです。
Cloud Natural Language APIの機能をざっくりと以下の表にまとめます。

機能のタイプ説明
感情分析指定されたテキストを調べて、そのテキストの背景にある
感情的な考え方を分析します。
エンティティ分析指定されたテキストに既知のエンティティ
(著名人、ランドマークなどの固有名詞)が含まれていないかどうかを調べて、それらのエンティティに関する情報を返します。
構文解析指定されたテキストの内容を分析します。
エンティティ感情分析エンティティ分析と感情分析の両方を組み合わせたものであり、テキスト内でエンティティについて表現された感情(ポジティブかネガティブか)の特定を試みることが可能です。
図1. Cloud Natural Language API 機能表

Google Cloud Platformの設定

次に、Google Cloud Platform(以下GCP)の設定をしていきます。GCPのアカウント登録などはこちらをご参照ください。
https://cloud.google.com/free/docs/free-cloud-features?hl=ja

①新しいプロジェクトの作成

まず、画像の赤丸の位置をクリックします。

次に新しいプロジェクトというところを選択し、
プロジェクト名は自分の好きなようにつけ、場所は特に目的がないのであれば、組織なしで作成します。

そしてしばらく待つとプロジェクトの作成が完了します。

②APIキーの発行

先ほどと同じく赤丸の位置をクリックすると自分で作成したプロジェクトが表示されると思うので、自分で作成したプロジェクトを選択しましょう。

左上の三本線を押し、メニューから「APIとサービス」→「認証情報」を選択しましょう。

次に、上の「認証情報を作成」から「APIキー」を選択するとAPIキーが作成されます。このAPIキーは非常に重要なものなので、必ずどこかにメモで控えておきましょう。そしてほかの人に公開するというようなことはしないようにしてください。

③APIの有効化

次に、今回使用するAPIの有効化をするので、APIとサービスのライブラリを選択します。

ライブラリのページを開くことができたら、検索窓にvisionと入力し、検索します。
すると「Cloud Vision API」が検索結果に出てくると思うので選択してください。

選択すると画像のようなページに飛ぶと思うので、有効にするを押します。

このようなページに飛んでいれば成功しています。
また、「有効なAPIとサービス」から

このように「Cloud Vision API」が適用されていることが確認できます。
同じように「Cloud Natural Language API」も適用してみましょう。検索窓にはnaturalなどと入れると出てくると思います。すると以下のようになりました。

これで2つ有効になっていることが確認できたので、GCPでの準備は終わりです。

Cloud Vision APIの実装

①APIキーの管理

まずは先ほど作成したプロジェクトのapiキーを管理する方法を記述していきます。

Gemfileに以下の記述をし、bundle installをします。

gem 'dotenv-rails'

dotenv-railsは環境変数を管理することができるgemです。
自身で作成したアプリケーションの直下に.envファイルを作成することで、パスワードなどネット上に公開させたくない情報を扱い、自動で読み込むことが可能になります。

ではさっそくアプリケーション直下に.envファイルを作成していきます。

.envファイルの中には以下の記述をします。APIキーは先ほど作成した自分のものを使用してください。

GOOGLE_API_KEY="API Keyの値をこちらに記載"

そしてgitで管理している人が忘れてはいけないことが、.envファイルをgitで管理しないように変更することです。APIキーというものは大抵の場合、使いすぎると課金が発生したり、いろいろな人が利用すると提供している側からapiの使用を差し押さえられてしまいます。なので、githubにapiの情報をpushしてしまうと、誰かに悪用されてしまうかもしれません。

そこで登場するのが、.gitignoreというファイルです。このファイルの中に書いてあるファイルはgitの管理下に置かれないというものです。railsでは標準で.gitignoreファイルを作成してくれるので、探して開いてみましょう。.gitignoreにはすでにいろいろ書かれていると思いますが、任意の場所に新しく行を作って以下の記述をしてください。

/.env

これで.envをgit の管理下から外すことができました

②ライブラリの作成

ここではCloud Vision APIを呼ぶためのライブラリを作成していきます。自分で作成するライブラリはlibファイルの下に置きます。libファイルの中にvision.rbを作成します。
vision.rbの中には以下を記述してください。

require 'base64'
require 'json'
require 'net/https'

module Vision
  class << self
    def get_image_data(image_file)
      # APIのURL作成
      api_url = "https://vision.googleapis.com/v1/images:annotate?key=#{ENV['GOOGLE_API_KEY']}"

      # 画像をbase64にエンコード
      base64_image = Base64.encode64(image_file.tempfile.read)

      # APIリクエスト用のJSONパラメータ
      params = {
        requests: [{
          image: {
            content: base64_image
          },
          features: [
            {
              type: 'LABEL_DETECTION',
              maxResults: 3
            }
          ]
        }]
      }.to_json

      # Google Cloud Vision APIにリクエスト
      uri = URI.parse(api_url)
      https = Net::HTTP.new(uri.host, uri.port)
      https.use_ssl = true
      request = Net::HTTP::Post.new(uri.request_uri)
      request['Content-Type'] = 'application/json'
      response = https.request(request, params)
      response_body = JSON.parse(response.body)

      # APIレスポンスをログに出力
      Rails.logger.debug "API response: #{response_body}"

      # APIレスポンス出力
      if (error = response_body['responses'][0]['error']).present?
        raise error['message']
      else
        response_body['responses'][0]['labelAnnotations'].pluck('description').take(3)
      end
    end
  end
end

はい。なんだかよくわかりませんね。部分ごとに分けて解説していきます。

module Vision
  class << self
    def get_image_data(image_file)

今回はVisionというmoduleのなかにget_image_data(image_file)というクラスを定義しています。

# APIのURL作成
api_url = "https://vision.googleapis.com/v1/images:annotate?key=#{ENV['GOOGLE_API_KEY']}"

# 画像をbase64にエンコード
base64_image = Base64.encode64(image_file.tempfile.read)

api_urlはapiを呼び出すためのurlです。そして、Cloud Vision APIではbase64という形に画像を変換する必要があるので、base64_imageとして変換しています。

# APIリクエスト用のJSONパラメータ
      params = {
        requests: [{
          image: {
            content: base64_image
          },
          features: [
            {
              type: 'LABEL_DETECTION',
              maxResults: 3
            }
          ]
        }]
      }.to_json

ここではjsonでリクエストを書いています。LABEL_DETECTIONはこちらで説明した機能の部分にあたります。今回は画像コンテンツ分析の機能を使用します。maxResultsは結果をいくつ返すかというものです。今回は3に設定してあります。

# Google Cloud Vision APIにリクエスト
      uri = URI.parse(api_url)
      https = Net::HTTP.new(uri.host, uri.port)
      https.use_ssl = true
      request = Net::HTTP::Post.new(uri.request_uri)
      request['Content-Type'] = 'application/json'
      response = https.request(request, params)
      response_body = JSON.parse(response.body)

      # APIレスポンスをログに出力
      Rails.logger.debug "API response: #{response_body}"

      # APIレスポンス出力
      if (error = response_body['responses'][0]['error']).present?
        raise error['message']
      else
        response_body['responses'][0]['labelAnnotations'].pluck('description').take(3)
      end

Cloud Vision APIにリクエストを送り、成功したときはapiでの分析の結果が返ってくるようにしています。この際にデバッグできるようにRails.loggerのデバッグ用の記述を書いています。

自作したライブラリはこのままでは動いてくれないので、config/application.rbに読み込むための記述をします。以下の一文を追加しておきましょう。

config.paths.add 'lib', eager_load: true

③モデルファイルの作成&ビューファイルの修正

ここでようやくいつものrailsのようなコードを書いていきます。長かった…

今回は本の投稿に画像を添付し、その画像コンテンツを分析する仕様にします。その結果をタグとして付けたいので、そのタグを保存するためのmodelを作っていきます。

ターミナル↓

rails g model tag name:string book_id:integer
rails db:migrate

models/tag.rb

class Tag < ApplicationRecord
  belongs_to :book
end

models/book.rb

has_many :tags, dependent: :destroy #追加

モデルの作成は以上です。次に、ビューファイルを少し修正していきます。

booksのindexページは部分テンプレートで作っているので、以下のように修正します。

<table class='table table-hover table-inverse'>
  <thead>
    <tr>
      <th></th>
      <th>Title</th>
      <th>Opinion</th>
      <th>Tag</th>
      <th colspan="3"></th>
    </tr>
  </thead>
  <tbody>
    <% books.each do |book| %>
      <tr>
        <td><%= link_to(book.user) do %>
          <%= image_tag book.get_image, size:'50x50' %>
          <% end %>
        </td>
        <td><%= link_to book.title,book %></td>
        <td><%= book.body %></td>
        #ここから
        <td>
          <% book.tags.each do |tag| %>
            <span class="label"><%= tag.name %></span>
          <% end %>
        </td>
       #ここまで
        <td id="favorite_buttons_<%= book.id %>">
          <%= render "favorites/favorite", book: book %>
        </td>
        <td><%= "コメント数: #{book.book_comments.count}" %></td>
      </tr>
    <% end %>
  </tbody>
</table>

これに合わせてshowファイルを以下のように変更していきます。

<div class='container'>
  <div class='row'>
    <div class='col-md-3'>
      <h2>User info</h2>
      <%= render 'users/info', user: @user %>
      <h2 class="mt-3">New book</h2>
      <%= render 'form', book: @new_book %>
    </div>
    <div class='col-md-8 offset-md-1'>
      <h2>Book detail</h2>
      <table class='table'>
        <tr>
          <td><%= link_to(@book.user) do %>
            <%= image_tag @book.get_image, size:"100x100" %><br>
            <%= @book.user.name %>
          <% end %>
          </td>
          <td><%= link_to @book.title, @book %></td>
          <td><%= @book.body %></td>
          #ここから
          <td>
            <% @book.tags.each do |tag| %>
              <span class="label"><%= tag.name %></span>
            <% end %>
          <td>
          #ここまで
          <% if @book.user == current_user %>
            <td><%= link_to 'Edit', edit_book_path(@book), class: "btn btn-sm btn-success" %></td>
            <td><%= link_to 'Destroy', @book, method: :delete, data: { confirm: '本当に消しますか?' }, class: "btn btn-sm btn-danger"%></td>
          <% end %>
          <td id="favorite_buttons_<%= @book.id %>">
            <%= render "favorites/favorite", book: @book %>
          </td>
          <td><%= "コメント数: #{@book.book_comments.count}" %></td>
        </tr>
      </table>

      <div id="comments_area">
        <%= render "book_comments/comments", book: @book %>
      </div>

      <div class="new-comment">
        <%= render "book_comments/form", book: @book, book_comment: @book_comment %>
      </div>
    </div>
  </div>
</div>

ついでにタグのcssも適当に作っていきます。

app/assets/stylesheets/application.css↓

.label {
  background-color: #337ab7;
  font-weight: 500;
  display: inline;
  padding: .2em .6em .3em;
  font-size: 75%;
  font-weight: bold;
  line-height: 1;
  color: #fff;
  text-align: center;
  white-space: nowrap;
  vertical-align: baseline;
  border-radius: .25em;
}

はい。これで下準備が整いました。

④コントローラの修正

createの部分を以下のように変更します。

def create
  @book = Book.new(book_params)
  @book.user_id = current_user.id
  if @book.image.attached?
    tags = Vision.get_image_data(book_params[:image])
  else
    tags = ["none"]
  end
  if @book.save
    tags.each do |tag|
      @book.tags.create(name: tag)
    end
    redirect_to book_path(@book), notice: "You have created book successfully."
  else
    @books = Book.all
    render 'index'
  end
end

なんかよくわからない記述が出てきましたね。

if @book.image.attached?
  tags = Vision.get_image_data(book_params[:image])
else
  tags = ["none"]
end

この部分が今回の大きな変更箇所です。画像を投稿するフォームで画像があるときには先ほど作成したVisionのライブラリを呼び出し、apiに分析させるという記述です。そして、画像が添付されなかった場合には自動的にnoneのタグをつけるようにしました。この状態でフォームを送ると以下のようになります。

フォームに入力↓

投稿後↓

画像を分析した結果がタグについています。これで成功です。

番外編 デバッグ

ここではデバッグをする方法を簡単に説明していきます。
railsでは開発環境での実行ログをlog/development.logにて管理しています。サーバーを立ち上げたままこのファイルを監視していきます。
サーバーを閉じずに別のターミナルを開き、以下のコマンドを打ち込みます。

tail -f log/development.log | grep response

これでリアルタイムでrailsの動きを監視することができます。tail -f log/development.logだけでもいいですが、これだとrails上で動いているすべてのリクエストが返ってきてしまうため、apiのレスポンスを見たいときには適していません。なので、grepで検索しながら見てあげるととても見やすくなります。このコマンドを実行して、もう一度投稿してみましょう。結果が以下になります。

今回タグとして使用している場所はdescriptionと書いてある場所になります。

Cloud Natural Language APIの実装

①ライブラリの作成

先ほどapiキーの設定などをやったので、今回はライブラリの作成からになります。もしまだやっていない方はこちらをご確認ください。

先ほどと同じようにlibファイルの下にライブラリを作成していきます。今回はlanguage.rbというファイルで作成しました。

require 'base64'
require 'json'
require 'net/https'

module Language
  class << self
    def get_data(text)
      # APIのURL作成
      api_url = "https://language.googleapis.com/v1/documents:analyzeSentiment?key=#{ENV['GOOGLE_API_KEY']}"
      # APIリクエスト用のJSONパラメータ
      params = {
        document: {
          type: 'PLAIN_TEXT',
          content: text
        }
      }.to_json
      # Google Cloud Natural Language APIにリクエスト
      uri = URI.parse(api_url)
      https = Net::HTTP.new(uri.host, uri.port)
      https.use_ssl = true
      request = Net::HTTP::Post.new(uri.request_uri)
      request['Content-Type'] = 'application/json'
      response = https.request(request, params)
      # APIレスポンス出力
      response_body = JSON.parse(response.body)

      # APIレスポンスをログに出力
      Rails.logger.debug "API response: #{response_body}"
      
      if (error = response_body['error']).present?
        raise error['message']
      else
        response_body['documentSentiment']['score']
      end  
    end
  end
end

ここのライブラリも先ほどまでとほとんど同じですが、type: ‘PLAIN_TEXT’を指定しています。ここはHTMLまたはPLAIN_TEXTを指定できますが、今回はテキストを入力するので、PLAIN_TEXTにしています。そして受け取る値は[‘documentSentiment’][‘score’]というもので、これは送られた文章がポジティブかネガティブかを判断してくれます。

②モデルファイルの作成&ビューファイルの編集

まずはbookのmodelにscoreのカラムを追加していきます。

ターミナル↓

$ rails g migration AddScoreToBooks 'score:decimal{5,3}'
$ rails db:migrate

今回の感情分析のスコアは-1~1の範囲で送られてくるので、小数点を扱えるdecimal形で作成します。
{5,3}はprecision: 5, scale: 3という意味になっています。
これは簡単に言うと全体で5桁、小数点以下3桁という意味になります。

次にビューファイルを作成していきます。

_index.html.erb↓

<td><%= book.score %></td> # 追加

show.html.erb

<td><%= @book.score %></td> #追加

これで下準備が整いました。

③コントローラの編集

@book.score = Language.get_data(book_params[:body])

はい。この一行をcreateの中に追加するだけです。今回はbodyが空で投稿された場合はバリデーションではじかれるので、if文で処理しなくてもエラーで弾かれません。
この一行によってscoreカラムにはフォームのbodyの内容がネガティブなものかポジティブなものかapiが判断した数値が格納されます。

これで完成なので、どのような動きになるか見てみましょう。

フォームに入力↓

投稿結果↓

このようにポジティブなことを書くと+のスコアが表示され、ネガティブなことを書くと-のスコアが表示されます。
試しにデバッグしてみてみましょう。

しっかりと値を受け取れていますね。表示されている値もdocumentSentimentのscoreなので、正常です。

おわり

ここまでお付き合いくださり、ありがとうございました。rails6にaiの機能を入れようと今回のことを始めてみたのはいいものの、いろいろなところでエラーが出てきて想像以上に時間がかかってしまいました。GCPにはまだたくさんのAPIが用意されているので、いろいろ試してみてください!調子に乗って使いまくると課金されてしまうので、ご注意を…
この下に、今回作成したアプリのリポジトリを置いておきます。当記事を読んでいただきありがとうございました。

Contribute to 0611NASKA/Bookers2-blog development by creating an account on GitH…
github.com