読者です 読者をやめる 読者になる 読者になる

画像処理とか機械学習とか

画像処理や機械学習関連の事について気まぐれで書いていきます。歩行者検出関係が多いと思います。ハリネズミもたまに出現します。

Windowsでchainerを使って画像を説明する文章を自動生成するサンプルを実行する方法

qiita.com

今回はこちらの記事で紹介されている、畳み込みニューラルネットワーク+時系列ディープラーニングを用いる手法を紹介します。

上記記事のサンプルでは、画像とそれを説明する文章データを学習させ、入力画像を説明する文章を自動生成するネットワークを実装されたと言うことで、実際に動かす方法を日本語で説明を加えて紹介します。

・概要図(画像中で単語と文字と表記がブレてますが、同じものを指してます)
f:id:hiro2o2:20160701170213p:plain

実際にdsannoさんが実装されたプログラムが
GitHub - dsanno/chainer-image-captionこちらになります。
動作に必要なのが

  1. Chainerをインストール済みのPC
  2. 学習データ(ダウンロード
  3. Caffeの学習済みネットワーク(ダウンロード

の3つになります。このサンプルでは画像のベクトル化に畳み込みニューラルネットワークはCaffeの学習済みモデルを用い、文章を自動生成する部分に時系列ディープラーニングを用いて、時系列ディープラーニングの部分を学習させます。

  • 文章生成モデルの学習

まず、ダウンロードしたデータセットを解凍し、dataset.json と vgg_feats.matをサンプルのルートフォルダにコピーします。
サンプルのルートフォルダでCtrl+右クリックを押して、コマンドウィンドウをここで開くを選択し、コマンドウィンドウを開きます。

$ python src/convert_dataset.py dataset.json dataset.pkl

コマンドウィンドウで上記コマンドを実行し、データセットを変換します。

そして、下記コマンドで学習を行い、文章を生成するためのモデルを学習します。
GPUを使用するので-g 0 をつけています。

$ python src/train.py -g 0 -s dataset.pkl -i vgg_feats.mat -o model/caption_gen
  • ダウンロードしたCNNの準備

画像を扱うために、ダウンロードしたCaffeモデルをpklに変換するために以下のコマンドを実行します。

$ python src/convert_caffemodel_to_pkl.py VGG_ILSVRC_19_layers.caffemodel vgg19.pkl
  • 画像から文章を自動生成

以下のコマンドを実行すると、画像からキャプションを付けるサンプルが実行できます。

$ python src/generate_caption.py -s dataset.pkl -i vgg19.pkl -m model/caption_gen_0010.model -l image/list.txt



以下に、サンプルのtrain.pyにソースコード理解の為にコメントを加えた物を載せます。
python初心者なので、間違っている部分もあるかもしれませんが、参考までに。

# -*- coding: utf-8 -*-

import argparse
import cPickle as pickle
import json
import numpy as np
import scipy.io
import random

#chainerライブラリの読み込み
import chainer
from chainer import cuda, Variable, optimizers, serializers, functions as F
from chainer.functions.evaluation import accuracy

#net.pyファイルに定義したネットワークの読み込み
from net import ImageCaption

import time

#--------------------------------------------------------------------------#
#   引数の設定                                                             #
#--------------------------------------------------------------------------#
parser = argparse.ArgumentParser(description='Train image caption model')
parser.add_argument('--gpu', '-g', default=-1, type=int,
                    help='GPU ID (negative value indicates CPU)')
parser.add_argument('--sentence', '-s', required=True, type=str,
                    help='input sentences dataset file path')
parser.add_argument('--image', '-i', required=True, type=str,
                    help='input images file path')
parser.add_argument('--model', '-m', default=None, type=str,
                    help='input model and state file path without extension')
parser.add_argument('--output', '-o', required=True, type=str,
                    help='output model and state file path without extension')
parser.add_argument('--iter', default=100, type=int,
                    help='output model and state file path without extension')
args = parser.parse_args()

gpu_device = None
args = parser.parse_args()
xp = np

#gpuを使用する際
if args.gpu >= 0:
    cuda.check_cuda_available()
    gpu_device = args.gpu
    cuda.get_device(gpu_device).use()
    xp = cuda.cupy

#--------------------------------------------------------------------------#
#   データセットの設定                                                    #
#--------------------------------------------------------------------------#

#言語のデータセットを読み込む
with open(args.sentence, 'rb') as f:
    sentence_dataset = pickle.load(f)
    
#画像データセットをmatファイルから読み込む
image_dataset = scipy.io.loadmat(args.image)
images = image_dataset['feats'].transpose((1, 0))

#読み込んだデータセットへの名前をわかりやすく変更
train_image_ids = sentence_dataset['images']['train']
train_sentences = sentence_dataset['sentences']['train']
test_image_ids = sentence_dataset['images']['test']
test_sentences = sentence_dataset['sentences']['test']
word_ids = sentence_dataset['word_ids']
feature_num = images.shape[1]

#隠れ層の数
hidden_num = 512

#バッチサイズ
batch_size = 128

#読み込んだデータセットの単語数を表示
print 'word count: ', len(word_ids)

#画像にキャプションをつけるネットワークのインスタンス化(単語数, 画像の次元数, 隠れ層の数)
caption_net = ImageCaption(len(word_ids), feature_num, hidden_num)
if gpu_device is not None:
    caption_net.to_gpu(gpu_device)
#最適化手法の設定(ここではAdam)
optimizer = optimizers.Adam()
optimizer.setup(caption_net)

#引数で初期化にモデルが与えられていたら、モデルを読み込む
#(本プログラムでは、CNNにCaffeの学習済みモデルを使用しているため、必須)
if args.model is not None:
    serializers.load_hdf5(args.model + '.model', caption_net)
    serializers.load_hdf5(args.model + '.state', optimizer)

#文字コード関係
bos = word_ids['<S>']
eos = word_ids['</S>']
unknown = word_ids['<UNK>']

#--------------------------------------------------------------------------#
#   関数の定義                                                          #
#--------------------------------------------------------------------------#

    #----------------------------------------------------------------------#
    #   学習データの順番をランダムに入れ替える                             #
    #----------------------------------------------------------------------#
def random_batches(image_groups, sentence_groups):
    batches = []

    #引数で与えられた画像群と文章群の要素を代入しながら要素分繰り返す
    for image_ids, sentences in zip(image_groups, sentence_groups):
        #文章の長さ
        length = len(sentences)
        #indexは0からlengthまでのint32型の整数が入った配列
        index = np.arange(length, dtype=np.int32)
        #indexの配列をランダムにシャッフル
        np.random.shuffle(index)

        #0から値をbatch_sizeだけlengthまで増やしながらnに代入し、繰り返す
        for n in range(0, length, batch_size):
            #batch_indexは、シャッフル後のindex中のn~(n+batch_size-1)までのインデックスをコピーしたもの
            batch_index = index[n:n + batch_size]
            #batch_indexのimage_idsとsentencesをbatchesに追加
            batches.append((image_ids[batch_index], sentences[batch_index]))

    #batchesをさらにシャッフルする
    random.shuffle(batches)
    return batches


    #----------------------------------------------------------------------#
    #   image_idsとsentencesからグループを作成                             #
    #----------------------------------------------------------------------#
def make_groups(image_ids, sentences, train=True):
    #学習の場合
    if train:
        #境界を固定値で設定
        boundaries = [1, 6, 11, 16, 21, 31, 41, 51]
    else:
        #境界を1~40までの整数値で設定
        boundaries = range(1, 41)

    sentence_groups = []
    image_groups = []

    #zipは、複数オブジェクトを同時にループで回す際に使用する
    #boundariesのbeginが0番目~最後から-1番目の値、endが1番目~最後の値
    for begin, end in zip(boundaries[:-1], boundaries[1:]):

        #beginからendまでのリストに対してlambda関数を適用し、それをsumする
        #begin=1, end=6なら、[1,2...5,6]の各値(sentences[1]の長さ~sentences[6]の長さを足す)=size
        size = sum(map(lambda x: len(sentences[x]), range(begin, end)))

        #size×(end+1)の行列を作る 要素は文章の終わりの記号eosで初期化
        sub_sentences = np.full((size, end + 1), eos, dtype=np.int32)
        #文章の始めの記号bosを設定
        sub_sentences[:, 0] = bos

        #sub_image_idsにsize分のゼロ行列を作成
        sub_image_ids = np.zeros((size,), dtype=np.int32)
        offset = 0

        #現在注目しているbeginとendの配列の要素を一つずつnに代入し、要素数分ループ
        for n in range(begin, end):
            #sentence[n]の長さ
            length = len(sentences[n])
            #長さが0より大きい場合
            if length > 0:
                #sub_sentencesに今注目しているsentence[n]を代入
                sub_sentences[offset:offset + length, 1:n + 1] = sentences[n]
                #sub_image_idsに今注目しているimage_ids[n]を代入
                sub_image_ids[offset:offset + length] = image_ids[n]
            #代入した文字数分だけoffset(代入の開始位置)をずらす    
            offset += length

        #現在のsub_sentencesをグループとしてsentence_groupsに追加    
        sentence_groups.append(sub_sentences)
        #現在のsub_image_idsをグループとしてimage_groupsに追加
        image_groups.append(sub_image_ids)
        
    return image_groups, sentence_groups

    #----------------------------------------------------------------------#
    #   順伝播の操作                                                       #
    #----------------------------------------------------------------------#
def forward(net, image_batch, sentence_batch, train=True):
    #chainerで扱える型(Variable型)へ配列を変換
    images = Variable(xp.asarray(image_batch), volatile=not train)
    
    n, sentence_length = sentence_batch.shape

    #imagesでnetを初期化
    net.initialize(images, train=train)

    #変数を初期化
    loss = 0
    acc = 0
    size = 0

    #i=0~sentence_batchの長さだけ繰り返し
    for i in range(sentence_length - 1):
        #sentence_batch配列で要素[:, i]がeosでなければ1, eosなら0を返し,targetは1,0が入った配列になる
        target = xp.where(xp.asarray(sentence_batch[:, i]) != eos, 1, 0).astype(np.float32)
        #現在のターゲットが全てeosの場合
        if (target == 0).all():
            break

        #ターゲットに文章が入っている場合
        #入力(文章)
        x = Variable(xp.asarray(sentence_batch[:, i]), volatile=not train)
        #1時刻先の入力
        t = Variable(xp.asarray(sentence_batch[:, i + 1]), volatile=not train)
        #LSTMで予測した次の単語確率の配列
        y = net(x, train=train)
        #最も高い確率のインデックスを取り出す
        y_max_index = xp.argmax(y.data, axis=1)

        # mask = 縦がtargetの長さ分、横が1次元の配列を作成
        # これをy.dataの横の長さ分繰り返す 
        mask = target.reshape((len(target), 1)).repeat(y.data.shape[1], axis=1)

        #予測した単語確率の配列 × maskをVariable型に変換したもの
        y = y * Variable(mask, volatile=not train)

        #損失を計算(予測したy, 真の次の時刻の単語t)
        loss += F.softmax_cross_entropy(y, t)
        #精度を計算
        acc += xp.sum((y_max_index == t.data) * target)
        #現在のターゲットの総単語数
        size += xp.sum(target)
        
    return loss / size, float(acc) / size, float(size)

    #----------------------------------------------------------------------#
    #   学習                                                               #
    #----------------------------------------------------------------------#
def train(epoch_num):
    #画像と単語の学習データグループを作成
    image_groups, sentence_groups = make_groups(train_image_ids, train_sentences)
    #画像と単語のテストデータグループを作成
    test_image_groups, test_sentence_groups = make_groups(test_image_ids, test_sentences, train=False)

    #epoch_num分繰り返す
    for epoch in range(epoch_num):

        #学習データグループからランダムでバッチを作成
        batches = random_batches(image_groups, sentence_groups)

        #変数を初期化
        sum_loss = 0
        sum_acc = 0
        sum_size = 0

        #バッチのサイズを計算
        batch_num = len(batches)

        #enumerateでインデックス付きで要素を取得し、batchesの最後までループ
        for i, (image_id_batch, sentence_batch) in enumerate(batches):

            #順伝播させる
            loss, acc, size = forward(caption_net, images[image_id_batch], sentence_batch)

            #chainerのライブラリを使用して誤差逆伝播させ、最適化手法でパラメータを更新
            optimizer.zero_grads()
            loss.backward()
            loss.unchain_backward()
            optimizer.update()

            #sentenceの長さを取得
            sentence_length = sentence_batch.shape[1]
            sum_loss += float(loss.data) * size
            sum_acc += acc * size
            sum_size += size

            #500回繰り返す毎に学習状況を出力
            if (i + 1) % 500 == 0:
                print '{} / {} loss: {} accuracy: {}'.format(i + 1, batch_num, sum_loss / sum_size, sum_acc / sum_size)

        #バッチの最後まで学習が終わったら、現在のepochでの学習状況を出力        
        print 'epoch: {} done'.format(epoch + 1)
        print 'train loss: {} accuracy: {}'.format(sum_loss / sum_size, sum_acc / sum_size)
        sum_loss = 0
        sum_acc = 0
        sum_size = 0

        #テストのループ image_idsにテスト画像、sentencesに文章をテストグループから代入しながら繰り返す
        for image_ids, sentences in zip(test_image_groups, test_sentence_groups):

            if len(sentences) == 0:
                continue
            size = len(sentences)

            # i=0~文章の長さまで,バッチサイズ分増やしながら繰り返す
            for i in range(0, size, batch_size):
                image_id_batch = image_ids[i:i + batch_size]
                sentence_batch = sentences[i:i + batch_size]

                #順伝播させる
                loss, acc, size = forward(caption_net, images[image_id_batch], sentence_batch, train=False)
                sentence_length = sentence_batch.shape[1]

                #誤差と精度を計算
                sum_loss += float(loss.data) * size
                sum_acc += acc * size
                sum_size += size

        #現在のパラメータにおける、テストでの精度を出力        
        print 'test loss: {} accuracy: {}'.format(sum_loss / sum_size, sum_acc / sum_size)

        #現在のepochでの学習済みモデルを出力
        serializers.save_hdf5(args.output + '_{0:04d}.model'.format(epoch), caption_net)
        serializers.save_hdf5(args.output + '_{0:04d}.state'.format(epoch), optimizer)

#メイン
train(args.iter)



※参考文献:Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理