kerasで二つのモデルの出力を結合させる

深層学習のモデルを設計するとき、複数のモデルの出力を結合させたくなることがあるんじゃないかと思います。適当な例ですが、たとえば車の画像と価格を入力して、その車が売れる個数を予測するとか。

そこで今回はkerasを使って、複数のモデルを結合してみます。

学習はcifar10の画像データを使用します。画像は畳み込みニューラルネットワークと通常のニューラルネットワークにそれぞれ別に入力します。そして、その二つのモデルの予測結果を結合して別のニューラルネットワークに入力して、最終的な出力とします。

ソースコード全体

from keras.models import Model 
from keras.layers import Conv2D, MaxPooling2D, Dropout, Flatten, Dense, concatenate, Input
from keras.utils import to_categorical, plot_model
from keras.datasets import cifar10

(x_train, y_train), (x_test, y_test) = cifar10.load_data()

# 入力を正規化
x_train = x_train.astype("float32")
x_test = x_test.astype("float32")
x_train /= 255.0
x_test /= 255.0

# 教師データをone-hot配列に変換
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

# 畳み込みモデルと全結合モデルへの入力をそれぞれ設定
conv_input = Input(shape=(32, 32, 3), dtype="float32", name="conv_input")
dense_input = Input(shape=(32, 32, 3), dtype="float32", name="dense_input")

# 二つめの()の中が層への入力
# 畳み込みモデルの設定
conv_output = Conv2D(32, (3, 3), padding="same", activation="relu")(conv_input)
conv_output = MaxPooling2D()(conv_output)
conv_output = Conv2D(64, (3, 3), padding="same", activation="relu")(conv_output)
conv_output = MaxPooling2D()(conv_output)
conv_output = Flatten()(conv_output)
conv_output = Dense(10, activation="softmax")(conv_output)

# 全結合モデルの設定
# Denseは行列を入力にできないのでベクトルへ変換
dense_output = Flatten()(dense_input)
dense_output = Dense(256, activation="relu")(dense_output)
dense_output = Dense(256, activation="relu")(dense_output)
dense_output = Dense(10, activation="softmax")(dense_output)

# 二つのモデルの出力を結合する
model_output = concatenate([conv_output, dense_output])

# 残りの出力層までを設定
model_output = Dense(512, activation="relu")(model_output)
model_output = Dense(10, activation="softmax")(model_output)

# モデルを作成
# 3つの出力から損失を計算する
model = Model(inputs=[conv_input, dense_input], outputs=[model_output, conv_output, dense_output])

# loss_weightsで、どの出力の損失値をどの程度重視するか設定できる
# 今回は最終的な出力を一番重視する
model.compile(loss="categorical_crossentropy", optimizer="adam", loss_weights=[1.0, 0.5, 0.3], metrics=["accuracy"])
"""
lossやloss_weightsは
loss = {"main_output":"categorical_crossentropy", "conv_output":"categorical_crossentropy", "dense_output":"mean_squared_error"}
loss_weights = {"main_output":1.0, "conv_outp
"""

# モデルの入力と出力の指定方法が通常と異なる
# 今回はサンプルなので、学習データ数は100枚にしている
model.fit(x=[x_train, x_train], y=[y_train, y_train, y_train], epochs=3, batch_size=128)
"""
fitも同様に
inputs={"conv_input":x_train, "dense_input":x_train}
outputs={"main_output":y_train, "conv_output":y_train, "dense_input":y_train}
とすることができる
"""

# モデルの評価
score = model.evaluate(x=[x_test, x_test], y=[y_test, y_test, y_test], batch_size=128)
print("Loss:", score[0])
print("Acc:", score[1])

学習データの読み込み

画像配列の値は0~1の間に正規化します。
教師ラベルはデフォルトでは整数値になっているので、one-hot配列に変更しておきます。

(x_train, y_train), (x_test, y_test) = cifar10.load_data()

# 入力を正規化
x_train = x_train.astype("float32")
x_test = x_test.astype("float32")
x_train /= 255.0
x_test /= 255.0

# 教師データをone-hot配列に変換
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

モデルへの入力の設定

今回はInput層を使って入力を設定します。
この層はいろいろと便利な機能を持っているらしいので、私も勉強中です。

# 畳み込みモデルと全結合モデルへの入力をそれぞれ設定
conv_input = Input(shape=(32, 32, 3), dtype="float32", name="conv_input")
dense_input = Input(shape=(32, 32, 3), dtype="float32", name="dense_input")

畳み込みニューラルネットワークの構築

畳み込みの方のモデルを構築します。
kerasのFunction APIを使っているので、よく見るkerasのコードとは雰囲気が違いますね。

# 二つめの()の中が層への入力
# 畳み込みモデルの設定
conv_output = Conv2D(32, (3, 3), padding="same", activation="relu")(conv_input)
conv_output = MaxPooling2D()(conv_output)
conv_output = Conv2D(64, (3, 3), padding="same", activation="relu")(conv_output)
conv_output = MaxPooling2D()(conv_output)
conv_output = Flatten()(conv_output)
conv_output = Dense(10, activation="softmax")(conv_output)

通常のニューラルネットワークの構築

もう一つのモデルも構築します。
特に注意するところはありませんね。

# 全結合モデルの設定
# Denseは行列を入力にできないのでベクトルへ変換
dense_output = Flatten()(dense_input)
dense_output = Dense(256, activation="relu")(dense_output)
dense_output = Dense(256, activation="relu")(dense_output)
dense_output = Dense(10, activation="softmax")(dense_output)

二つのモデルを結合して出力

concatenate層でベクトル同士を結合できます。
単純にくっつけるだけじゃなくて、足したり、平均とったり、いろんな結合方法があるみたいです。

# 二つのモデルの出力を結合する
model_output = concatenate([conv_output, dense_output])

# 残りの出力層までを設定
model_output = Dense(512, activation="relu")(model_output)
model_output = Dense(10, activation="softmax")(model_output)

モデルの生成、学習まで

モデルのinputsなどに入力データのリストを渡しているのが特徴です。
辞書形式で設定もできますが、長くなるのでここは好みでしょう。
出力に関しても同様です。

# モデルを作成
# 3つの出力から損失を計算する
model = Model(inputs=[conv_input, dense_input], outputs=[model_output, conv_output, dense_output])

# loss_weightsで、どの出力の損失値をどの程度重視するか設定できる
# 今回は最終的な出力を一番重視する
model.compile(loss="categorical_crossentropy", optimizer="adam", loss_weights=[1.0, 0.5, 0.3], metrics=["accuracy"])
"""
lossやloss_weightsは
loss = {"main_output":"categorical_crossentropy", "conv_output":"categorical_crossentropy", "dense_output":"mean_squared_error"}
loss_weights = {"main_output":1.0, "conv_outp
"""

# モデルの入力と出力の指定方法が通常と異なる
# 今回はサンプルなので、学習データ数は100枚にしている
model.fit(x=[x_train, x_train], y=[y_train, y_train, y_train], epochs=3, batch_size=128)
"""
fitも同様に
inputs={"conv_input":x_train, "dense_input":x_train}
outputs={"main_output":y_train, "conv_output":y_train, "dense_input":y_train}
とすることができる
"""

おわりに

ここから先のモデルの評価に関しては、特筆することが無いので省略します。
今回は別にモデルを分ける必要が無い所を強引に分けているので、精度にはむしろ悪影響なんじゃないかと思います。
しかし、複数の要素を入力すると思わぬ成果につながることがあるので、覚えておくと便利なんじゃないでしょうか。
あと、層が深いモデルを学習させるときは、モデルの出力を途中で取り出して部分ごとに学習をすることもあるようです(GoogLeNetとか)。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする

コメント

  1. Takafumi Katayama より:

    初めまして、ブログ参考にさせていただきました。
    kerasの複数入力に関してなのですが、異なる入力ファイル(画像情報)を指定したいと考えています。この場合Input関数で解決可能でしょうか?

    例)
    Input directory (train)
    |-Dir A : Dog1
    |-Dir B : Dog2
    Answer (output) Dog

    Input directory (test)
    |-Dir A : Dog1
    |-Dir B : Dog2
    Answer (output) Dog

    もしご存じであればご教授いただきたいです。
    よろしくお願いいたします。

    • ボンド より:

      Modelインスタンスを生成する際に、引数にinputsを渡していると思います。
      例えば入力が二種類なら、
      Model(inputs=[input1, input2], outputs=…)
      で設定できます。

      あとはデータの渡し方ですが、DirA, DirBの画像データがそれぞれ、Dog1, Dog1, Dog2, Dog2という名前のnumpy配列になっているとしたら
      model.fit(x=[Dog1, Dog2], y=…)
      とすれば渡せるはずです。

  2. オオカミさん より:

    ボンドさん
    素早いご返答ありがとうございます!
    numpy配列にするため?に下記のように
    ImageDataGeneratorを使用しております。
    これを利用することでDirからの画像とラベルを
    関連づけすることができると考えていますが、
    こちらと、Model, model.fitとの関係性がいまいち理解できていません。
    もしご存じであれば教えていただきたいです。

    train_datagen = ImageDataGenerator(
    rescale=1.0 / 255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

    test_datagen = ImageDataGenerator(rescale=1.0 / 255)

    train_generator = train_datagen.flow_from_directory(
    directory=’train_images’,
    target_size=(img_rows, img_cols),
    color_mode=’rgb’,
    classes=classes,
    class_mode=’categorical’,
    batch_size=batch_size,
    shuffle=True)

    test_generator = test_datagen.flow_from_directory(
    directory=’test_images’,
    target_size=(img_rows, img_cols),
    color_mode=’rgb’,
    classes=classes,
    class_mode=’categorical’,
    batch_size=batch_size,
    shuffle=True)

    • ボンド より:

      ImageDataGeneratorは、画像を加工して無限に学習データを吐き出してくれるインスタンスです。
      学習データの量に不安があるときに使います。
      http://aidiary.hatenablog.com/entry/20161212/1481549365

      これを使うときはfit関数ではなく、fit_generator関数を使用します。
      公式のドキュメントに例が載っています。
      https://keras.io/ja/models/sequential/
      また、この記事も参考になるかもしれません。

      イメージでは、ImageDataGeneratorをinputs兼outputsとして渡すような感じではないでしょうか。

      • オオカミさん より:

        ボンドさん
        色々勉強不足で申し訳ございません。
        参考にさせていただきます。
        もう少し勉強してから、ご連絡させていただきます。
        ありがとうございました。

      • オオカミさん より:

        ボンドさん
        色々調べてみて簡単そうな方法を調べたのですが、
        下記で行き詰っております。ネットワークの構築はできているのですが、
        tupleのErrorが出るので値の指定方法が良くないと考えています。
        すいませんが、もし簡単な修正で済みそうであれば、
        ご教授お願い致します。

        参考URL
        https://stackoverflow.com/questions/41823555/multi-scale-cnn-network-python-keras
        https://github.com/fchollet/keras/issues/3386

        主要部分==========================================
        # CNN Structure
        # First branch
        F_model = Sequential()
        F_model.add(Convolution2D(4, 3, 3, input_shape=(img_rows, img_cols, channels)))
        F_model.add(Activation(‘relu’))
        F_model.add(MaxPooling2D(pool_size=(2, 2)))
        F_model.add(Dropout(0.5))
        F_model.add(Flatten())
        F_model.add(Dense(64))

        # Second branch
        S_model = Sequential()
        S_model.add(Convolution2D(4, 3, 3, input_shape=(img_rows, img_cols, channels)))
        S_model.add(Activation(‘relu’))
        S_model.add(MaxPooling2D(pool_size=(2, 2)))
        S_model.add(Dropout(0.5))
        S_model.add(Flatten())
        S_model.add(Dense(64))

        merged_model = Merge([F_model, S_model], mode=’concat’)

        # Final branch
        model = Sequential()
        model.add(merged_model)
        model.add(Dropout(0.5))
        model.add(Dense(nb_classes))
        model.add(Activation(‘softmax’))

        model.compile(loss=’categorical_crossentropy’,
        optimizer=’adam’,
        metrics=[‘accuracy’])

        # モデルのサマリを表示
        model.summary()
        plot_model(model, show_shapes=True, to_file=os.path.join(result_dir, ‘model.png’))

        # ディレクトリの画像を使ったジェネレータ
        # train_datagen = ImageDataGenerator(
        # rescale=1.0 / 255,
        # shear_range=0.2,
        # zoom_range=0.2,
        # horizontal_flip=True)

        # test_datagen = ImageDataGenerator(rescale=1.0 / 255)

        # train_generator = train_datagen.flow_from_directory(
        # directory=’train_images’,
        # target_size=(img_rows, img_cols),
        # color_mode=’rgb’,
        # classes=classes,
        # class_mode=’categorical’,
        # batch_size=batch_size,
        # shuffle=True)

        # test_generator = test_datagen.flow_from_directory(
        # directory=’test_images’,
        # target_size=(img_rows, img_cols),
        # color_mode=’rgb’,
        # classes=classes,
        # class_mode=’categorical’,
        # batch_size=batch_size,
        # shuffle=True)

        generator = ImageDataGenerator(rotation_range=0,
        width_shift_range=0,
        height_shift_range=0,
        zoom_range=0)

        genX1 = generator.flow_from_directory(
        directory=’train_images’,
        target_size=(img_rows, img_cols),
        color_mode=’rgb’,
        classes=classes,
        class_mode=’categorical’,
        batch_size=batch_size,
        shuffle=False)

        genX2 = generator.flow_from_directory(
        directory=’train_images’,
        target_size=(img_rows, img_cols),
        color_mode=’rgb’,
        classes=classes,
        class_mode=’categorical’,
        batch_size=batch_size,
        shuffle=False)

        genY1 = generator.flow_from_directory(
        directory=’test_images’,
        target_size=(img_rows, img_cols),
        color_mode=’rgb’,
        classes=classes,
        class_mode=’categorical’,
        batch_size=batch_size,
        shuffle=False)

        genY2 = generator.flow_from_directory(
        directory=’test_images’,
        target_size=(img_rows, img_cols),
        color_mode=’rgb’,
        classes=classes,
        class_mode=’categorical’,
        batch_size=batch_size,
        shuffle=False)

        train_generator = zip(genX1, genX2)
        test_generator = zip(genY1, genY2)

        history = model.fit_generator(
        train_generator,
        samples_per_epoch=samples_per_epoch,
        nb_epoch=nb_epoch,
        validation_data=test_generator,
        nb_val_samples=nb_val_samples)
        ==========================================

        Error log=====

        Traceback (most recent call last):
        File “smallcnn_proposed_3.py”, line 178, in
        nb_val_samples=nb_val_samples)
        File “C:\Users\Takafumi Katayama\Anaconda3\lib\site-packages\keras\legacy\interfaces.py”, line 88, in wrapper
        return func(*args, **kwargs)
        File “C:\Users\Takafumi Katayama\Anaconda3\lib\site-packages\keras\models.py”, line 1124, in fit_generator
        initial_epoch=initial_epoch)
        File “C:\Users\Takafumi Katayama\Anaconda3\lib\site-packages\keras\legacy\interfaces.py”, line 88, in wrapper
        return func(*args, **kwargs)
        File “C:\Users\Takafumi Katayama\Anaconda3\lib\site-packages\keras\engine\training.py”, line 1895, in fit_generator
        batch_size = x.shape[0]
        AttributeError: ‘tuple’ object has no attribute ‘shape’

        ==============

        • ボンド より:

          この場合はおそらく学習モデルへの入力が不正なんだと思います。
          本来は(input1, input2) となるべきところが((input1, output1), (input2, output2))となっているのではないでしょうか。
          一度train_generatorの出力を確認した方がいいと思います。

          train_generatorの出力が不正だった場合、generator同士の結合をあまりやったことがないので、解決法はよくわかりません。
          kerasのgeneratorを使うよりも、yieldを使用して自分でgeneratorを作った方がいいかもしれません。
          fit_generator関数のところに例が記載されています。
          https://keras.io/ja/models/sequential/

          • オオカミさん より:

            ボンドさん
            あれから色々試行いたしましたが、
            numpyを使用して3次元行列に変換することで入力はできるようになりました。
            ただし、まだ完全に動作したわけではありません...orz

            https://qiita.com/tsunaki/items/608ff3cd941d82cd656b
            上記のブログを参考に.npyファイルを2種類作成し、
            下記のようにマージをいたしました。
            おそらくまだデータが入力されてないような気がします。。Errは出ないのですが、学習が開始しないので。

            self.nb_classes = len([name for name in os.listdir(input_dir) if name != “.DS_Store”])
            x_train1, x_test1, y_train, y_test = np.load(“./npy/flower.npy”)
            z_train1, z_test1, k_train, k_test = np.load(“./npy/flower2.npy”)
            # データ1を正規化する
            self.x_train1 = x_train1.astype(“float”) / 256
            self.x_test1 = x_test1.astype(“float”) / 256
            self.y_train = np_utils.to_categorical(y_train, self.nb_classes)
            self.y_test = np_utils.to_categorical(y_test, self.nb_classes)
            # データ2を正規化する
            self.z_train1 = z_train1.astype(“float”) / 256
            self.z_test1 = z_test1.astype(“float”) / 256

            left_branch = Sequential()
            left_branch.add(Convolution2D(32, 3, 3, border_mode=’same’, input_shape=self.x_train1.shape[1:]))

            right_branch = Sequential()
            right_branch.add(Convolution2D(32, 3, 3, border_mode=’same’, input_shape=self.z_train1.shape[1:]))

            merged = Merge([left_branch, right_branch], mode=’concat’)

            # 学習してモデルを保存
            model.fit([self.x_train1, self.z_train1], self.y_train, batch_size=32, nb_epoch=10)
            hdf5_file = “./model/flower-model.hdf5”
            h5_file = “./model/flower-model2.h5”
            model.save_weights(hdf5_file)
            model.save_weights(h5_file)

            # modelのテスト
            score = model.evaluate([self.x_test1, self.z_test1], self.y_test)

          • ボンド より:

            エラーが出ないのに学習が始まらず、プログラムも終了しないのであれば、学習データやモデルの読み込みに時間がかかっているのかもしれません。
            一晩ほど置いておくと学習が始まっている可能性もあります。

  3. オオカミさん より:

    ボンドさん
    ご指摘ありがとうございます。
    まだ解決はしておりませんが、下記の方針で作成してみます。
    色々ありがとうございました。

    >kerasのgeneratorを使うよりも、yieldを使用して自分でgeneratorを作った方がいいかもしれません