Skip to main content

Show and Tell

これはShow and TellのTensorFlow実装に関する論文です
Created on February 5|Last edited on February 17
このレポートは、Aritra Roy GosthipatyDevjyoti Chakrabortyによる「Show and Tell」の翻訳です。



概要

Kaggle Notebookをチェックする

数年前、誰かが提示された風景を正しく説明できる仮想アシスタントがいると主張した場合、人々はそれを笑い飛ばしていたでしょう。機械学習がゆっくりとディープラーニングに乗り出し、無限の可能性を開くにつれて、私たちが夢にも思わなかったアイデアが可能になり始めました。それらのアイデアの1つは、Vinyalsetal。による Show and Tell: A Neural Image Caption Generatorに示されています。この論文では、著者は画像キャプションジェネレータのエンドツーエンドのソリューションを提案しています。このホワイトペーパーの前に、このタスクで提案されたのは、独立したタスクの最適化(ビジョンと自然言語)と、これらの独立したタスクの手作業によるステッチングでした。

このペーパーは、エンコーダーが特定の言語でシーケンスをトレーニングし、別の言語でシーケンスを吐き出すデコーダーの固定長表現を生成するニューラル機械翻訳からインスピレーションを得ています。この考えに基づいて、著者はエンコーダーとして視覚特徴抽出器を使用し、デコーダーとしてシーケンスモデルを使用しました。



📜論文を読む

学術論文を手に入れることは非常に素晴らしいことです。それを読むと、提案されたアイデアを自分で思いついた可能性があることが保証されます。その瞬間、あなたはアイデアがいかにシンプルでありながら強力であるかを考えます。Show and Tell: A Neural Image Caption Generatorは、画像キャプションジェネレーターの深層学習研究の門を開いた素晴らしい論文の1つです。

著者は、機械翻訳を相当導入していると言います。これにより、記事をロードマップの形で分類することになりました。ロードマップをたどると、読者は私たちが論文を読んでいたときと同じ喜びを感じるはずです。

ロードマップ:

  • タスク: 目前のタスクのコアコンセプトの調べ
  • モジュール:使用されているさまざまなアーキテクチャを調べ、それらを使用する理由についての詳しい説明
  • コード:コードを表示せずにチュートリアルを作成する方法
  • 損失と結果:使用される目的関数の定義。また、その動作方法


👨‍🏫タスク

画像が与えられた場合、その画像を説明するキャプションが必要です。これは単なる分類の問題ではなく、人工エージェントが画像がどのカテゴリに属するかを決定します。これは、人工エージェントが分類するオブジェクトに境界ボックスを描画する検出タスクではありません。ここでは、画像の内容を解読してから、画像の内容間の関係を表す一連の単語を形成する必要があります。

頭に浮かぶ最も簡単なアイデアは、タスクを2つの異なるタスクに分割することです。

  1. 👁️ ☒コンピュータビジョン:このパートでは、提供された画像を扱います。画像から特徴を抽出し、階層的特徴から概念を構築し、データ分布をモデル化しようとします。単純な畳み込みニューラルネットワークは、この目的に適しています。画像を入力すると、CNNカーネルは画像から階層的に特徴を取得します。これらの機能は、提示された画像のコンテンツの圧縮表現になります。 image.png

DeepLearning by Goodfellow et. al.

  1. 🗣️ .☒単語の生成:このタスクでは、トレーニング用の画像と画像のキャプションが提供されます。キャプションをモデル化する必要があります。キャプションは、画像を説明する一連の単語です。キャプションデータの分布をモデル化するには、自然言語プロセッサが必要です。モデルは、単語の分布と単語のコンテキストを理解する必要があります。ここでは、キャプションをモデル化し、提供されたキャプションデータスペースから厳密にサンプリングされた単語を生成できる単純な反復アーキテクチャを使用できます。 image.png

ここで注意が必要なのは、2つのレルムをつなぎ合わせることです。キャプションデータの分布から単語を生成するために自然言語プロセッサが必要になるだけでなく、検討中の画像を取得する必要もあります。画像の特徴は、画像キャプション生成の問題における重要な要素です。キャプションジェネレータは、画像の特徴を取得し、そのコンテキストで、キャプションスペースから単語をサンプリングして、画像の説明を提供する必要があります。2つの領域のステッチが、このタスクを非常に興味深いものにしているのです。 image.png

表示して伝える:ニューラル画像キャプションジェネレータ

私が非常に興味深いと思ったちょっとした洞察は、数字の使い方です。私たち人間は、数学と呼ばれる美しいコミュニケーション言語を思いついたのです。ここでは、概念やアイデアなどを数字や記号で表現できます。とりあえず数字に集中しましょう。コンピュータビジョンモデルは、本質的に数値(モデルの重みとバイアス)である画像から貴重な特徴を抽出します。同様に、言語と単語も数字で表すことができます(単語の埋め込み)。これは、利用すると、タスクの多様な領域をつなぎ合わせるという問題を解決できるという考えです。画像キャプション生成のタスクが成功するように、ビジョンモデルから言語モデルに数値を入力する必要があります。



データ

実験では、30,000枚の画像と各画像に対応する複数の一意のキャプションを格納した[Flickr30kデータセット](https://arxiv.org/abs/1505.04870を使用することを選択しました。ユースケースを容易にするために、データは前処理され、 Kaggleでホストされます。Flickr30kのKaggleデータセットを使用します。

データセットには、それぞれのキャプションにリンクする画像のレコードを含むCSVが含まれていました。

データセットの概要は次のとおりです。 image.png caption.png

先に進む前に、読者に<start><end>の使用法を示したいと思います。これは、キャプションの開始と終了をモデルに知らせるために特に重要です。データのトレーニング中は必要ないようですが、テスト時に、モデルがキャプションの最初の単語を生成するために<start>トークンをフィードする必要があります。一方、モデルは<end>トークンを生成した後、単語の生成を停止す���必要があります。




Run set
1


🏋️モデル

私たちは、タスクと、取り組む必要のあるモデルについてかなり理解しています。このセクションでは、数値に関する洞察が非常に役立ちます。著者によって提案されたモデルのアーキテクチャから始めて、作業について深く掘り下げてみましょう。

image.png

提案されたアーキテクチャ

ここに、目前の異なるタスクのための2つの異なるモデルがあります。左側はビジョンモデルで、右側は単語生成に使用される反復モデルです。ここでのアイデアは、画像の特徴を繰り返しモデルにフィードすることです。これは単なる別の言葉でした。画像から抽出される特徴は数字の集まり(ベクトル)です。キャプションの埋め込みスペースにベクトルをプロットすると、間違いなく単語の表現が得られます。このイメージの特徴が一言になり、プロセス全体の美しさです。埋め込みスペースにプロットされた画像の特徴は、シソーラスからの実際の単語を表していない可能性がありますが、反復モデルが学習するには十分です。このいわゆるイメージワードは、反復モデルへの最初の入力です。深く内省すると、読者はこのアイデアがいかにシンプルでありながら効果的であるかを知ることができます。同じ特徴を持つ2つの画像が埋め込みスペースの近くにあり、これらの特徴を反復モデルに提供すると、モデルは両方の画像で類似したキャプションを生成します。埋め込み空間でベクトル代数を計算できるようになったので、このイメージワードの概念も重要であると考えることができます。2つの概念を追加するだけで新しい概念を学習できます。 image_caption.png

イメージワードの例

アーキテクチャに従って、エンコーダーとしてCNNを使用し、デコーダーとしてGRUのスタックレイヤーを使用しました。GRUはLSTMよりも計算効率が高く、単純なRNNよりも効果的であるため、LSTMの代わりにゲート付き回帰ユニットを使用しています。巨大なデータセットを扱っていたことを念頭に置いて、計算の効率をいくらか失う代わりに、パイプラインの効率を高めるためにGRUを選択しました(勾配散逸の問題)。

👀Show

モデルのこの部分は、情報エンコーダーとして機能します。Imagenetからのデータで事前トレーニングされたrestnet50モデルを使用します。ここでは、モデルの最後のレイヤーを省略し、最後から2番目のレイヤーから出力を抽出します。resnet出力の上に、Global Average PoolingとDenseレイヤーをスタックします。GAPを使用して、最後から2番目のカーネル出力の平均を取り、Denseレイヤーを使用して、画像の特徴を単語の埋め込みと同じ形状に成形しようとします。

🗣️Tell

モデルのこの部分は、情報デコーダーとして機能します。これを説明するために、RNNモデルのルーツを調べる必要があります。リカレントアーキテクチャの改訂が必要な人にとっては、Under the Hood of RNNが最適な場所です。単一セルyt​の出力は、現在の入力xtと、前のセルht−1​からの非表示状態のアクティブ化によって決定されます。ここで、最初のRNNセルが入力としてエンコードされた画像を持っている場合、それが生成する非表示状態は次のセルに引き継がれます。この隠された状態は、現在のセルの入力と出力へのリンクとしても機能し、これはシーケンス内のすべてのセルに対して繰り返されます。要約すると、エンコードされた画像の効果は基本的にセルのシーケンス全体に渡されるため、予測される各単語は、与えられた画像を念頭に置いて実行されます。 image.png

Depiction of the recurrent formula



🖥️ コード

KaggleNotebookをチェックしてください

ここでは、コードの重要な部分について説明します。

テキスト処理

次のコードブロックでは、基本的なテキスト処理を行い、利用可能なキャプションのセットから語彙を作成します。また、手動で「パッド」トークンを追加して、後で自分の利益のために同じサイズのすべての文を作成できるようにします。

train_df = df.iloc[:train_size,:]
val_df = df.iloc[train_size+1:train_size+val_size,:]
test_df = df.iloc[train_size+val_size+1:,:]

# Choose the top 5000 words from the vocabulary
top_k = 10000
tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=top_k,
                                                  oov_token="<unk>",
                                                  filters='!"#$%&()*+.,-/:;=?@[\]^_`{|}~')

# build the vocabulary
tokenizer.fit_on_texts(train_df['comment'])

tokenizer.word_index['<pad>'] = 0
tokenizer.index_word[0] = '<pad>'

基本的な語彙の作成が適切に行われたかどうかを確認するために、ヘルパー関数を作成します

# This is a sanity check function
def check_vocab(word):
    i = tokenizer.word_index[word]
    print(f"The index of the word: {i}")
    print(f"Index {i} is word {tokenizer.index_word[i]}")
    
check_vocab("pajama")

次のような出力が得られます。

index.png

次に、トークナイザーによって生成された語彙に従ってトレーニングデータを作成する必要があります。すべての文を手動で同じ長さにパディングします。データをtf.data パイプラインに統合します。

# Create the tokenized vectors
train_seqs = tokenizer.texts_to_sequences(train_df['comment'])
val_seqs = tokenizer.texts_to_sequences(val_df['comment'])
test_seqs = tokenizer.texts_to_sequences(test_df['comment'])

# If you do not provide a max_length value, pad_sequences calculates it automatically
train_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(train_seqs, padding='post')
val_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(val_seqs, padding='post')
test_cap_vector = tf.keras.preprocessing.sequence.pad_sequences(test_seqs, padding='post')

train_cap_ds = tf.data.Dataset.from_tensor_slices(train_cap_vector)
val_cap_ds = tf.data.Dataset.from_tensor_slices(val_cap_vector)
test_cap_ds = tf.data.Dataset.from_tensor_slices(test_cap_vector)

画像処理

次に、flicker30Kデータセット内の画像のtf.dataパイプラインを作成します。画像の読み込み、デコード、データ型の変換、サイズ変更などの基本的な操作を行います。

@tf.function
def load_img(image_path):
    img = tf.io.read_file(image_path)
    img = tf.image.decode_jpeg(img)
    img = tf.image.convert_image_dtype(img, tf.float32)
    img = tf.image.resize(img, (224, 224))
    return img

train_img_name = train_df['image_name'].values
val_img_name = val_df['image_name'].values
test_img_name = test_df['image_name'].values

train_img_ds = tf.data.Dataset.from_tensor_slices(train_img_name).map(load_img)
val_img_ds = tf.data.Dataset.from_tensor_slices(val_img_name).map(load_img)
test_img_ds = tf.data.Dataset.from_tensor_slices(test_img_name).map(load_img)

データの結合

私たちの意図は、作成された2つのデータパイプラインをマージして、それらをネットワークに直接フィードできるようにすることです。データセット全体が非常に大きいため、データをバッチで取得しています。

# prefecth and batch the dataset
AUTOTUNE = tf.data.experimental.AUTOTUNE
BATCH_SIZE = 512

train_ds = tf.data.Dataset.zip((train_img_ds, train_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
val_ds = tf.data.Dataset.zip((val_img_ds, val_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
test_ds = tf.data.Dataset.zip((test_img_ds, test_cap_ds)).shuffle(42).batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)

モデル

Show

前述のように、Showは、画像を圧縮するアーキテクチャのエンコーダ部分を指します。ImageNetでトレーニングされたResNet50モデルは、特徴抽出器として機能し、その後にGAPが続きます。最後に、アーキテクチャのこの部分を完全に接続されたレイヤーで切り上げます。

class CNN_Encoder(tf.keras.Model):
    
    def __init__(self, embedding_dim):
        super(CNN_Encoder, self).__init__()
        self.embedding_dim = embedding_dim
        
    def build(self, input_shape):
        self.resnet = tf.keras.applications.ResNet50(include_top=False,
                                                     weights='imagenet')
        self.resnet.trainable=False
        self.gap = GlobalAveragePooling2D()
        self.fc = Dense(units=self.embedding_dim,
                        activation='sigmoid')
        
    def call(self, x):
        x = self.resnet(x)
        x = self.gap(x)
        x = self.fc(x)
        return x

教えて

GRUセルを収容するTellは、エンコーダーからの情報を使用して、学習したキャプションと元の入力の間のリンクを確立するデコーダーを指します。

class RNN_Decoder(tf.keras.Model):
    def __init__(self, embedding_dim, units, vocab_size):
        super(RNN_Decoder, self).__init__()
        self.units = units
        self.embedding_dim = embedding_dim
        self.vocab_size = vocab_size
        self.embedding = Embedding(input_dim=self.vocab_size,
                                   output_dim=self.embedding_dim)
    
    def build(self, input_shape):
        self.gru1 = GRU(units=self.units,
                       return_sequences=True,
                       return_state=True)
        self.gru2 = GRU(units=self.units,
                       return_sequences=True,
                       return_state=True)
        self.gru3 = GRU(units=self.units,
                       return_sequences=True,
                       return_state=True)
        self.gru4 = GRU(units=self.units,
                       return_sequences=True,
                       return_state=True)
        self.fc1 = Dense(self.units)
        self.fc2 = Dense(self.vocab_size)

    def call(self, x, initial_zero=False):
        # x, (batch, 512)
        # hidden, (batch, 256)
        if initial_zero:
            initial_state = decoder.reset_state(batch_size=x.shape[0])
            output, state = self.gru1(inputs=x,
                                      initial_state=initial_state)
            output, state = self.gru2(inputs=output,
                                      initial_state=initial_state)
            output, state = self.gru3(inputs=output,
                                      initial_state=initial_state)
            output, state = self.gru4(inputs=output,
                                      initial_state=initial_state)
        else:
            output, state = self.gru1(inputs=x)
            output, state = self.gru2(inputs=output)
            output, state = self.gru3(inputs=output)
            output, state = self.gru4(inputs=output)
        # output, (batch, 256)
        x = self.fc1(output)
        x = self.fc2(x)
        
        return x, state
    
    def embed(self, x):
        return self.embedding(x)
    
    def reset_state(self, batch_size):
        return tf.zeros((batch_size, self.units))

トレーニング

クラスオブジェクトを作成し、オプティマイザをAdamとして割り当てます。ここでは、ワンホットエンコーダーを使用するのは非効率的であるため、損失はスパースカテゴリクロスエントロピーです。また、マスクを使用してマスクを支援し、シーケンスモデルが同じものにオーバーフィットすることを学習させないようにします

encoder = CNN_Encoder(EMBEDDIN_DIM)
decoder = RNN_Decoder(embedding_dim=EMBEDDIN_DIM,
                      units=UNITS_RNN,
                      vocab_size=VOCAB_SIZE)

optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')

def loss_function(real, pred):
    mask = tf.math.logical_not(tf.math.equal(real, 0))
    loss_ = loss_object(real, pred)

    mask = tf.cast(mask, dtype=loss_.dtype)
    loss_ *= mask

    return tf.reduce_mean(loss_)

次に、バックプロパゲーションによって勾配を計算するトレインステップ関数を記述します。

@tf.function
def train_step(img_tensor, target):
    # img_tensor (batch, 224,224,3)
    # target     (batch, 80)
    loss = 0
    with tf.GradientTape() as tape:
        features = tf.expand_dims(encoder(img_tensor),1) # (batch, 1, 128)
        em_words = decoder.embed(target)
        x = tf.concat([features,em_words],axis=1)
        predictions, _ = decoder(x, True)

        loss = loss_function(target[:,1:], predictions[:,1:-1,:])

    trainable_variables = encoder.trainable_variables + decoder.trainable_variables

    gradients = tape.gradient(loss, trainable_variables)

    optimizer.apply_gradients(zip(gradients, trainable_variables))

    return loss

@tf.function
def val_step(img_tensor, target):
    # img_tensor (batch, 224,224,3)
    # target     (batch, 80)
    loss = 0
    features = tf.expand_dims(encoder(img_tensor),1) # (batch, 1, 128)
    em_words = decoder.embed(target)
    x = tf.concat([features,em_words],axis=1)
    predictions, _ = decoder(x, True)
    loss = loss_function(target[:,1:], predictions[:,1:-1,:])
    return loss


📉損失と結果

Kaggle Notebookをチェックする

目的関数は、生成された単語の負の対数尤度です。これをもう少し直感的にするために、提供されているアーキテクチャへのフィードフォワード実行に取り掛かりましょう。CNNに入力された画像は、画像機能を提供します。次に、この機能は、提供されている単語の埋め込みと同じ形状のテンソルにエンコードされます。画像フィーチャは、最初のタイムステップでGRUに入力されます。次に、このセルは単語の語彙全体のソフトマックスを生成します。私たちのタスクの目的は、画像を厳密に説明する単語の可能性を高めることです。このメトリックを最小化してモデルをトレーニングできるように、負の対数尤度を取ります。次のタイムステップでは、キャプションの単語を入力として提供し、すぐ次の単語の確率を最大化しようとします。




Run set
1


結語

機械翻訳から学んだ簡単な概念を使用して、著者は実際に画像に与えられた自動キャプションを生成する素晴らしい方法をもたらしました。この論文は、自動キャプション生成ドメインの後続のいくつかの論文、特にShow, Attend and Tellのインスピレーションであり、その名前から推測されるように、このレポートで話されている概念とともに注意を使用しています。

著者と話し合う:

名前TwitterGitHub
Devjyoti Chakrobarty@Cr0wley_zz@cr0wley-zz
アリトラロイゴスティパティ@ariG23498@ariG23498

Iterate on AI agents and models faster. Try Weights & Biases today.