stMind

about Tech, Computer vision and Machine learning

DALL-E 2, Imagen, Parti

テキストから画像を生成するモデルが話題ですが、代表的なモデルであるDALL-E2とImagen、Partiのアーキテクチャを比較しているTwitterのスレッドを紹介。

以下は、アーキテクチャの構成ブロックの簡単な比較を表にまとめたもの。

DALL-E2 Imagen Parti
Text Encoder CLIP T5-XXL Transformer Encoder
Text Embeddings to Image (64x64 or 256x256) Diffusion Model x 2 (Prior + Decoder) Diffusion Model Transformer Decoder + ViT-VQGAN
Upsample the Image (256x256->1024x1024) Diffusion Model x 2 Diffusion Model x 2 Convolution

それぞれの論文は画像がたくさん含まれてはいますが、DALL-E2が27ページ、Imagenが46ページ、Partiは49ページもあって、読むぞ!とは気軽に言えない分量ですね...

TD OD APIでデータ拡張のオプションを追加する

データ拡張のオプションは、TrainConfigの中でdata_augmentation_optionsとして定義されている。

data_augmentation_optionsは、repeatedフィールドで、PreprocessingStepに指定されているデータ拡張を任意の数だけ指定することができる。

// Message for configuring DetectionModel training jobs (train.py).
// Next id: 31
message TrainConfig {
  // Effective batch size to use for training.
  // For TPU (or sync SGD jobs), the batch size per core (or GPU) is going to be
  // `batch_size` / number of cores (or `batch_size` / number of GPUs).
  optional uint32 batch_size = 1 [default=32];

  // Data augmentation options.
  repeated PreprocessingStep data_augmentation_options = 2;

例えば、random_horizontal_flipは、水平方向の反転を行う処理で、デフォルトでは50%の確率で行われる。

pipeline.config内では、フォーマットに従って使いたい拡張を指定すれば良い。

train_config: {
  ...
  data_augmentation_options {
    random_horizontal_flip {
    }
   random_image_scale {
       min_scale_ratio: 0.9
       max_scale_ratio: 1.1
    }
  }
}

一方、ファイル編集ではなくコードの中で直接データ拡張を追加したい場合には、以下の様に行う。

例えば、ssd_resnet50_v1_fpn_640x640_coco17_tpu-8のrandom_horizontal_flipとrandom_crop_imageに対して、random_image_scaleを追加する場合。

from google.protobuf import text_format

from object_detection.builders import preprocessor_builder
from object_detection.core import preprocessor
from object_detection.protos import preprocessor_pb2
from object_detection.utils import config_util

configs = config_util.get_configs_from_pipeline_file(/path/to/pipeline_config)
train_config = configs['train_config']

# Random image scale
preprocessor_text_proto = """
random_image_scale {
  min_scale_ratio: 0.8
  max_scale_ratio: 2.2
}
"""
preprocessor_proto = preprocessor_pb2.PreprocessingStep()
text_format.Merge(preprocessor_text_proto, preprocessor_proto)
train_config.data_augmentation_options.append(preprocessor_proto)

データ拡張オプションが追加されて、3つの処理が指定されている状態。

print(train_config.data_augmentation_options)
"""
[random_horizontal_flip {
}
, random_crop_image {
  min_object_covered: 0.0
  min_aspect_ratio: 0.75
  max_aspect_ratio: 3.0
  min_area: 0.75
  max_area: 1.0
  overlap_thresh: 0.0
}
, random_image_scale {
  min_scale_ratio: 0.800000011920929
  max_scale_ratio: 2.200000047683716
}
]
"""

参考

discuss.tensorflow.org

BERTの概要を掴む

一回目のMulti Head Attention、二回目のGPTに続いて、三回目はBERT。

Multi Head Attentionの概要を掴む - stMind

GPTの概要を掴む - stMind

以下、メモを残す。

3/n. Bi-directional

GPTでは、未来の単語の予測を目的として、言語モデルをpre-train出来ることを確認してきた。BERTでは、未来の単語の予測の代わりに、空欄を埋める目的を使用する。GPTと異なり、過去と未来のトークンを同時に見るため、双方向と呼ばれる。

4/n. BERT

BERTはどのように動作するか?アーキテクチャは以下の通り。GPTと二つの項目を除いて、ほぼ同じように動作する。一つ目の違いは、Causalマスクの代わりにランダムマスクを使うこと、二つ目はシーケンスの先頭に[CLS]を追加すること。この[CLS]トークンとは何か?

5/n. [CLS]トーク

例えば、Sentiment Analysisを行いたいとする。Transformerの出力shapeは [B, T, D]であり、これを[B, D]に圧縮し、分類器に入力する集約的な表現にしたい。最初に思いつく方法として、Tについて平均をとるのはどうか?

6/n. Aggregate representation

これでも機能するが、全てのトークンは分類に等しく有用であることを想定している。Attentionは、関連性に基づいてトークンを重みづけすることが重要だったはず。入力に新しいトークンを追加して、他のトークンをAttentionで集約したらどうか?それが[CLS]。

7/n. 分類器への入力としての[CLS]

分類タスクに対してBERTをFinetuneする場合、[CLS]トークンの最後の隠れ状態を分類器への入力として使用する。この時、MLPはshapeが[B, D]の隠れ状態を[B, クラス数]の出力に変換する。

8/n. [MASK]と[SEP]

BERTでは、[CLS]の他にも特別なトークンがある。マスクされたトークンを置き換える[MASK]と、センテンスの区切りとなる[SEP]。Tokenizationはデータの読み込み時に行われ、その後、positional embeddingが加えられる。

9/n. Transformer block

BERTがランダムマスク、GPTがCausalマスクを使用する違いはあるが、Transformer blockの実装はほぼ同じである。

10/n. generate_random_mask, mlm_loss

BERTでは、pretrainのためにデータのバッチを渡すたびに、新しいランダムなマスクをサンプリングする。これは、GPU上で直接マスクを作成すれば、効率的に行うことができる。以下が、マスクの作成とmasked language modelingの目的の実装。

11/n. tl;dr

BERT=GPT、ただしランダムマスクと他の特別なトークンを使用する違いがある。Transformerはとてもシンプルで汎用的であることは驚きである。

次回は、コンピュータビジョンにTransformerを適用したViTとMAE lossについて。

GPTの概要を掴む

前回は、Multi Head Attentionに関するTwitterの一連のスレッドを紹介した。

Multi Head Attentionの概要を掴む - stMind

今回はGPTについて。

以下、前回同様に自分が理解したメモを残す。

2/n. Multi Head Attentionの振り返り

Attentionは、ネットワークが入力に含まれる全ての単語とその関係を捉えることを可能にする。結果として、ネットワークはその目的を最適化するための最も重要な単語に注意を向ける様になる。

3/n. MHAの最適化

これまでのところ、MHAが最適化する目的を定義していない。GPTでは、非常にシンプルな、次の単語の予測というunsupervisedな目的を用いる。直前までの単語が与えられたら、次の単語を予測する。この目的であればラベルは不要なので、unsupervisedと呼ばれる。

4/n. Causal構造

未来の単語を予測する場合には、直前までの単語だけ参照するCausal構造を強制する必要がある。Causal Attention行列において、0は「関係性なし」を意味し、現在の単語と未来の単語間のAttentionは全て0にする必要がある。そのために、Weights行列( QK^{T})において、未来の単語を-infとする。

5/n. Causal Attention

Weights行列にSoftmaxを適用した後で、未来の単語をマスクして0にすると正規化されなくなる。そのため、Weights行列で未来の単語を-infにしてからSoftmaxを適用する。

6/n. Masked Causal Attention

Masked Causal AttentionがGPTのメインアイデア。GPTのTransformer BlockはMHA→LayerNorm→MLP→LayerNrom。入力shapeは(B, T, D_in)、出力shapeは(B, T, D_out)で、大抵はD_out=D_inとなる。

7/n. Loss

GPTの目的は次の単語の予測であった。英語には約100万語あり、文字通りに単語を予測する場合、100万クラスの分類をすることになる。GPTが最適化するロスを記述する。

loss = cross_entropy(preds, targets) # (B, T, 1e-7)

8/n. Tokens

ロスにおけるクラス数を削減するために、トークンを用いる。トークンは文字とベクトル間のmapで、例えばアルファベットの文字は26個の一意なベクトルで表すことができる(one-hot ベクトル)。文字列からユニークなベクトルへの変換はトークン化(Tokenization)と呼ばれる。

9/n. Byte Pair Encoding

GPTでは、頻度の高い文字グループ(ペア)をトークンにするByte Pair Encoding(BPE)を用いる。以下は、Attention is not too shabbyにBPEを適用して、11個のユニークなトークンになった例(ユニークなCharは12個)。

10/n. The order of words

最後の、しかし重要な問題は、今のモデルには語順を知る方法がないということ。

11/n. Positional tokens

語順をエンコードするために、Positional tokensを使用する。文字トークンと同じように、位置をユニークなベクトルで表す。文字と位置のトークンをそれぞれ線形層で変換した後、それらを足し合わせる。その後、Transformerブロックに入力される。

12/n. 実装

Multi Head Attentionの概要を掴む

DeepMindのResearch Scientistの方がツイートしていたMulti Head Attentionのスレッドの紹介。 全部で12個。英語だけど、日本語に翻訳すれば10分くらいで読めるし、コードサンプルと図もあって短い時間でMHAの概要が掴めると感じた。

以下は、自分が理解した内容のメモ書き。

2/n. Attentionとは

例として、sentiment analysisをしたいとする。「Attention is not too shabby.」 shabbyはネガティブを示唆しているけれど、not shabbyであればネガティブではなくポジティブ。正しく分類するためには、文中の全ての単語を考慮しないといけない。

3/n. 全ての単語を考慮する

全ての単語を考慮する最もシンプルな方法は、全ての単語をネットワークに入力すること。これで十分か?というと、そうではない。各単語を考慮するだけでなく、他の単語との関係も理解しないといけない。つまり、notはshabbyに注意を向けているということが重要。そこで出てくるのが、query, key, value(Q,K,V)。

4/n. Value

単語を線形層に通し、そこから得られた出力をValueと呼ぶ。

words.shape # (T,in_dim)
values = Linear(words)
values.shape # (T, D)

では、Value同士の関係をどの様にエンコードするか?それぞれのValueを混合(sumをする)することで、関係を見ることができる。ただし、これには問題がある。

out = np.ones((T, T)) @ values # (T, T) @ (T, D)
out.shape # (T, D)

5. 問題

単純な総和の問題は、全ての関係が等しいと想定していること。isとtoo、notとshabbyでいえば、明らかに後者の方が感情分類に重要。

6. QueryとKey

全てが等しい関係ではなく、word_jに対してword_iがどれだけ有用かを表す様にしたい。そこで、Valueと同じように単語を線形層に通して得られるQueryとKeyを導入する。QueryとKeyから求めた重みWeightsにおいて、w_ijはi番目のQueryに対して、j番目のKeyの間の内積に比例した値になっているはず。

queries.shape # (T, D)
keys.shape # (T, D)
weights = queries @ keys.T # (T, T)

7. Rescaling

QueryとKeyから得られたWeightsの各要素を \sqrt{D}でrescale。

8. Single Head Attention

Weightsの列ベクトルについてSoftmaxを適用して正規化する。直感的には、Qは単語Kに対してのどの程度有用かという質問で、内積が高いということは非常に有用、逆に内積が低い時はあまり役に立たないということ。これがAttention。

attention = softmax(Q @ K.T / sqrt(D), dim=1) @ Value # [T, D] = [T, T] @ [T, D]

9. Single Head Self Attention

10. Why Multi Head

なぜMulti Headなのか?Single Headだと学習データにオーバーフィットするかもしれない。過学習対策の一般的な戦略であるアンサンブルで、複数のAttentionによりロバストな結果を獲得する。(Multi Head Attentionは、Single Head Attentionの[T, D]をN個連結したもので、[T, NxD])

11. Multi Head Self Attention

forwardの入力xは、[T, D]をN個分concatした[B, T, ND](ただし、Bはバッチサイズで、ND = C)と想定している。

データセットをtrain/val/testに分割するコードをnumpyで簡潔に記述する

tl;dr

numpy.splitを使って、aryを3つのsubarrayに分割する。

import numpy as np
train, val, test = 
    np.split(ary, [int(len(ary) * .6), int(len(ary) * .8)])

簡単な説明

データをtrain/testに分割する時、scikit-learnのtrain_test_splitを使うことが多いと思いますが、 train/val/testと分割しようとすると、一度train/testと分けた後でtestに対して再度train_test_splitするなどが必要です。

numpy.split(ary, [a, b])は、第一引数に指定されたaryに対してary[:a], ary[a:b], ary[b:]と分割されるため、一回の処理でデータセットをtrain/val/testに分割することができます。

train/val/testで60/20/20に分割するときは、

np.split(ary, [int(len(ary) * .6), int(len(ary) * .8)])

と指定すればOKです。

ただし、np.splitは単純に分割するだけですので、各セットのラベルの分布を維持するtrain_test_splitのstratifyのような分割をしたい場合には、別途ケアする必要があります。

参照先

以上の内容は、下記stackoverflowの投稿を参照したものです。

stackoverflow.com

Pythonで行列から行と列を取得するコードを簡潔に記述する

tl;dr

アスタリスクによるアンパックとzipを組み合わせる。

for row_vals, col_vals in zip(matrix, zip(*matrix)):
    print(row_vals, col_vals)

簡単な説明

まず、matrixは次のようなリストとする。

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

行は難しいことはなく、問題は列の方。 ここで、アスタリスクでアンパック→アンパックされた各リストに対して、zipで一つずつ要素を取り出してタプルとして返す、という処理をすることで行列の転置と同じことが実現できる。

print(*matrix)
# [1, 2, 3] [4, 5, 6] [7, 8, 9]

for col_vals in zip(*matrix):
    print(col_vals)
# (1, 4, 7)
# (2, 5, 8)
# (3, 6, 9)

従って、最初のコードを実行すると、行と列を一つずつ取り出すことが出来る。

for row_vals, col_vals in zip(matrix, zip(*matrix)):
    print(row_vals, col_vals)
# [1, 2, 3] (1, 4, 7)
# [4, 5, 6] (2, 5, 8)
# [7, 8, 9] (3, 6, 9)