AIプログラミング

画像キャプションの自動生成

キャプション生成

今回は,画像を入力して,その画像の内容を説明する文章を出力するディープラーニングモデルについて解説します.これまでとは違って,かなり長い解説になると思います.

文章を生成する場合,それまでの単語あるいは文章から,次の単語を出力することを繰り返し行う必要があります.そこで,文章生成などのタスクでは時系列データを取り扱うことの出来るRNN(リカレントニューラルネットワーク)が一般的に用いられます.
RNNは,それまでに計算された情報を保持し,次の計算に用いることができます.そして,次々と単語を出力して文章を生成した結果,累積されていった損失から,各出力過程の重みを調整します.このようにすることで,文章などの連続したデータの学習が可能になります.
今回は,RNNの一種である,LSTM(Long short-term memory)を用いてキャプション生成を行います.LSTMはRNNよりも複雑な構造ですが,Kerasで実装する場合は,これまでに行ったディープラーニングモデルと同様にモデルの途中にLSTMレイヤを挟み込むだけなので,あまり細かい中身を意識しなくても容易に実装することが可能です.

データセットの準備

キャプション生成用のデータセットとして,ここではMS-COCOを用います.MS-COCOには元々英語のキャプションが付いていますが,ありがたいことに日本語のキャプションも用意してくださった方々がいるので,今回は日本語キャプションで学習と生成を行いましょう.日本語のキャプションデータは,STAIR Lab / ステアラボ千葉工業大学人工知能・ソフトウェア技術研究センターが作成・公開しています.

このキャプションデータは,MS-COCOの16万枚の画像に各画像5文ずつ,計80万文が用意されています.ですが,今回は雰囲気を掴むために,バリデーション用のデータである『stair_captions_v1.2_val_tokenized.json』のさらに一部,2000枚(1万文)を用いて学習および評価,テストを行いましょう.

MS-COCOおよびSTAIR Labによるキャプションデータのダウンロードは,こちらのリンクから可能です.MS-COCOのデータは,『2014 Val images [41K/6GB]』を選んでください.(http://cocodataset.org/#download)(http://captions.stair.center/

また,このキャプションデータはJSON形式で記述されていることから,少々見づらく,さらに今回ここで用いるにあたっては余計なデータも多く含まれているので,まず最初により使いやすい形に成形したいと思います.そのためのコードを以下に示します.

最初に,必要なモジュールをインポートし,JSONファイルをインポートして読み込みます.日本語を扱う都合上,文字コードに関するエラーが出る可能性は往々にしてあるので,万一出てしまったら随時対応して下さい.

続いて,このJSONファイルにおいては画像のファイル名がIDに紐づけられていることから,画像のファイル名とIDを対応させたディクショナリを作っておきます.

続いて,総キャプション数に合わせたpandasのデータフレームを作り,データフレームに各画像とキャプションを対応させていきます.(どうせ2000枚しか使わないのに全画像を処理するのは無駄感がありますが,大した量ではないのでザザっとやっちゃってます)

最後に,データフレームの上から10000行をtxtファイルとして出力します.生成されたtxtファイルの,上から6行分が以下のようになっています.

COCO_val2014_000000000042.jpg “白い 靴 や 赤 や 黒 の 草履 が 乱雑 に 置か れ た 棚”
COCO_val2014_000000000042.jpg “玄関 の かご の 中 に 靴 と 一緒 に 紛れこん で いる トイ プードル”
COCO_val2014_000000000042.jpg “ケージ に 置か れ た 靴 に 犬 が 頭 を のせ て 寝 て いる”
COCO_val2014_000000000042.jpg “かご の 中 に 靴 が 入れ られ て いる”
COCO_val2014_000000000042.jpg “スニーカー や サンダル が 入っ た バスケット に 犬 が 寝 て いる”
COCO_val2014_000000000073.jpg “アスファルト の 上 に 停め て ある 黒い バイク”

このように,画像1つにキャプションが5つ,画像名[半角スペース]”説明文”の形で指定されていることが確認できます.ちなみに,COCO_val2014_000000000042.jpgの画像は以下のような画像です.(気づいたかもしれませんが,本記事のサムネイルのAIはこの正解データと同じ文をドンピシャで想像しているので,ちょっとやりすぎですね,こんな風に文を出力してくれるといいんですが.)

また,説明文を見ると分かると思いますが,単語ごとに文章が区切られています.これは,単語ごとに学習・出力を行う必要があるからです.英文ではそもそも各単語間にスペースがあるため心配ないのですが,日本語でキャプション生成を行う場合はこのような配慮(形態素解析)が必要です.なお,STAIR Labが用意したキャプションの形態素解析は,形態素解析を自動で行うツールであるMeCab(めかぶ)を用いて行われているとのことです.

そして,今回用いるデータセットについて,トレーニング用に8割(1600枚),バリデーション用に約2割(399枚),最後の1枚がテスト用,になるように事前に指定しておきます.また同時に,今回使う2万枚の画像も,ダウンロードしたCOCOデータセットが入っているフォルダ(val2014)から,新しい他のフォルダにコピーしておきます.以下にそのためのコードを示します.

最初に,必要なモジュールをインポートし,先ほど出力したtxtファイルを読み込みます.

続いて,txtファイルに書かれていた画像名の部分だけをリストに格納していきます.

そしてまずは,トレーニング画像とバリデーション画像をコピーして入れておくフォルダ「photo_data」を作成し,COCOデータセットのフォルダ「val2014」から,最後の1枚以外を「photo_data」にコピーします.適宜自分の環境に合わせてパスを設定してください.(参照ディレクトリ内に同じ名前のフォルダが既に存在するとエラーが出ます.)

同じ要領で,最後の1枚だけを新しく作ったフォルダ「test_data」にコピーします.

最後にデータをトレーニングデータとバリデーションデータに分けて,train.txt,val.txtの2つのファイルを新たに生成します.

なお,tarin.txtの上から3行分は,以下のようになっています.

COCO_val2014_000000000042.jpg
COCO_val2014_000000000073.jpg
COCO_val2014_000000000074.jpg

このように,使用する画像の名前が羅列してあります.

以上でデータセットの準備は完了です.以上のような形式でデータを用意さえすれば,今回解説するコードでキャプション生成が可能です.ですから,自前のデータをExcelに入力してtxt形式で出力・保存するなど,自分にあった形でデータセットを構築して構いません.

それでは,本題のキャプション生成モデルの作成について,以下に説明します.

サンプルコード

以下,サンプルコードです.

まず,全てのデータのパスを指定しておきます.今後,適宜呼び出します.

画像の特徴抽出

画像からキャプション生成を行うにあたっては,各画像の特徴ベクトルに対応してキャプションを学習・生成します.そのために,まず最初にCNNに画像を通して,最終層でクラス分類される手前で出力される特徴量を抽出します.そのためのコードが以下になります.

今回は特徴抽出用のCNNモデルとして,VGG16を用いています.VGG16の最終層を取り払い,画像をVGG16に入力できる形に成形し,モデルに通して抽出した特徴と画像名を紐づけて,全ての画像のデータをpklファイルとして保存します.今後,文章の学習・生成をする際には,これらの特徴量を初期入力とします.なお,このようにすると学習時にVGG16モデルそのものの学習は行われませんが,学習できるように設定してもあまり効果的でないことが知られています.

キャプションの読み込みと成形

まずはファイルの読み込みのための関数を定義し,全てのキャプションを読み込みます.この関数は今後も適宜使用します.

つづいてディクショナリを定義し,各画像とキャプションを紐付けています.

今回用いるキャプションの中には,文頭文末のダブルクォーテーションなど,不要な記号が含まれています.これらをここで除去します.string.punctuationで[!”#$%&'()*+,-./:;<=>?@[\]^_`{|}~]←これらをまとめて指定可能です.

語彙が縮小されたキャプションは,descriptions.txtとして新たに保存されます.

最後に,画像数のチェックを行っています.今回は2000枚あるので,2000と出力されると思います.

トレーニングデータとバリデーションデータの準備

ここでは,トレーニングデータとバリデーションデータを,いくつかの段階を踏まえて,学習・評価に使える形に成形します.

まず,上記の関数を使って,トレーニングデータとバリデーションデータの画像名が格納されたリストを作成しています.なお,引数に用いているtrain_dataとval_dataは,本サンプルコードの一番最初に指定したものです.

上記の関数の引数filenameには先ほど作成したファイルdescriptions.txtを指定し,引数datasetには先ほど作成したリストtrainおよびリストvalを指定します.この関数を使って,トレーニングデータとバリデーションデータの画像名とキャプションが紐付けられたディクショナリを作成しています.
ここで,各キャプションを何らかの開始語と終了語で挟みます(ここではstartseqおよびendseqとしています).文章生成の際には,画像と開始語を入力として与えて,そこから続く単語を次々と予測し,終了後が出力されたら文章生成を打ち止めます.

上記の関数の引数filenameには最初に作成したファイルfeatures.pklを指定し,引数datasetには先ほど作成したリストtrainおよびリストvalを指定します.この関数を使って,トレーニングデータとバリデーションデータの画像名と特徴量が紐付けられたディクショナリを作成しています.

機械学習において文字列を扱うためには、単語をベクトル表現する必要があります.単語をベクトル表現することで,各単語間の関係性を学習することが可能になります.KerasにはTokenizerという便利なものがあり,これを使うことで単語をベクトル配列に変換することができます.ここではまず最初にキャプションのリストを生成し,これをもとにベクトル化します.なお,キャプションのリストにはtrain_descriptionsを指定するので,トレーニングデータのキャプションに含まれている単語のみベクトル化されます.

次に,トレーニングデータの文章のうち,最大の文の長さを取得します.これは,モデルの入力層のノード数は一定にしなくてはいけないので,それをトレーニングデータの文の最大長に合わせるためです.

そして最後に,キャプションデータ,画像データを紐づけ,モデルに入力するためのデータセットとします.ここでは,画像とそれに対応する入力語,入力語から予測されるべき出力語を,そのすべてのパターンにおいて設定します.以上でモデルに入力できる形のデータの準備は完了です.

モデルの構築

さて,ここからやっとメインのモデル構築です.

今までのモデルは単一のレイヤが一列に積み重なっていただけでしたが,今回のモデルは以下のような構造になっています.

この構造を見て目につくように,このモデルは入力を2つとって1つの出力としています.
すなわち,input1から画像(の特徴量),input2から文章を入力し,outputで続く単語を出力します.また,時系列データを処理するLSTMレイヤは,input2からなる層に配置されていることが確認できます.

input1の入力ノード4096は,VGG16の出力した特徴量を受け取ることからです.また,input2の入力ノード34は,トレーニングデータにおける文章の最大長です.input2には,それまで生成された文章が入力として与えられるのですが,その文章を最大長に合わせてパディングして入力として与えます.outputの7579は,出力されうる単語数を示しています.当たり前ですが,学習していない語彙は出力することはできません.(なお,34や7579というのは一例で,今回のデータセットでは,また違った数値になっていると思います.)

では,定義したモデルを使って学習を行います.

今回はKerasのコールバックを定義しています.エポックごとにval_lossをモニターすることで,val_lossがそれまでの最小値だったらそのときの重みのモデルをファイルとして保存します.
テストする際や,新規に画像のキャプションを生成する際は,最後に出力されたモデルを読み込み実施するだけで良いです.データ量が多いので,実行には少し時間がかかると思います.上記のコードを実行すると,以下のような結果が出力されると思います.

Train on 108400 samples, validate on 25833 samples
Epoch 1/10
– 188s – loss: 4.0848 – val_loss: 3.4455

Epoch 00001: val_loss improved from inf to 3.44553, saving model to model-ep001-loss4.085-val_loss3.446.h5

~省略~

Epoch 00005: val_loss improved from 3.13871 to 3.13543, saving model to model-ep005-loss2.581-val_loss3.135.h5

~省略~

Epoch 10/10
– 184s – loss: 2.2064 – val_loss: 3.3320

Epoch 00010: val_loss did not improve from 3.13543

エポックのval_lossが最小の場合は新しくモデルを保存しているのが分かります.それでは,テストデータの1つを使って実際に文章を生成してみます.

まず,本サンプルコードで最初に実行した,全ての写真から特徴を抽出する関数を少し書き換え,個別の画像から特徴量を抽出する関数を定義します.この関数を使って,テスト画像の特徴量を抽出します.ここで,テスト画像のパスを指定してください.

続いて,整数を単語に戻す関数を定義します.モデルを学習する際に,各単語は番号に置き換えているので,実際に出力される際にも番号で出力されます.これを,この関数で対応する単語に戻すことができます.

その次に定義しているのが,実際に画像からキャプションを生成する関数です.
yhat = model.predict([photo,sequence], verbose=0)の部分を見ると,画像(の特徴量)とそれまでに生成された文章を入力していることが分かります.なお,一番最初に入力されるのは開始語(この場合はstartseq)です.

結果としては各単語の確率が出力されるのですが,yhat = argmax(yhat)で,最も確率が高い単語を選択します.そして,in_text += ‘ ‘ + wordの部分で,それまでの文章と新たに出力された文章を繋げます.これを,次のループで新たな入力として使います.これを終了語が出力されるまで繰り返します.すなわちここでは,写真と,それまでに生成された文章から,次に来る確率が最も高い単語を繰り返し出力して文章を生成しているということです.

最後に,今回学習によって得られたモデルを変数modelに定義します.上記のコードは一例で,実際には最後に出力されたモデルのパスを入力してください.そして,キャプション生成の関数を実行し,その結果を表示します.以下のような結果が表示されました.(シード値の固定などは行っていないので,人によって答えは少し違うかもしれません.)

startseq 男性 が スキー を し て ジャンプ し て いる endseq

ちなみにテスト画像はこちらでした.

残念ながらこれは間違った文章です.スキーじゃなくてサーフィンだったらそこそこ良かったのですが.とはいえ,日本語として間違っているレベルのおかしな文章ではないですし,モデルを調整するか,データ量を増やすことで,精度向上の期待が持てそうです.

追記

今回のコードは説明を簡単にするために,学習データを小分けにするミニバッチ学習を行っていないので,これよりさらにデータ量を増やすとメモリエラーが発生する可能性があります.(環境によっては,今回のデータ量でもメモリエラーが発生するかもしれません.)ミニバッチ学習の方法をこの記事に追加するとあまりに内容が膨大になるので,次回以降に説明したいと思います.

また,今回の出力では最も確率の高い単語を順次つなげていったので,文章が1文のみ出力されました.とはいえ,場合によっては2文,3文出力すれば,1文では言い表せなかった内容も出力できる可能性があります.こちらについても,次回以降に併せて説明したいと思います.

コード全文

以下に,今回のコード全文を示します.

データの準備

上のコードは,STAIR Labのデータセットを,扱いやすいように成形するコードです.

上のコードは,成形したキャプションデータをもとに,画像をフォルダ分けして,トレーニングデータとバリデーションデータの画像名を指定したファイルを出力するコードです.

モデル構築

上記のコードは,モデルの構築と学習のコードです.通常,自分の環境で行う場合は最初にパスを指定するだけで,その後生成されるファイルは簡単に参照できるのですが,GoogleColabを使用する場合は少々面倒だと思います.(以前までの記事内でGoogleColabの使い方は一通り説明しているので,GoogleColabを使う場合はそれを参考にしてください.)

テストデータでキャプション生成

上記のコードは,テストデータを使ってキャプションを生成するコードです.(今回使ったテストデータ以外にも,自前のほかの画像でもすぐにキャプション生成できるので,色々と試してみてください.)
Jupyter notebookなどを使っている場合,学習に引き続いてこのコードを入力すれば,モジュールのインポートは上記のものだけで大丈夫です.しかし,別々のpyファイルとして扱う場合は,モジュールを再度インポートするほか,tokenizerを別途ファイル保存して読み込みする必要があります.