第1回では、物体検出の歴史とDETRの原理の簡単な解説、Google Colaboratory(以下、Colab)上での推論方法をご紹介しました。今回はもっと踏み込んでDETRとTransformerについて詳しく解説するとともに、DETRのFine-Tuning方法を説明します。
1.始めに
本記事では、第1回よりもDETRのアーキテクチャを詳細に解説しながら、自然言語処理のTransformerを解説しつつ、DETRとの違いやDETRで新たに使われている技術について説明いたします。また、Fine-Tuningを行う方法についても解説し、実際にOpen Images Dataset 1 を用いたFine-Tuning方法についてもご紹介します。DETRについて知識を深めたい、DETRを自分で再学習して特定の物体を検出してみたい、という方の参考になれば幸いです。
2.Transformerについて
DETRではTransformerを使っています。そのため、本題のDETRの説明に入る前にある程度Transformerについて理解できていた方が良いと思いますので、自然言語処理の内容になってしまいますが解説いたします。より詳細に知りたい場合は、弊社記事の 「はじめての自然言語処理 第3回 BERTを用いた自然言語処理における転移学習」や、Ryobot氏の 「論文解説 Attention Is All You Need (Transformer)」、halhorn氏の 「作って理解する Transformer / Attention」 などで分かりやすく解説されていますのでご参照ください。
最初はTransformerの概要から説明します。Transformerは、2017年12月にGoogleから「Attention Is All You Need」2 という論文で提案されました。今まで自然言語処理で主流だったRNNやCNNを使わず、Attention 3 のみを使っているEncoder-Decoderモデルです。Transformerのアーキテクチャは下の図になります。DETRのTransformerとの違いについては後述します。
左側がEncoder、右側がDecoderになります。また、論文タイトルである「Attention Is All You Need」を和訳すると「必要なのはAttentionだけ」となり、Transformerの理解にはAttentionが重要になることが分かります 4。TransformerではAttention以外にも色々な処理がありますので、それぞれ①~⑤の番号を割り振り、次節から「Attention is All You Need」を題材にして解説します。
2-1.①Embedding
まず初めに、TransformerのEncoderとDecoderに入力する単語もしくはサブワード (のID) を、ベクトルに変換します。Embeddingは「埋め込み」という意味で、自然言語処理だと単語などの特徴量を、ベクトル空間に埋め込む処理を行います。単語を3次元ベクトル空間にEmbeddingした際の例を下図に示します。
埋め込みベクトルに変換することで、インプットデータ間の類似度や関連性を比較することができます。例えば、「犬」と「ダックスフンド」は文字だけ見ても似ていないですが、ベクトル間の距離が近いので類似度が高い、ということが埋め込みベクトルだと分かるかと思います。また、上記の例では3次元のベクトル空間ですが、「Attention is All You Need」の論文では512次元、DETRの論文では256次元にEmbeddingしています。ちなみに、各語彙の埋め込み表現の具体的な値はそれ自体が学習パラメータで、Transformerの学習中に一緒に形成されていくことになります。
2-2.②Positional Encoding
TransformerではCNNやRNNを使わないため、位置情報(自然言語系だと単語の順序、画像系だとピクセルの位置関係など)が考慮されません。そのため、各インプットデータがどの位置にあるのかを表す一意の値であるPositional Encodingを足し合わせることで、位置情報を付与しています。「Attention is All You Need」の論文では、正弦波の固定値をPositional Encodingとして足し合わせています。数式は以下になります。
pos
はインプットデータの何番目のデータか、2i,2i+1
はEmbeddingの何番目の次元かを表し、dmodel はEmbeddingした際の次元数を表します。また、実際に上記式を可視化すると以下のようなイメージになります。横軸がインプットデータの長さ(上記例だとl=100)、縦軸がEmbeddingの次元数(上記例だとd=512)を表します。
「Attention is All You Need」の論文では、上記式は相対的な位置(PE(pos+k)など)を線形関数で表現可能であるためニューラルネットが学習しやすい、という根拠で使われています。そのため、Positional Encodingの値は正弦波の固定値以外でも問題なく、初期値ランダムの学習パラメータが使われることもあります。第1回目の記事 の「4-1.重みパラメータのみをダウンロードして推論」 のモデルでは、Positional Encodingは初期値ランダムの学習パラメータを使いました。
2-3.③Attention
Attention 3 は、その名の通りデータの注目度や類似度を求める処理です。詳細なアーキテクチャは上図のようになっていて、上図を式で表すと以下のようになります。
Attentionのインプットは「注目度を調べたい入力(input)」と「注目度を調べる対象データ(memory)」の2つになります。それぞれEmbeddingした埋め込みベクトルのシーケンス (要素数は[文章をサブワードに分割したシーケンス長, 埋め込みベクトルの次元数]) です。その2つのインプットデータを、さらに「Query」「Key」「Value」の3つに分けて考えます。以下のようにinputとmemoryを、Query(Q)、Key(K)、Value(V)に割り当てます。
- Q = Wq(input)
- K = Wk(memory)
- V = Wv(memory)
KとVは同じデータであるmemoryを使いますが、Q、K、Vそれぞれに個別の重みパラメータ Wq、Wk、Wv を持っているので、必ずしも同じデータになるとは限りません。
次に、QとKの行列積をとります。QとKは埋め込みベクトルのシーケンスであるため、「2-1.①Embedding」 で解説した通りベクトルが近いと注目度が大きくなり、ベクトルが離れていると注目度が小さくなります。その行列積にSoftmax関数を適用することで、QとKの注目度を確率に変換しています。また、行列積を √dk で除算している理由は、QとKの次元数が大きいほどSoftmax関数の勾配が0に近くなってしまい、うまく学習できなくなってしまうためです(この処理を「Scaled Dot-production」と言います)。そして最後にVと掛け合わせることで、Attentionが求まります。
次節では、自然言語処理でのAttentionの具体的な処理例を紹介いたします。
2-4.Attentionの具体例
例えば、「ラーメン食べよう」という文章に対して、「お腹が減った」という文章がどのような注目度を持っているのかを求める場合を考えます。
(1)埋め込みベクトル化
「ラーメン食べよう」と「お腹が減った」の関係性を求めたいので、inputとmemoryは以下のようになります。また、それぞれの文章をサブワードに分割してEmbeddingします。
input = [ラーメン / 食べ / よう] の埋め込みベクトルのシーケンス(以下記述省略) memory = [お腹 / が / 減った] の埋め込みベクトルのシーケンス(以下記述省略)
(2)Positional Encodingを付与
実際はPositional Encodingを加算しますが、分かり難くなるため省略します。
(3)Query、Key、Valueに割り当て
inputをQueryに、memoryをKeyとValueに割り当てます。また、それぞれの重みをWq、Wk、Wvとします。
Q = Wq([ラーメン / 食べ / よう]) K = Wk([お腹 / が / 減った]) V = Wv([お腹 / が / 減った])
(4)それぞれの注目度を求める
QとKの内積を計算してSoftmax関数で確率に変換します。各サブワードに対して注目度を求めるので、下記画像のようなイメージになります。また、確率は分かりやすいように適当な値を記載していますので、実際はこのような綺麗な値にはならないと思います。
(5)VとかけてAttentionを導出
最後にValueと掛け合わせて、加重和を求めます。最終的にAttentionは、「ラーメン」の注目度、「食べ」の注目度、「よう」の注目度の3つのシーケンスになります。
Attention(Q,K,V) = [ 0.7×Wv([お腹]) + 0.1×Wv([が]) + 0.2×Wv([減った]), 0.5×Wv([お腹]) + 0.2×Wv([が]) + 0.3×Wv([減った]), 0.3×Wv([お腹]) + 0.4×Wv([が]) + 0.3×Wv([減った]) ]
これでAttentionの処理は終わりです。Attentionの処理イメージを掴んでいただけたら幸いです。
2-5.Attentionの種類
Attentionには、「Self-Attention」と「SourceTarget-Attention」の2種類あります。特に内部処理が変わっているわけではなく、インプットデータが違うだけです。「Self-Attention」ではinputとmemoryで同じデータを使い、「SourceTarget-Attention」ではinputとmemoryで異なるデータを使います。前章の例のように具体例を示すと、以下の通りになります。
(1)Self-Attention
Q = Wq([ラーメン / 食べ / よう]) K = Wk([ラーメン / 食べ / よう]) V = Wv([ラーメン / 食べ / よう])
(2)SourceTarget-Attention
Q = Wq([ラーメン / 食べ / よう]) K = Wk([お腹 / が / 減った]) V = Wv([お腹 / が / 減った])
「2.Transformerについて」 に図示したTransformerのアーキテクチャを見ると、Attentionが3箇所あります。右上にあるDecoderの「Multi-Head Attention」の箇所がSourceTarget-Attentionで、それ以外はSelf-Attentionになります。
2-6.Multi-Head Attention
「Attention is All You Need」の論文では、性能向上のためにAttentionに「Multi-Head Attention」という工夫をしています。「Multi-Head Attention」は上図の通り、Query、Key、Valueを何個かに分割してそれぞれAttentionを計算し、計算結果を結合する手法です。1つのAttentionで深く関係性を見るよりも、分割してAttentionを計算した方がより広範囲に豊かな関係性を見る事ができるため、通常のAttentionよりも性能が向上します。「Attention is All You Need」の論文では埋め込みベクトルの次元数が512次元、Multi-Headの個数が8個で処理されています。ですので、1つ目のAttentionでは1~64次元、2つ目のAttentionでは65~128次元、といったように分割してAttentionを計算します。
2-7.④Add & Norm
ここでは、残差接続やLayer Normalization、Drop outなどTransformerがうまく学習できるような処理を行っています。詳細は割愛しますので、興味のある方は上記キーワードで検索していただければと思います。
2-8.⑤Feed Forward
FFN(Feed Forward Network)と書かれることもあります。和訳すると「順伝播ネットワーク」になります。普通の深層学習のように、ReLUなどの非線形活性化関数をかけることでうまく学習できるようにする処理になります。ちなみに「Attention is All You Need」の論文では、Linear(ReLU) - Linear の2層ネットワークを使っています。
2-9.Transformer
前章まででTransformerの各処理を説明しました。それらを踏まえて、改めてTransformer全体の説明をします。「2.Transformerについて」 に図示したアーキテクチャを再掲しますので、改めてご覧ください。
左側のEncoderでは、インプットデータをEmbeddingやPositional Encodingで前処理を行い、Self-Attentionを実行して、Add&Normで残差接続・Layer Normalization・Dropoutなどの処理を行います。その後、FFNを実行して再度Add&Norm処理を行っています。インプットデータをSelf-Attentionに通すことで、インプットデータ自身の特徴や構造、重要部分の単語などを理解することができます。
右側のDecoderは、自己回帰(Autoregressive)になっています。つまり、時刻tの入力からt+1の単語を生成し、t+1の単語を入力に追加して、t+2の単語を生成します(例えば、<BOS> ⇒ <BOS>/ラーメン ⇒ <BOS>/ラーメン/食べ
のように、1単語ずつ生成するイメージです)。前処理と後処理はEncoderと同じで、違いはAttentionの処理をSelf-AttentionとSourceTarget-Attentionの2回行う点です。最初のSelf-Attentionでアウトプットデータ自身の特徴を把握し、SouceTarget-AttentionでEncoderのアウトプットをmemoryとして入力することで、インプットデータとアウトプットデータの特徴を使って新しいアウトプットデータを生成しています。
Transformerについて、ある程度は理解いただけたでしょうか。今回はDETRの記事ですので詳細な説明ではないですし、省略している部分もありますので、興味を持たれた方はぜひご自身でも調べていただければ幸いです。それでは、次章から本題であるDETRの説明を行います。
3.DETRのアーキテクチャの詳細説明
第1回目の記事でも図示しましたが、DETRの論文 5 に記載されているアーキテクチャの図を、改めて下図に示します。また、DETRのTransformer部分のアーキテクチャ図も示します。前回よりも細かい解説と各次元の要素数(以下、shape)を明記しながら解説していきます。
3-1.①backbone
前章で説明したTransformerと比較すると、「2-1.Embedding」 に相当します。ここでは、主に2つの処理を行います。
- インプット画像に対してCNNで畳み込みを行い、d次元の特徴マップに変換。
- Transformerのインプットにするためにreshape。
CNNでは、ImageNetで事前学習したResNet-50 6 をTorchVisionからインポートし、最終層を削除してd次元に変換する2次元畳み込み層を追加したネットワークを使います。また、第1回目の記事 でも掲載した処理イメージの図を見た方が分かりやすいかと思いますので、再掲します。
バッチサイズをB、入力画像の幅・高さ・チャネル数をそれぞれW、H、C、CNN適用後の特徴マップの幅・高さ・チャネル数をそれぞれW'、H'、dで表しています(論文ではW’=W/32、H’=H/32、d=256)。インプット画像のshapeは(B, H, W, C)
だったものが、最終的に(B, H'×W', d)
になります。
3-2.②positional encoding
「3-1.①backbone」 で生成した特徴マップに対して、位置情報を付与します。上図のDETRのTransformer部分のアーキテクチャを見ていただければ分かる通り、DETRにはPositional encodingとして「Spatial positional encoding」と「Object queries」の2種類が使われています。また、インプット時に1回だけPositional Encodingを足し合わせるのではなく、Attention実行前に毎回Positional Encodingを足し合わせています。
(1) Spatial positional encoding
空間位置エンコーディングと呼ばれます。「Attention is All You Need」の論文と同じく、正弦波の固定値を使います。詳細は 「2-2.Positional Encoding」 をご確認ください。EncoderのAttention前、DecoderのSourceTarget-Attentionの前にEncoderのアウトプットに対して足し合わせます。
(2) Object queries
オブジェクトクエリと呼ばれます。Object queriesの本来の役割はDecoderのインプットですが、Decoder側のPositional Encodingとしても使われています(Object queriesの詳細は 「3-4.④decoder」 で説明します)。また、Object queriesは正弦波の固定値ではなく、初期値がランダムな学習パラメータになります。
なぜPositional Encodingが2種類あって、「Spatial positional encoding」には正弦波の固定値、「Object queries」には初期値がランダムな学習パラメータを使っているのかというと、様々な組み合わせを実験した結果、この組み合わせが一番性能が高かったためです。実験結果がDETRの論文に記載されているので、下図に示します。
上記表では、左側がPositional Encodingの組み合わせ、右側が性能を表しています。「spatial pos. enc.」の「encoder」がEncoderのAttention前、「decoder」がDecoderのSourceTarget-Attentionの前のEncoderのアウトプットに足し合わせる箇所を指します。また、「output pos. enc.」の「decoder」はDecoder側のPositional Encodingのことです。それぞれ色々なパターンを試しています。「none」はPositional Encoding自体を使いません。「sine at input」はAttention前ではなくインプット時に1回だけ正弦波の固定値を足し合わせます。「sine at attn.」はAttentionごとに正弦波の固定値を付与。「learned at input」と「learned at attn.」は学習パラメータをそれぞれ使った場合を意味します。最終的に、表の一番下の行の性能が一番良かったため、このような組み合わせでPositional Encodingを使うことになったわけです。また、Positional Encodingのshapeは特徴マップと同じく(B, H'×W', d)
になります。
3-3.③encoder
DETRのTransformerアーキテクチャ図の左側部分に相当します。「2-9.Transformer」 で解説した内容とほぼ同じ仕組みですが、DETRではSelf-AttentionごとにPositional Encodingを足し合わせる点が異なります。
補足になりますが、DETRの論文ではEncoderのSelf-Attentionを可視化していたので、下図に示します。それぞれ赤点から見て、各地点のAttention Scoreの高い箇所を黄色く表示したものになります。下図を見たら分かるように、Encoderの時点である程度オブジェクトを分離できていることが分かります。
また、Encoderのアウトプットのshapeですが、インプットのshapeと同じく(B, H'×W', d)
になります。
3-4.④decoder
DETRのTransformerアーキテクチャ図の右側部分に相当します。Encoderと同様、基本は「2-9.Transformer」 で解説した内容とほぼ同じです。違いはインプットにObject queriesを使うことと、自己回帰構造ではなくなったので並列的に処理できるようになったことです(並列デコーダについては後述)。
Object queriesとは何かというと、主な役割はDecoderへのインプットで、その実態は画像内から物体検出とラベル分類をするために学習されるパラメータのことです。また、前述の通りDecoderのPositional Encodingとして使う、という2種類の役割を持っています。初期値はランダムなベクトル値であり、d次元のベクトルが任意のN個 (論文ではN=100) で、それら全てが学習パラメータになります。下図は物体検出結果から、対応するObject queriesのうち20個を可視化したものです(Object queries自体のベクトルを可視化したものではありません。DecoderのアウトプットのObject queriesをFFNに通して、バウンディングボックスを検出した結果を可視化しています)。バウンディングボックスの中心位置に点を図示しており、赤色の点は大きな横長のバウンディングボックス、青色の点は大きな縦長のバウンディングボックス、緑色は小さなバウンディングボックスを表します。Object queriesの1つ1つが別々のエリアやボックスサイズに特化して学習しているので、N個の物体を検出できるということになります。そのため、100個以上の物体を検出したい場合はN>100にする必要があります。
Encoderの補足と同様に、DecoderのAttentionを可視化したものを下図に示します。可視化した箇所は象の鼻など色がついて光っている箇所で、バウンディングボックスではないことにご注意ください。また、Object queriesごとに別の色で可視化しています。Encoderで大まかなオブジェクト分離は完了しているため、Decoderではオブジェクト境界(足や耳など)を集中して注目していることが分かります。
また、Decoderのアウトプットのshapeはインプットのshapeと同じになるため、(B, N, d)
になります。
3-5.⑤prediction heads
TransformerのDecoderで出力された予測済みのObject queriesを、FFNに通してバウンディングボックスの座標とクラスラベルに変換します。FFNは、バウンディングボックス側は Linear(ReLU) - Linear の2層ネットワーク、クラスラベル側はLinear層によって計算されます。ここで注意なのが、結果が必ずN個出力される点です。Nは通常、画像内の実際のオブジェクト数よりも多く設定するので、大半はオブジェクトが検出されないことになります。そのため、オブジェクトに紐付かないことを示すクラスラベル ∅ (no object) を設定します。また、学習時に予測結果と正解を紐づける必要がありますが、紐付け方法は「4-2.2部マッチングロス (Bipartite Matching Loss)」 で後述します。
4.DETRで使われた新技術
2章と3章では、元々のTransformerとDETRについて詳しく解説しました。本章では、DETRから新たに使われている技術について紹介します。
4-1.並列デコーダ (Parallel Decoding)
元々のTransformerのDecoderは 「2-9.Transformer」 で解説した通り、自己回帰で単語を生成したり予測を行います。そのため、出力データ長に比例して推論コストが非常に高くなってしまいます。しかしDETRでは、Object queriesを使うことによって、回帰構造ではなく並列処理ができるDecoderにすることができました。
4-2.2部マッチングロス (Bipartite Matching Loss)
DETRに画像を入力すると、Object queriesの数N(=100) 個のバウンディングボックスとクラスラベルが出力されます。DETRで学習する際には、この出力結果と正解ラベルとを比較してロスを計算する必要があります。 例えば下の図のようにN=4の時を考えた場合、それぞれ対応するバウンディングボックスに対してロス計算をしていきます。
上記画像のように学習がある程度進んでいれば対応付けは容易ですが、学習初期などは出力結果は滅茶苦茶になっていると考えられます。また、N=100など個数が多くなってくると、出力結果と正解ラベルとの紐付けが非常に困難です。この問題を「2部マッチング問題」と言います。DETRでは、2部マッチング問題を効率的に解くために「ハンガリアンアルゴリズム」を使います。ハンガリアンアルゴリズムは最も効率の良いマッチングパターンを見つける手法であり、計算コストもO(n!)からO(n3 )になります。
以下に、実際にハンガリアンアルゴリズムを使ったロス計算の数式を紹介します。最初はハンガリアンアルゴリズムを使って、正解と推論結果の組み合わせでロス総和が最小になるようなマッチングパターン σ^ を求める数式です。
yi は正解、y^i は推論結果を表します。また、yi={ci, bi} となり、それぞれciは正解のクラスラベル、biは正解のバウンディングボックス位置を表します(c^iとb^iも同じく、推論結果のクラスラベルとバウンディングボックス位置です)。NはObject queriesの数です。また、SN はN次の対称群を表します。対称群とは集合の用語で、DETRでは正解と推論結果の組み合わせが存在する群を示しています。つまり σ(i) は、正解のi番目に対応する推論結果のインデックス番号を返します。数式の(1)部分は、SN 上で(2)部分が最小となるマッチングパターン σ の集合という意味になります。数式の(2)部分は、ロス Lmatch の総和を意味します。
ロス Lmatch の数式は以下の通りです。左項はクラスラベルのロス、右項はバウンディングボックス位置のロスに相当します。ここで正解yi と推論結果yσ(i) をマッチングした時に生じるロスを計算しています。
太文字の 1 は集合を表していて、数式の(3)部分は、該当クラスなし(no object)の場合は0の集合、それ以外は1の集合にするという意味です。また、数式の(4)部分では正解クラスラベルである確率のことを表します。右項も同様に、該当クラスなし(no object)の場合は0の集合にして、それ以外の場合はLboxを計算します。
Lbox の数式は以下になります。バウンディングボックス位置に対するロスを表していて、左項では物体予測位置に対する損失であるGeneralized IoU(以下GIoU)ロス、右項はL1ロスを計算しています。バウンディングボックス位置に対するロスの計算には、通常はLP 損失が使われますが、大きな物体はロスが大きくなり、小さな物体は損失が小さくなってしまいます。そのため、スケール依存性を小さくするために、GIoUロスとL1ロスを足し合わせています。
λiou と λL1 はハイパーパラメータです。数式の(6)部分ではL1ノルム(各成分の絶対値の和)を計算しています。(5)部分はGIoUロス関数で、数式は以下の通りです。なお、GIoUの数式をひとまとめにして記述すると分かり難いため、GIoU(A,B)として別式で記載しています。
∩は積集合、∪は和集合、バックスラッシュは差集合を表しています。また、Aはbi、Bはb^σ(i)、CはAとBの領域を囲う最小の矩形領域を示しています。
ここまでの数式で、ロス総和が最小になるようなマッチングパターン σ^ を求めることができます。このマッチングパターン σ^ を使い、ハンガリアンロスを以下の数式で求めます。
数式の(7)部分では、正解クラスラベルの対数確率を求めています。右項はLmatch と同じくバウンディングボックス位置のロスを計算します。つまり LHungarian では、最適なマッチングパターン σ^ からクラスラベルロスとバウンディングボックス位置のロスの和を求め、全オブジェクトの総和を求めています。
5.DETRのFine-Tuning
ここからは、本題のDETRのFine-Tuning方法を紹介します。DETRにはmain.pyが用意されており、pythonコマンドで引数を渡すことで学習や推論、Fine-Tuningが可能になっています。しかし、COCO dataset 6 以外を使うことが想定されておらず、「--dataset_file
」引数で指定できるパラメータが ‘coco’ または 'coco_panoptic’ しかありません。自分でパラメータを追加してもいいのですが、既に有志の方がCOCO dataset以外を使えるように修正したソースコードがありましたので、今回はそちらを使ったFine-Tuning方法をご紹介したいと思います。修正版のソースコードはwoctezuma氏のGitHub になります。また、オリジナルのDETRからの修正箇所は こちら です。「--dataset_file
」引数に 'custom’ を追加されていることが分かるかと思います。
第1回目の記事 と同じく、Google Colaboratory(以下、Colab)上にソースコードを上から順番にコピーすれば動作するように記載していきます。しかし、推論と違い学習の場合はGPUモードにする必要があります。以下の画像のようにColabのメニューの「ランタイム」→「ランタイムのタイプを変更」から「GPU」に変更してください。ただし、ColabのGPUは使いすぎるとアクセス制限がかかることがあります。今回ご紹介するソースコードをTesla K80が割り当てられたColabで筆者が試したところ、7epoch程度でアクセス制限に引っかかってしまいました。もっとFine-Tuningを回したい場合は、AWSやGCPを使う等別途環境を用意する必要があります。
5-1.Open Image Datasetのフォーマット変換
Fine-Tuning元の学習済みモデルには、第1回目の記事 でも紹介した、Model Zooに公開されている「DETR-DC5」を使います。このモデルはCOCO datasetを使って学習しているため、Fine-TuningをするためにCOCO dataset以外のデータセットを用意します。COCO datasetと重複していないクラスラベルを持つデータセットを使いたいので、今回は Open Image Dataset 1 を使うことにします。しかしOpen Image Dataset はそのままではDETRでは使えません。元々のDETRはCOCO datasetしか対応していないため、COCO dataset formatに変換する必要がありますので、フォーマット変換方法も合わせて紹介します。
まずはOpen Image Datasetをダウンロードする方法です。2021年時点のOpen Image Datasetの最新バージョンはv6ですが、今回はv4をダウンロードできるツールである OIDv4 ToolKit 7 を利用します。本来は500GB超えのデータセットをダウンロードする必要があるのでColab上だと展開できないですが、ツールを使うことで必要なクラスラベルを必要な枚数だけダウンロードすることができるので、Colab上でも問題なくダウンロードすることができます。
では、早速OIDv4 ToolKitのGitリポジトリをクローンして必要なパッケージをインストールします。requirements.txtだけではバージョンの互換性エラーが出るため、urllib3とfoliumはバージョンを指定して個別にインストールしています。
!git clone https://github.com/EscVM/OIDv4_ToolKit.git !pip install urllib3==1.25.11 folium==0.2.1 !pip install -r OIDv4_ToolKit/requirements.txt
次は、OIDv4 ToolKitを使って画像をダウンロードします。例として、今回は丸いものを物体検出できるようにFine-Tuningするために、「Apple」「Ball」「Balloon」「Clock」「Orange」の5つのクラスラベルを使うことにします。いくつかはCOCO datasetにも存在するクラスラベルですが、「Balloon」などはOpenImage Datasetにしか存在しないクラスラベルになります。ですので、COCO datasetに存在しない「Balloon」を検出できる、かつCOCO datasetに存在して今回の5つのクラスラベルに存在しないクラス(「Umbrella」など)が検出できなくなっていれば、Fine-Tuningは成功していると言えます。
また、ダウンロードに時間がかかるため、今回は各クラスのダウンロード枚数を100枚に設定して行います。(その分精度は落ちてしまうので、ちゃんと精度を出したい場合は全データをダウンロードした方が良いです。)
# ファイル数上限を100枚にするので--limitを指定 !python OIDv4_ToolKit/main.py downloader -y --classes Apple Orange Ball Balloon Clock --type_csv train --limit 100 !python OIDv4_ToolKit/main.py downloader -y --classes Apple Orange Ball Balloon Clock --type_csv validation --limit 100
上記プログラムを実行することで、「/content/OID/Dataset」に画像ファイルがダウンロードされます。また、OIDv4 ToolKitのオプション引数の詳細は公式のGitHub 7 に記載されていますが、今回使ったものについて簡単に解説します。
-y
… 不足しているcsvファイルが必要な時、自動的に「yes」を選択する。--classes
… ダウンロードする対象のクラスラベル(複数指定可能)。指定できるクラスラベルは、公式サイト の「Category」欄のもの。--type_csv
… train、validation、testのいずれかを指定。--limit
… 各クラスラベルのダウンロード上限枚数を指定。
Open Image Datasetをダウンロードできたので、次はアノテーションデータをOpen Image formatからCOCO formatに変換を行います。各フォーマットの詳細は省略しますが、気になる方はkenichiro-yamoto氏の はじめての Google Open Images Dataset V6 や、harmegiddo氏の COCO Formatの作り方 で詳しく解説されていますのでご確認ください。また、GitHubに openimages2coco という変換ツールが公開されていますが、OIDv4 ToolKitを使った場合はうまく動作しませんでしたので、今回は自分で変換プログラムを作成しました。変換プログラムを作成する際には、soumenpramanik氏の「Convert-Pascal-VOC-to-COCO」 を参考にさせていただきました。
import os, json, glob def OID2JSON(OIDFiles, saveName, subset): """ アノテーションをOpenImage format(txt)からCOCO format(json)に変換 Parameters ---------- OIDFiles : string OpenImageDatasetのフォルダパス saveName : string 保存ファイル名(json) subset : string 変換したいtype_csv。train、validation、testのいずれかを指定。 """ attrDict = dict() # categories要素の設定 attrDict['categories'] = [] categories = sorted(os.listdir(os.path.join(OIDFiles, 'Dataset', subset))) for i in range(len(categories)): attrDict['categories'].append({'supercategory': 'none', 'id': i, 'name': categories[i]}) images = list() annotations = list() filenames = list() image_id = 1 anno_id = 1 for category in attrDict['categories']: for jpg_file in glob.glob(os.path.join(OIDFiles, 'Dataset', subset, category['name'], '*.jpg')): filename = os.path.splitext(os.path.basename(jpg_file))[0] # カテゴリ全体で同じファイル名が存在する場合、imageとannoをリネーム if filename in filenames: rename_filename = filename + '_' + str(image_id) os.rename(jpg_file, os.path.join(OIDFiles, 'Dataset', subset, category['name'], rename_filename + '.jpg')) os.rename(os.path.join(OIDFiles, 'Dataset', subset, category['name'], 'Label', filename + '.txt'), os.path.join(OIDFiles, 'Dataset', subset, category['name'], 'Label', rename_filename + '.txt')) filename = rename_filename filenames.append(filename) # images要素の設定 # ※DETRではheightとwidthを使わないので、'none'を設定 image = {'file_name': filename + '.jpg', 'height': 'none', 'width': 'none', 'id': image_id} images.append(image) # annotations要素の設定 anno_path = os.path.join(OIDFiles, 'Dataset', subset, category['name'], 'Label', filename + '.txt') with open(anno_path) as f: for line in f: splitline = line.split(' ') # カテゴリがcategories要素に存在しないバウンディングボックスは使わない if splitline[0] in [d.get('name') for d in attrDict['categories']]: # OpenImageの座標は(xmin, ymin, xmax, ymax)、COCOの座標は(x, y, width, height) x1 = int(float(splitline[1])) y1 = int(float(splitline[2])) x2 = int(float(splitline[3])) - x1 y2 = int(float(splitline[4])) - y1 # areaはピクセル数(float) area = float(x2 * y2) # segmentationは(x1, y1, x2, y2, ...)と順番に定義 segmentation = [[x1, y1, x1, (y1+y2), (x1+x2), (y1+y2), (x1+x2), y1]] annotation = {'iscrowd': 0, 'image_id': image_id, 'bbox': [x1, y1, x2, y2], 'area': area, 'category_id': category['id'], 'ignore': 0, 'id': anno_id, 'segmentation': segmentation} anno_id += 1 annotations.append(annotation) image_id = image_id + 1 attrDict['images'] = images attrDict['annotations'] = annotations attrDict['type'] = 'instances' jsonString = json.dumps(attrDict) with open(saveName, 'w') as f: f.write(jsonString)
上記関数を実行することでOpen Image Datasetのアノテーションを、Open Image format(.txt)からCOCO format(.json)に変換することができます。Open Image formatでは画像ごとにアノテーションデータがtxtファイルに用意されていますが、COCO formatではtrain、validation、testごとに1つのjsonファイルに記載する必要があります。また、クラスラベルをまたぐと同名のファイルが存在するのでリネームしたり、座標の記載方法が違うので変換する等の処理を行っています。詳細はプログラムにコメントを記載していますので、そちらをご覧ください。
それでは上記関数を使って、実際にCOCO formatのjsonファイルを生成します。
OID2JSON('/content/OID', 'custom_train.json', 'train') OID2JSON('/content/OID', 'custom_val.json', 'validation')
これでCOCO formatのアノテーションデータの準備は完了です。
5-2.データセットのディレクトリ構成変更
前章はフォーマット変換を行いました。次は、画像ファイルや前章で作成したアノテーションデータを指定のディレクトリに配置します。元々がCOCO datasetを使うように想定されているため、ディレクトリ構成もCOCO datasetに合わせる必要があるためです。ディレクトリ構成はDETRのGitHubにも記載がありますが、以下のような構成になります。
# path/to/coco/ # ├ annotations/ # JSON annotations # │ ├ custom_train.json # │ └ custom_val.json # ├ train2017/ # training images # └ val2017/ # validation images
それでは、「/content/data」フォルダ内にファイルを移動してみましょう。
import shutil source_train_paths = glob.glob(os.path.join('/content/OID/Dataset', 'train', '**/')) source_val_paths = glob.glob(os.path.join('/content/OID/Dataset', 'validation', '**/')) train_path = '/content/data/custom/train2017/' val_path = '/content/data/custom/val2017/' convert_anno_path = '/content/data/custom/annotations/' # ディレクトリ作成 os.makedirs(train_path, exist_ok=True) os.makedirs(val_path, exist_ok=True) os.makedirs(convert_anno_path, exist_ok=True) # train移動 for source_train_path in source_train_paths: for img_path in glob.glob(os.path.join(source_train_path, '*.jpg')): shutil.move(img_path, train_path) # val移動 for source_val_path in source_val_paths: for img_path in glob.glob(os.path.join(source_val_path, '*.jpg')): shutil.move(img_path, val_path) # anno移動 shutil.move('/content/custom_train.json', convert_anno_path) shutil.move('/content/custom_val.json', convert_anno_path)
上記プログラムを実行すれば、ディレクトリ構成変更作業は完了です。「/content/data/train2017/」以下にクラスラベル関係なく画像ファイルが置かれていて、「/content/data/annotations/」以下に前章で作成したアノテーションファイルが置かれていればOKです。
5-3.Fine-Tuning
前章でデータセットの準備は完了したので、本章ではFine-Tuningについて解説します。まずは必要なパッケージやライブラリをimportします。ここで注意なのが、PyTorchのバージョンが1.9.0以上だとFine-Tuning時にエラーとなってしまいました。ですので、1.8.0にダウングレードを行っています。
!pip install -q torch==1.8.0 torchvision==0.9.0 torchtext==0.9.0 import torch, torchvision import torchvision.transforms as T import matplotlib.pyplot as plt from PIL import Image import requests print(torch.__version__) # 1.8.0 print(torchvision.__version__) # 0.9.0 print(torch.cuda.is_available()) # True torch.set_grad_enabled(False);
次に、COCO dataset以外に対応した、woctezuma氏のDETRをチェックアウトします。ブランチも切り替える必要があるので、finetuneブランチに切り替えています。
%cd /content/ !rm -rf detr !git clone https://github.com/woctezuma/detr.git # ブランチの切替 %cd detr/ !git checkout finetune
「/content」に「detr」というフォルダができていれば成功です。次は、学習済みモデルをダウンロードして、それをベースにFine-Tuningを行います。学習済みモデルは、Model Zooに公開されている「DETR-DC5」を使います。また、このモデルはCOCO datasetで使う想定であるため、COCO datasetのクラス数である92クラスに分類するようになっています。今回は6クラス(Open Image Datasetの5クラス+no-object
) に分類したいため、該当箇所の重みを削除して一から学習する必要があります。その方法は DETRのissues に載っていましたので、そちらを参考にしながらコーディングを進めます。下記ソースコードを実行すると、学習済みモデルのパラメータから不要なパラメータを削除したモデルを 「detr-r50_no-class-head.pth」
というファイル名で保存します。
# 学習済みモデルの取得 checkpoint = torch.hub.load_state_dict_from_url( url='https://dl.fbaipublicfiles.com/detr/detr-r50-e632da11.pth', map_location='cpu', check_hash=True ) # 分類ヘッドの削除 del checkpoint['model']['class_embed.weight'] del checkpoint['model']['class_embed.bias'] # 保存 torch.save(checkpoint, 'detr-r50_no-class-head.pth')
次はいよいよFine-Tuningです。クラスラベルの数を指定する必要があるので、main.pyを実行する前に定義しています。ここで注意ですが、no-object
は除いたクラスラベル数を定義する必要があります。no-object
自体は、DETRのプログラム内で自動的に「num_classes
」と同じIDで採番されるためです。(つまりnum_classes
=5の場合、'Apple’=0, 'Ball’=1, 'Balloon’=2, 'Clock’=3, 'Orange’=4, 'no-object’=5 のようにIDが採番されます。)
Fine-Tuningを行うと処理に時間がかかりますので、とりあえず下記に示すソースコードでは、10epochだけ学習するようにしています。精度が高いモデルを作りたい場合は、OIDv4 ToolKitの「--limit
」オプションを撤廃して画像枚数を増やしたり、「--epochs
」オプションの値を大きくして学習回数を増やしてみてください。その分処理時間がかかってしまいますので、そこはご注意ください。
補足として、筆者が試した場合の処理時間を記載します。また、ColabのGPUはTesla K80が割り当てられている場合になります。
--limit=100
… 1epochに5分程度。--limitなし
… 1epochに1時間弱。
num_classes = 5 %cd /content/detr/ os.makedirs('outputs', exist_ok=True) # 学習 !python main.py \ --dataset_file "custom" \ --coco_path "/content/data/custom/" \ --output_dir "outputs" \ --resume "detr-r50_no-class-head.pth" \ --num_classes $num_classes \ --epochs 1
エラーが発生せず学習が進んでいれば、Fine-Tuningは成功です。しかし、Colabで実行するとGPUの使用制限 8 に引っかかってしまう等の問題があり、筆者が「--limitなし
」でColabで試したところ、7epoch程度しか学習できませんでした。実際にFine-Tuningを何十epochも回す場合は、checkpointとresumeを実装して途中保存しながら学習するか、AWSやGCP等の環境を使った方が良さそうです。
5-4.Fine-Tuning結果の確認
本章では、Fine-Tuningを行ったモデルを使って正しく学習できているか、正しく物体検出できているかを確認します。モデルについては、前章で説明したとおりColabでは厳しかったため、GCPに環境を用意して「--limitなし
」かつ50epoch程度学習したモデルを使って結果を確認します。モデルは「/content/detr/outputs」に配置されているものとし、解説していきます。
まずはモデルのlossとmAPを確認してみます。mAPは物体検知モデルに使われる評価指標であり、mAPが高いほどモデルの精度が高く常に自信があるモデルといえます。本記事ではmAPについて解説はしませんが、Jonathan Hui氏の 「mAP(mean Average Precision) for Object Detection」 という記事が非常に分かりやすかったので、興味のある方はこちらをご確認いただければと思います。lossとmAPの確認方法ですが、DETRでは既に確認用プログラムを作ってくださっているので、そちらを使って確認していきます。
from util.plot_utils import plot_logs from pathlib import Path %cd /content/detr/ log_directory = [Path('/content/detr/outputs')] # 実線 ... トレーニング結果(train_loss) # 破線 ... 検証結果(val_loss) fields_of_interest = ( 'loss', 'mAP', ) plot_logs(log_directory, fields_of_interest)
上記プログラムを実行すると、下画像のようなグラフが出力されます。lossを見てみると、train_loss
は学習が進むにつれて下がっているので正しく学習できていそうですし、val_loss
も上がっていないので過学習にもなっていないことが分かります。mAPを見ても、学習が進むにつれて右肩上がりになっています。正しく学習できていますが、まだ収束していないので、もう少し学習を回した方が良かったかもしれません。
次は、実際に画像を入力して物体検出をしてみます。最初にモデルをロードします。Fine-TuningしたモデルとFine-Tuningしていないモデルを比較したいので、2種類のモデルをロードしておきます。Fine-Tuningしていないモデルに関しては、第1回目の記事 で使用したモデルを使います。
finetuned_model = torch.hub.load('facebookresearch/detr', 'detr_resnet50', pretrained=False, num_classes=num_classes) checkpoint = torch.load('/content/detr/outputs/checkpoint.pth', map_location='cpu') finetuned_model.load_state_dict(checkpoint['model'], strict=False) finetuned_model.eval() original_model = torch.hub.load('facebookresearch/detr', 'detr_resnet50_dc5', pretrained=True) original_model.eval()
モデルの準備が完了したので、次は実際に画像を入力して物体検出を行うための前処理や画像表示処理を定義します。なお前処理等は、第1回目の記事 で紹介した内容と同じなので、詳細な解説は前回の記事を見てください。
# 可視化用クラスラベル oid_labels = [ 'Apple', 'Ball', 'Balloon', 'Clock', 'Orange', ] coco_labels = [ 'N/A', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'N/A', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'N/A', 'backpack', 'umbrella', 'N/A', 'N/A', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'N/A', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'N/A', 'dining table', 'N/A', 'N/A', 'toilet', 'N/A', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'N/A', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush' ] # 可視化用COLOR COLORS = [[0.000, 0.447, 0.741], [0.850, 0.325, 0.098], [0.929, 0.694, 0.125], [0.494, 0.184, 0.556], [0.466, 0.674, 0.188], [0.301, 0.745, 0.933]] # 標準的なPyTorchのmean-std入力画像の正規化 transform = T.Compose([ T.Resize(800), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def box_cxcywh_to_xyxy(x): """ (center_x, center_y, width, height)から(xmin, ymin, xmax, ymax)に座標変換 """ # unbind(1)でTensor次元を削除 # (center_x, center_y, width, height)*N → (center_x*N, center_y*N, width*N, height*N) x_c, y_c, w, h = x.unbind(1) b = [(x_c - 0.5 * w), (y_c - 0.5 * h), (x_c + 0.5 * w), (y_c + 0.5 * h)] # (center_x, center_y, width, height)*N の形に戻す return torch.stack(b, dim=1) def rescale_bboxes(out_bbox, size): """ バウンディングボックスのリスケール """ img_w, img_h = size b = box_cxcywh_to_xyxy(out_bbox) # バウンディングボックスの[0~1]から元画像の大きさにリスケール b = b * torch.tensor([img_w, img_h, img_w, img_h], dtype=torch.float32) return b def filter_bboxes_from_outputs(outputs, threshold=0.7): # 閾値以上の信頼度を持つ予測値のみを保持 probas = outputs['pred_logits'].softmax(-1)[0, :, :-1] keep = probas.max(-1).values > threshold probas_to_keep = probas[keep] # [0, 1]のボックスを画像のスケールに変換 bboxes_scaled = rescale_bboxes(outputs['pred_boxes'][0, keep], im.size) return probas_to_keep, bboxes_scaled # 結果の表示 def plot_finetuned_results(pil_img, prob=None, boxes=None, labels=None): plt.figure(figsize=(16, 10)) plt.imshow(pil_img) ax = plt.gca() colors = COLORS * 100 if prob is not None and boxes is not None: for p, (xmin, ymin, xmax, ymax), c in zip(prob, boxes.tolist(), colors): ax.add_patch(plt.Rectangle((xmin, ymin), xmax-xmin, ymax-ymin, fill=False, color=c, linewidth=3)) cl = p.argmax() print(labels, p) text = f'{labels[cl]}: {p[cl]:0.2f}' ax.text(xmin, ymin, text, fontsize=15, bbox=dict(facecolor='yellow', alpha=0.5)) plt.axis('off') plt.show() # 物体検出 def run_worflow(my_image, my_model, labels, threshold=0.7): # mean-std入力画像の正規化(バッチサイズ : 1) img = transform(my_image).unsqueeze(0) # モデルに反映 outputs = my_model(img) probas_to_keep, bboxes_scaled = filter_bboxes_from_outputs(outputs, threshold=threshold) plot_finetuned_results(my_image, probas_to_keep, bboxes_scaled, labels)
これで物体検出の準備ができました。それでは、実際に物体検出をしてみましょう。試しに、Open Image DatasetからBalloonの画像をダウンロードして物体検出してみます。Fine-Tuningしたモデルでは検出できて、Fine-Tuningしていないモデルでは検出できないはずです。また、閾値は0.9を指定しています。
from io import BytesIO url = 'https://farm7.staticflickr.com/52/106887535_a29c34113b_o.jpg' response = requests.get(url) im = Image.open(BytesIO(response.content)) # Fine-Tuningモデルで物体検出(閾値0.9) run_worflow(im, finetuned_model, oid_labels, 0.9) # Fine-Tuningしていないモデルで物体検出(閾値0.9) run_worflow(im, original_model, coco_labels, 0.9)
実行すると、下画像のような結果が表示されます。左側がFine-Tuningしたモデル、右側がFine-Tuningしていないモデルです。想定通り、Fine-Tuningした場合はBalloonが検出できていますが、Fine-Tuningしていない場合は検出できていません。
Open Image DatasetやCOCO datasetの画像を使い色々なパターンを試してみたので、以下に結果を列挙します。上記画像と同じく、左側がFine-Tuningしたモデル、右側がFine-Tuningしていないモデルとなります。Fine-Tuningで学習しているりんごやオレンジ、アナログ時計に関してはFine-Tuning前と同程度の検出精度ですが、学習していない傘や人や犬、デジタル時計などは検出しないようになっていて、正しくFine-Tuningできていることが分かるかと思います。
6.おわりに
今回の記事では、前半ではDETRの詳細と自然言語処理とのTransformerの違いを解説し、後半はDETRでのFine-Tuning方法と結果を確認しました。DETRのFine-Tuning方法を紹介している記事が無さそうでしたので、誰かのお役に立てれば幸いです。また、DETRだからこそできる物体検出があるわけではなく、既存のFasterR-CNNと同程度の性能ですので、案件への応用例などは既存手法と変わりません。しかし、既存手法と全く違うアーキテクチャでありながら、既存手法と同程度の性能が出せることが非常に素晴らしいので、今後DETRをベースにした手法が出てくることでしょう。
今回でDETRの記事は終了です。次回の記事では、DETRと自然言語処理で使われるRoBERTaを組み合わせた、「MDETR」9 を紹介予定です。画像とテキストを混ぜたマルチモーダル推論モデルで、両方ともTransformerを使うため既存手法よりも自由度が高い推論ができるものになっています。
-
余談ですが、2021年3月にGoogleから「Attention is not all you need」 という論文が公開され、Attention以外の処理も重要であることが言及されています。興味のある方はこちらの論文もご確認ください。 ↩