エクサウィザーズ Engineer Blog

株式会社エクサウィザーズのエンジニアチームブログ

機械学習エンジニア、あるいはデータサイエンティストの選考に関する徒然

こんにちは! 構造化データグループのグループリーダー小林広明です。 今回は表題について、いくつか資料の紹介と私なりに思うところを少し書いていきます。

免責事項

弊社の選考基準について書いたものではありません。エクサウィザーズの他の面接官は異なる意見を持っていると思います。 ただし、私も書類選考や面接に関わっていて、その視点は入っています。

私は AI Frontier 部に所属していて、こちらのメンバーには基本的に機械学習エンジニアという職名を用いています。 ですが、特に私が所属している表形式データを主に扱うグループでは、一般にデータサイエンティストと呼ばれている職種が担う仕事も多く扱っていると思われるので、この記事では機械学習エンジニア・データサイエンティストの違いには触れずに書いていきます。*1

どちらかといえば中途採用(経験者)での転職希望者に向けて書いていますが、未経験者や学生、あるいは面接官にも参考になる部分があるのではないかと願っています。

応募書類/履歴書

今年の2月頃に一部界隈で話題になっていた、Chip Huyen が書いた「What we look for in a resume」というタイトルのブログ記事は共感できる内容でした。その記事で扱っていた項目の内2つについて以下に書きます。

We look for demonstrated expertise, not keywords (キーワードではなく、実証された専門性を求めています)

我々の職種に対する応募書類には技術的なキーワードが多く書かれていることがあります。 求人票もキーワードまみれになっている場合があって、一概に応募者側を非難することはできないですが、書類に書くキーワードは厳選して紐づく内容を充実させる方が好印象です。

私は面接時間の大半で、統計的機械学習やデータ分析に関して、応募者が時間を掛けて取り組んだことや得意なことをご説明いただいています。 書類時点で把握できる内容が多いほど、応募者に相応しい質問を考えることができますし、応募者が我々に対して質問する時間を確保しやすくなります。

守秘義務によって仕事内容を詳しく書けないこともあると思いますが、適度に抽象して書いていただければ幸いです。

We care about impact, not meaningless metrics (無意味な指標ではなく、インパクトを重視します)

教師あり学習ではテストデータに対する予測精度を向上させることを目標としています。 しかしながら、実際は予測することそのものが目的になっている場合は少ないのではないでしょうか。何らかのシステムの一部になっているか、意思決定の材料の一つであるとすれば、それらの目的に対して適っていたかが重要です。

上位の目的に対するインパクトは定量化しづらいこともあります。それでも数値指標で安易に代替するより、自分が行った学習や分析などの価値に向き合った過程や結果について記された書類は惹かれるものがあります。その上で、例えば目的関数(≒ 損失関数+モデル)をどのように設計・採用したかまでがストーリーになっていると最上に近いです。

これらについて書かれた「What we look for in a resume」はぜひ多くの人にご覧いただきたいです。

面接

面接について書かれた本を紹介します。

著者は先に紹介したブログ記事の書き手と同じ Chip Huyen です。本書は二部構成となっており、第一部では機械学習に関する職種の面接プロセス(オファー後の交渉含む)、企業における機械学習の役割、各役割が必要とするスキル、よく聞かれる質問の種類、それらにどのように備えるかについて説明しています。第二部では、機械学習の重要な概念と一般的な誤解に関する200以上の知識問題が含まれています。まだ未完だと思われますが、凡そのコンテンツは揃っています。

著者はスタンフォード大学で機械学習システムデザインの講義を開いていて、その講義をもとにした本日本語訳が今年の9月に出版予定)も書いていますが、本書を読んだ後に機械学習システムデザインに関する 27 問の質問にトライすることを勧めています。実際の採用面接でこのような質問をすることを私はしないですが、一問一答形式の勉強だけでなく総合的な現場の課題に近い問題を考えることは様々な立場の人にとって有用だと思います。

本書の付録には The zen of interviews (面接の禅)がついています。自らの戒めにしつつ、こちらでも共有します。

  • The goal of the hiring process is to hire people, not to eliminate them.(採用プロセスの目標は人材を採用することであり、排除することではありません。)
  • Interview questions should be tailored for each candidate. Companies who ask different candidates for the same role the same questions are process-driven, not people-driven.(面接の質問は候補者ごとに調整する必要があります。同じ役割の異なる候補者に同じ質問をする企業は、人主導ではなくプロセス主導です。)
  • Standardized interview questions lead to standardized answers which, in turn, leads to standardized people.(標準化された面接の質問は標準化された回答につながり、それが標準化された人々につながります。)
  • If a piece of knowledge is easy to acquire, it’s not worth testing for.(簡単に得られる知識であれば、テストする価値はありません。)
  • Ask questions with multiple hurdles. After giving the candidate a hint, the question becomes slightly easier, but there’s still room for the candidate to show off their skills.(複数のハードルを持った質問をしましょう。受験者にヒントを与えた後、質問は少し簡単になりますが、受験者が自分のスキルを披露する余地はまだあります。)
  • Ask hard questions. Extraordinary candidates can write up a simple solution within an hour, and there’s no upper bound on how much a candidate can improve the solution. Asking hard questions shows that you think about and work on hard problems, which attracts people who want to solve hard problems.(難しい質問をしてください。並外れた候補者は 1 時間以内に簡単な解決策を書き上げることができ、候補者がその解決策をどれだけ改善できるかに上限はありません。難しい質問をするということは、あなたが難しい問題について考えて取り組んでいることを示し、それが難しい問題を解決したい人を引き寄せます。)
  • A good interview is a question that turns into a conversation.(良い面接とは、質問が会話に変わることです。)
  • For long-term hires, interviews should focus on critical thinking and problem-solving skills instead of techniques. Techniques change over time, but critical thinking and problem-solving skills will always be relevant.(長期雇用の場合、面接ではテクニックではなく批判的思考と問題解決スキルに焦点を当てるべきです。テクニックは時間の経過とともに変化しますが、批判的思考と問題解決スキルは常に役に立ちます。)
  • Both interviewers’ and candidates’ time should be respected.(面接官と候補者の時間は双方とも尊重されるべきです。)
  • Interviews should be transparent. For each interview, not only the final evaluation but also the log of the questions and answers should be submitted.(インタビューは透明性のあるものでなければなりません。毎回の面接では、最終評価だけでなく質疑応答の記録も提出する必要があります。)

二冊目は深層学習に関する面接問答集で、先の本の後半をより詳細にしたイメージの本です。内容の大枠は以下のような感じです。

  • 数学的な基礎: 情報理論、微積分、自動微分、ベイズの定理など
  • 機械学習の基礎 (ロジスティック回帰、アンサンブル学習など)
  • 深層学習の基礎 (ニューラルネットワーク、ベイズ深層学習など)
  • 深層学習の応用 (CNN からの特徴量抽出がメイン)

基礎的な話題が多く、最近の生成AIはもちろん発展的な深層学習の話題についてかなり物足りない感じがしますが、構造化データグループではむしろこのような理解がしっかりされている方を好ましく思っています。面接中にいきなり問題として出すというより、取り組まれたこと/勉強されたことの技術的理論的な理解について、基礎的な質問をすることもあります。一通り勉強した後のチェックとしてご活用いただく際の素材一例として紹介しました。

終わりに

この手の記事を書くのはブーメランになって自身に返ってくるようで、プレッシャーがあります。面接官や候補者どちらの立場になっても、なるべく良い時間を過ごせるように私も精進が必要だと感じたことが、記事を書いた一番の収穫な気がします。

もしエクサウィザーズの採用情報にご興味がございましたら、以下のページをご覧ください。

recruit.exawizards.com

最後に、この記事を読まれた方に深く感謝します。

*1:こんなタイトルの記事を書いておいてなんですが、各社・各人における仕事内容の分散が大きいため、機械学習エンジニアとデータサイエンティストの分類モデルを作るような行為は有用性が低いと思っています。

Zero-shot Learning網羅的サーベイ:CLIPが切り開いたVision & Languageの新しい世界

こんにちは! 画像システムグループで機械学習エンジニアをやっている小島です。

この記事では、今ホットな「Zero-shot Learning」と「Vision & Language」に関する最新情報を、CLIPという研究を起点として網羅的にサーベイをしていきます。このために論文1000本に目を通し、70本程度を記事にしました。

Zero-shotやVision & Languageは、Stable Diffusionに代表される画像生成AIとも密接に関連している技術です。この記事を通して、Vision & Languageの奥深い世界を体感できるでしょう。

注意事項

この記事は非常に長いため、全部読むのに1時間以上かかる可能性があるので、休憩を取りながら、または必要な部分だけ読んでください。各セクションを個別に読んでも問題ありません。

また、文章中の画像は、特別な記載がない限り、引用元の論文からのものです。

目次

はじめに

CLIPとは

以前のブログでも触れましたが、CLIPは、2021年にOpenAIによって発表された研究です。GPT-2やGPT-3のゼロショット学習と同様に、タスクに特化した最適化をせず、「画像が与えられたときに、最も類似度の高いテキストを選択することで、分類問題を解く」というモデルです。訓練データがなくても画像分類ができるという画期的なフレームワークです。

これをよしなにできるフレームワークも存在し、例えば、Hugging Faceのtransformersというライブラリを使えば、公式ドキュメントより引用

from PIL import Image
import requests

from transformers import CLIPProcessor, CLIPModel

model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)

inputs = processor(text=["a photo of a cat", "a photo of a dog"], images=image, return_tensors="pt", padding=True)

outputs = model(**inputs)
logits_per_image = outputs.logits_per_image  # this is the image-text similarity score
probs = logits_per_image.softmax(dim=1)  # we can take the softmax to get the label probabilities

このような簡単なコードで、「犬か猫か」を分類できます。

CLIPの革新性は、訓練データが不要なだけでなく、「画像と言語の分散表現を学習する」ことで応用範囲が格段に広がる点です。従来の画像だけの学習では、訓練データセット内の限られた表現しか学習できませんでした。しかし、言語モデルの豊富な表現力を利用することで、ゼロショット転移性を持つ(訓練データにない分布にも適応できる)ようになりました。これが非常に強いのです。これにより、さまざまな画像認識タスクがゼロショット方面に拡張されます。

CLIPは現在のVision & Languageの研究の基礎となっており、多くのモデルで活用されています。最近の話題の生成モデルや基盤モデルも、CLIPの影響を大きく受けているものが多数あります。

この記事の目的

この記事では、CLIPの後続の研究をひたすらサーベイし、CLIPの遺伝子がどのように受け継がれているかを見ていきます。CLIPを追跡することで、現在のVision & Languageの最先端の動向を理解することができるでしょう。論文の抽出は以下の手順で行いました。

  • Google ScholarでCLIPを引用している約1000本の論文を抽出
  • 各論文をざっと見て、「面白そうなもの」や「特にコードが付いているもの」を選ぶ
  • その中から約70本を紹介

論文の選択は2022年末に行ったため、生成モデルなどの情報が少し古い部分があることをご了承ください。

下流タスクの用語について

この記事では、「下流タスク(Downstream task)」という表現が何度も登場します。これは大規模モデルでよく使われる言葉で、例えば、CLIPのような大規模な事前訓練を行ったモデルがあると仮定して、その上で「犬か猫かを分類する分類器を作りたい」とします。

この「犬か猫かを分類する」タスクが下流タスクです。「事前訓練と対比して言っている」ということを頭の片隅に入れておいてください。

参考資料

NTT人間情報研究所・西田さんの「NLPとVision-and-Languageの基礎・最新動向」シリーズが詳しくておすすめです。こちらもあわせて読むと楽しめると思います。

CLIPの効果と画像生成モデル

CLIPの効果が顕著に現れているのは、「画像生成モデル」です。画像生成モデルとは、最近ではMidjourneyやStable Diffusionなどが有名です。Stable Diffusion(v2.1時点)の構成モデルの1つにCLIPが含まれています。

CLIPが登場する前の画像生成モデル(例:StyleGAN2)

CLIPが登場する前の画像生成モデルは、クローズドな設定(訓練データに含まれる特定ドメインの画像しか生成できません)でした。例えば、現実の顔写真のデータセットで訓練されたモデルは、「スターウォーズのストームトルーパーの顔」のような訓練データにはない画像や、「宇宙空間にいるオバマ」のような現実にはありえない画像を生成することはできませんでした。これはStyleGAN2の論文からです。

これは、FFHQという顔写真データセットから生成された画像です。非常にきれいな顔写真が生成できていますが、顔以外の画像を生成したい場合は、別のデータセットで再び訓練する必要があります。

後に登場したStyleGANとCLIPを組み合わせたモデル(StyleGAN-NADA)では、訓練データ以外のドメインでも画像を生成できるようになり、この問題が解決されました。あくまでこれはCLIPが登場した後のことです。

CLIPが登場した後の画像生成モデル(例:BigSleep、DALL-E 2)

CLIPが登場してからは、「訓練データに含まれる特定ドメインしか生成できない」という制約がなくなり、現実にはありえない画像も生成できるようになりました(オープンな設定)。

かつて、BigGANはStyleGANと双璧をなす強力なモデルでしたが、あくまでクローズド設定な場合でした。BigGANにCLIPを組み合わせたBigSleepというライブラリを使うと(リポジトリからの引用です)、オープンな設定になります。

この画像は、「廃墟の街に浮かぶ風船」というプロンプトで生成されました。画像生成をやっている方ならピンとくると思いますが、画像生成を行う際、どのような画像を作りたいかを示すテキストを「プロンプト」と呼びます。「廃墟に風船が浮かんでいる光景」というのは実世界に存在するのでしょうか? 「訓練データに含まれる特定ドメインしか生成できない」という縛りが外れたことで、CLIPが学習している概念であれば、画像として生成できるようになりました。

次に、2022年にOpenAIが発表したDALL-E 2で生成された画像を見てみましょう(ホームページからです)

「馬に乗った宇宙飛行士」という現実的にはありえない光景が、高画質で生成されています。これが可能になった理由は、言語モデルが持つ幅広い概念や深い理解を、画像生成に活用しているからです。「馬」「宇宙飛行士」「廃墟」「風船」などの個々の概念は、CLIPが学習済みで、生成モデルの訓練データにもおそらく含まれています。プロンプトに基づいて、「宇宙で馬に乗っている宇宙飛行士」のような新しい概念を理解し、生成できるのが、CLIP以降の画像生成モデルの大きな進歩です。

「いや、ちょっとまって。CLIPが強力だからじゃなくて、拡散モデルが強力だからじゃないの?」と思うかもしれません。確かに、DALL-E 2は拡散モデルをベースにしており、これは画質向上の点では非常に大きいです。しかし、BigSleepのようなGANベースのモデルでも、訓練データに含まれる特定ドメインの外側の生成、つまりゼロショット生成ができています。つまり、拡散モデルとゼロショットの両方のキーパーツが揃って、今日の高画質な画像生成が実現されているのです。

DALL-E 2Imagenのような最近の画像生成のホームページを見ると、「馬に乗った宇宙飛行士」や「タイムズスクエアで自転車に乗るコーギー」のような現実にはありえない画像が多数掲載されています。なぜこのようなパフォーマンスをしているかというと、絵としての単純な面白さもあるものの、「ゼロショットの画像生成がすごい」という技術的なアピールをしたいという意図があると私は理解しています。

以下では、「分類」「物体検出」といった個別のタスクについて、簡単に論文を紹介していきます。

分類

分類モデルは、純粋なCLIPの拡張です。かなり多くの研究が出ています。まずは、CLIPと同じ時期に登場したALIGNを紹介し、その後、下流タスクの適用についても説明します。

ALIGN

同じくゼロショット分類の研究でよく引用されるのが、ALIGNという手法です。ALIGNは、「ALIGN: A Large-scale ImaGe and Noisy-text embedding」の略で、大量の画像とノイズの多いテキストデータを使った対照学習(Contrastive Learning)を行います。CLIPと非常に似ていますが、CLIPが先に発表され、ALIGNはその後続となります。

ALIGNとCLIPの主な違いは、ALIGNが10億を超えるノイズの多いalt-text(画像のalt属性のテキスト)を使った対照学習であることです。一方、CLIPは4億枚のデータセットを使っており、ALIGNと比べて規模が小さいです。また、データ収集方法も異なり、CLIPはWikipediaの頻出単語をクエリとしてクロールするというトップダウンアプローチです。ALIGNは、画像のalt-textというノイズの多いデータを、高価なフィルタリングなしで使ったボトムアップアプローチです。ALIGNの主張は、ノイズの多いデータでも、Image-EncoderとText-Encoderを使った対照学習がうまく機能することです。

最近では、特にLAION-2B, 5Bが登場したことで、データセットの大きさに関係なく「CLIP」と呼ばれることが多くなっています。しかし、ノイズの多いデータセットでもCLIPの手法が有効であることが分かったのは、ALIGNの貢献と言えるでしょう。

プロンプトエンジニアリング

2023年現在、プロンプトエンジニアリングは、ChatGPTやGPT-4を使っていかに有用な回答を引き出す方法や、Stable Diffusionを用いていかに美しい画像を生成する方法(呪文)として一般的に認識されています。しかし、プロンプトエンジニアリングの有効性は、オリジナルのCLIP論文からすでに指摘されていました。

CLIPにおけるプロンプトエンジニアリングはもっとプリミティブで、テンプレート構文です。OpenAIが公開しているJupyter Notebookがわかりやすいですが、単に「a photo of {class_name}」というプロンプトだけで分類するよりも、

  • a bad photo of a {class_name}.
  • a photo of many {class_name}.
  • a photo of the hard to see {class_name}.
  • : :

のようなテンプレート構文をText Encoderへ大量に入力し、それらのEmbeddingの平均をとって分類することで、「a photo of {class_name}」のような単純なプロンプトよりも高い分類精度が得られることが報告されています(CLIPの論文より)。

プロンプトエンジニアリングを使うか使わないかで比較すると、同じ精度を達成するために、計算量が4倍も必要なことが驚きですね。

プロンプトチューニング

CoOp

プロンプトエンジニアリングは、GPTやStable Diffusionと同じく、手動での泥臭い調整が必要です。ここでの焦点は、「下流タスクの少量データセットで、どのようにプロンプト設計すれば精度を上げられるか?」や「下流タスクで、Zero-shotからFew-shotにスケールできるか?」といった問題です。これに対して、学習ベースのアプローチが多く提案されています。

その中でも基本的なものが、プロンプトチューニング(Prompt Tuning)という手法です。これは、下流タスクのプロンプト表現を学習ベースで行う方法です。

これはプロンプトチューニングベースの手法の走りである「CoOp」という方法です。CoOpは、プロンプトエンジニアリングを計算ベースで行い、学習可能なコンテクスト(Learnable Context)をプロンプトの表現に追加することで、手動のプロンプトやLinear-Probeよりも優れた下流タスクの性能が得られると報告されています。Linear-Probeとは、CLIPのImage-Encoderを固定し、末尾に線形回帰を追加して、特徴量ベースのロジスティック回帰を行う方法です。

CoOpでは、画像やテキストのモデルのパラメータは固定され、プロンプトのコンテクストキーワードだけが学習されます。コンテクストキーワードと言われるとあまりイメージがわかないですが、実際はベクトルの値です。

DualCoOp

CoOpの発展形としてDualCoOpがあります。この手法では、学習に使うコンテクストキーワードをポジティブとネガティブの2種類用意します。

ネガティブなプロンプトを用意することは、Stable Diffusionのようで直感的に理解しやすいです。DualCoOpは、もともと「アノテーションが限られた状況でのマルチラベル問題」に対処するために開発されました。アノテーションが限られるとは、ラベルが「ポジティブ、ネガティブ、不明」のようにざっくりとした状況を指します。DualCoOpは、正負両方のコンテクストを使って、誤検出を減らしながら新しい画像のラベルを正確に予測し、精度向上に貢献しています。

Visual Prompt

「CoOpがテキスト側のプロンプトチューニングするなら、画像側のプロンプトチューニングもすればいいじゃん」というのがVisual Prompt Tuningです。Jia et al.Bahng et al.がありますが、ここでは後者を紹介します。前者の解説記事はcvpaper.challengeが詳しいです。

Visual Promptとは、画像の外枠にノイズのようなものを追加し、それをプロンプトとするものです。プロンプト部分だけを学習ベースで最適化していく方法です。また、この論文の後半の議論が面白く、衛星画像(EuroSAT:CLIPのZero-shot精度が低いことで知られる)に1ピクセルの赤い点をVisual Promptとして追加するだけで、精度が3%も向上したと報告されています。この結果は、Unadversarial ExamplesやDomain Adaptationとの関連性が議論されている点が興味深いです。

TPT

テスト時にラベルを使わずにプロンプトチューニングを行う方法があります。Test-Time Prompt TuningTPTという手法です。

テスト時にData Augmentationをすることを「Test-Time Augmentation」と言いますが、そのアナロジーで理解するとわかりやすいでしょう。TPTでは、テストデータ全体に対してAugmentationを適用し、複数のビューを作成した上で、画像の特徴を抽出します。その後、信頼度でフィルタリングし、エントロピーを最小にするようにプロンプトを最適化します。このエントロピー最小化では、正解のラベルは必要ありません。信頼度に基づく閾値はハイパーパラメータとして設定されます。

MaPLe

プロンプトチューニングは、画像やテキストだけでなく、マルチモーダル(複数のモード)にも適用できます。Multi-modal Prompt LearningなのでMaPLeです。

この図に示すように、クロスモーダル(異なるモード間)でプロンプトを学習させる際には、ネットワーク間でレイヤーごとに密接なつながりを持たせます。これにより、画像と言語のプロンプトが強く結びつき、相乗効果が生まれます。その結果、各モダリティごとに独立したプロンプトを学習させず、下流タスクの精度を向上させることが目的です。

UPL

教師なしでプロンプトラーニングを行う方法として、Unsupervised Prompt Learning(略してUPL)という手法があります。

UPLでは、ラベルのないデータとプロンプトを使って、擬似ラベル(Pseudo Label)を生成します。その中から信頼度が高いTopKのサンプルを選び、プロンプトの表現を学習させます(プロンプトチューニング)。この信頼度の高い疑似ラベルを使ってプロンプトを学習させることが、教師なしを可能にしたポイントです。研究では、11のデータセットに対して、CLIPのゼロショット精度が59.18%から、追加のラベルなしで63.38%まで向上したことが報告されています。さらに、CLIPのプロンプトエンジニアリングと組み合わせることで、精度が68.37%まで向上し、CoOpやTip-Adapterの8-shotと同等の結果が得られたとのことです。

Adapter

これまでプロンプトのチューニング方法を見てきましたが、最近ではAdapterという別の方法が主流になっています。Adapterは、ネットワークを固定し、新しい層を追加してその層だけを訓練する方法です。一部の層だけを訓練するという考え方は、プロンプトチューニングに似ていますが、Adapterでは訓練箇所がプロンプトの表現に限られません。

このAdapterの良い例が、「LoRA」という手法で、Stable Diffusionで特定のデザインを生成したり、ローカルで動作するLLM(例:Alpaca、LLaMA)を任意の言語に適用するために使われます。LoRAはファインチューニングに似ていますが、追加された層だけを訓練するため、訓練時間が短く済みます。

CLIP-Adapter

CLIP-Adapterは、各エンコーダーの最後にブランチのボトルネック構造のレイヤーを追加し、その部分だけを訓練する方法です。コードを見ると分かる通り、Adapterの構造は非常にシンプルで、Image-Encoderの最後にResNetのSkip Connectionだけが追加されてた形です。

この方法では、元の事前学習済みの特徴と、Skip Connectionによる特徴をブレンドすることで、CoOpよりもシンプルなモデル構造が実現され、少数のデータでの下流タスクの精度が向上することが報告されています。そのため、CLIPにおいてプロンプトチューニングの代わりとして、Adapterが注目されるようになります。

SVL-Adapter / Tip-Adapter

CLIP-Adapter以降、多くの「○○-Adapter」という手法が登場しました。ここではSVL-AdapterTip-Adapterを紹介します。

SVL-Adapterは、画像側に自己教師あり学習を追加したものです。これは、CLIPの訓練データセットに含まれていないデータ(例:衛星画像や熱/モーショントリガーカメラの画像)を扱いたいためです。これらの分類は「ローショット分類」と呼ばれ、ファインチューニングに使える画像が限られています。ローショット分類は、CLIPによる分類の信頼性が低く、インターネット上にデータが少ない困難な状況です。

SVL-Adapterを使うと、ローショット領域で既存手法よりも10%精度が向上することが報告されています。さらに、ハイパーパラメータの自動選択方法も提案されています。

一方、Tip-Adapterは、Few-shot学習データからKey-Value Cacheモデルを使ってAdapterを構築する手法です。Keyには訓練データのImage Encoderの特徴量、Valueにはクラスラベルが格納されます。推論時には、特徴量の検索を行い、CLIPの事前知識を更新します。訓練データのキャッシュのみを行っているため、従来のAdapterとは異なり、訓練(バックプロパゲーションを伴う)が不要です。ハイパーパラメータに敏感な欠点がありますが、精度は良く、計算量やデータ数が限られた状況では有効です。

Kronecker Adaptation

最後に、少し異なるタイプのアダプターを紹介しましょう。これはKronecker Adaptation(KAdaptation)という方法で、Stable Diffusionでよく用いられるLoRAの改良版にあたります。実は、これもCLIPの文脈で使用されています。最近話題の画像生成の裏に、CLIPがちらほら登場するのが面白いですね!

Kronecker Adaptationは、LoRAをクロネッカー積にしたものです。左の図は、20個の下流タスクのデータセットに対するCLIPの適応精度の平均を示しています。縦軸は精度で、横軸は訓練パラメーターの数です。LoRAやLinear-Probeよりも、計算量と精度の両方で効率的であることが示されています。

画像生成では、LoRAの代わりに最近LyCORISというOSSが使われるようになりましたが、これはアダマール積ベースの手法です。LoRAの改良で、アダマール積を使うか、クロネッカー積を使うかという、親戚のような関係が興味深いです。

ネットワークバイパス

ここでは、ネットワークバイパス技術の進化を見ていきます。これらは一種のAdapterと考えることができますが、画像分類ネットワーク全般に適用可能であるため、CLIPとは関連性が薄い部分もあります。

Convpass

この研究では、ViT(Vision Transformer)の各レイヤー間に畳み込みブロックのSkip Connectionを追加することで、精度が向上することが示されています。この技術は、ConvのバイパスなのでConvpassと呼ばれています。位置付けとしては、LoRAや後述のAdaptFormerの改良版となります。

この方法では、Multi-Head Self AttentionやMLPの間にCNNモジュールをバイパスさせます。ViTの各層の間にCNNを挟むことで精度が向上する理由として、論文では「画像指向のネットワーク設計により、新たな帰納バイアスが生じ、効率性が向上した」と述べられています。

ViTが導入された当初は、「CNNは帰納バイアスがハードコーディングされている」という悪い意味で評価され、「ViTは帰納バイアスから解放されている」と良い意味で評価されることが多かったです。しかし、特に少量のデータを用いた転移学習(Adapter)の場合、CNNのようなハードコーディングされた帰納バイアスはむしろ良い方向に働くことが示されており、Convpassのように帰納バイアスを強化することで、下流タスクの効率性が向上することが示唆されています。

Conv-Adapeter

Conv-Adapterは、ResNetやConvNextなどのCNNに適用できるAdapterです。Depth-wise ConvやPoint-wise ConvといったブランチをAdapterとして追加することで、訓練パラメーターを削減しながら、Fine-tuningと同等の精度を達成できることが報告されています。

ConvpassはViTに対応したAdapterでしたが、Conv-AdapterはCNN向けのアダプターです。Adapterに使用されるモジュールは、逆に似たような機能を持っているのが興味深い点です(個人的にはConv-AdapterがMLPだった場合は気になります)。

AdaptFormer

AdaptFormerは先ほど紹介したConvpassの前にあたる研究です。ViTの中間層にMLPのブランチを追加しています。

特に動画データセット(行動認識)で、従来のFull-tuningよりも高い精度を達成しています。画像データセットでは、AdaptFormerを追加してもFull-tuningと同程度の精度でしたが、動画データセットではAdaptFormerによる精度向上が顕著でした。ただし、Full-tuningはViTのみで、時系列ネットワークは含まれていないことに注意が必要です。AdaptFormerによって、動画分類で「動きの理解」が向上している可能性があります。

学習パラメーター数は、ブランチ部分のみ訓練するため少なくなります。比較対象として、Visual Prompt Tuningが挙げられており、動画データセットでも機能することが興味深い点です。

PMF

PMFは、Few-shot Learningに焦点を当てた研究です。この研究では、事前学習データ、ニューラルネットワークの構造、メタ学習の観点から実験的に比較が行われています。

研究では、事前学習→メタ学習→ファインチューニングというシンプルなステップを用いて、従来のFine-tuningよりも大幅に上回る精度を達成しています。Few-shot Learningでは、CNNよりもViTが優れていることが報告されています。特に、外部データを用いた事前学習やメタ学習が行われている場合には、その優位性が顕著です。

モデルマージ

Stable Diffusionでは、好みの絵柄を出力するためにモデルマージすることが運用レベルで行われています。実際に、CLIPベースの研究でも似たようなアプローチが既に行われています。個人的な意見ですが、経験的に行われているDiffusionモデルマージの理論的根拠は、この辺りにあるのではないかと考えています。

CLIPにおけるモデルマージの動機はもっとプリミティブで、CLIPのゼロショット性能と、Fine-tuningモデルの下流タスクの精度を両立させることです。Fine-tuningを行うと、下流タスクの精度は大幅に向上しますが、他の知識を忘れてしまう現象(破滅的忘却)が広く確認されています。簡単に言うと、ゼロショットモデルがジェネラリストで、Fine-tuningがスペシャリストだとして、モデルマージによって両者のバランスを取ろうという考え方です。

WISE-FT

WISE-FTは、ゼロショットモデルとFine-tuningモデルをアンサンブルし、係数を線形平均で統合する方法を採用しています。分布シフトの精度と堅牢性のトレードオフを図ることが目的です。この方法の有効性を実験的に示した研究として注目されています。

PAINT

PAINTもやっていることはWISE-FTと同じで、モデルの係数をアンサンブルする手法です。PAINTはさらに進んで、目的データの精度を向上させるために、類似したドメインのデータで訓練し、係数をアンサンブルしたら目的データでも精度が向上することを示しています。

例として、MNIST(手書き数字)が目的データセットで、訓練データが得られない場合を考えます。SVHN(Googleストリートビューからの番地画像データ)をサポートデータとして使用し、Fine-tuningと係数のアンサンブルを行うことで、MNISTの精度が16.8%向上したと報告されています。

また、PAINTはモデルのタイポグラフィック攻撃(Adversarial Examplesの一種)耐性を向上させる応用も検討しています。タイポグラフィック攻撃とは、「猫が"dog"と書かれた付箋を持っている場合、ゼロショットモデルがそれを犬と認識してしまう(図左下のa)」という現象です。これに対処するために、「水族館の写真に機械的に"sky"と書かれたテキストを追加した(図左下b)」ような人工データを大量に作成し、それを使ってFine-tuningとPAINTを適用することで、タイポグラフィック攻撃耐性が大幅に向上したと報告されています。

周辺データでの訓練の話は、生成モデルを含めて示唆に富む話なので、画像生成でモデルマージが大衆化している現状を踏まえると、今後着目されそうな技術に思われます。

事前訓練の改善方法など

CLIPの訓練フレームワークを改善する方法や、その他の応用について説明します。

MUST

この研究は、事前訓練の改善に焦点を当てたもので、MUST(Masked Unsupervised Self-training)という手法が提案されています。MUSTは、ラベルがないデータが与えられたときの「教師の源は何か」という問題に対して、自己学習(Self-training)と自己教師あり学習(Self-supervised Learning)の2つのアプローチに注目しています。

自己学習は、教師モデルと生徒モデルの2つのモデルからなるフレームワークで、教師モデルが疑似ラベルを生成し、生徒モデルがそれを基に学習します。一方、自己教師あり学習は、データの一部をマスキングしたり、データ拡張を行ったりしながら、復元問題を解くものです。自然言語では、穴埋め問題がよく使われます。ただし、自己学習は疑似ラベルに過学習しやすく、自己教師あり学習は下流タスクに特化したFine-tuningが必要で、ワンステップで完結するわけではありません。

そこで、MUST(Masked Unsupervised Self-training)が提案されました。これは、自己学習と自己教師あり学習の欠点を克服するために、疑似ラベルと生画像の2つの異なる信号を相補的に学習することで、より高精度な教師なし学習を実現しています。ImageNetでゼロショットCLIPが68.3%だったものが、MUSTを使うとラベルなしで77.7%まで上がったことが報告されています。

VL-LTR

VL-LTRは、ロングテールデータに特化した訓練方法です。ロングテールデータとは、データセット内に多数のサンプルがあるクラスと、少数のサンプルのみをもつ多数のクラスが混在する不均衡なデータのことです。

この研究の目的は、従来の画像モデルだけでは不均衡データの性能が限定されるため、テキスト情報を活用して不均衡データに対処する方法を提案することです。ゼロショットCLIPはある程度のロバスト性がありますが、本研究の事前訓練手法を使うことで、特に多数サンプルクラスの精度が向上しました(ただし、ResNet50ベースでは少数サンプルクラスの精度が低下した原因は不明です)。

Domino

Dominoは、機械学習モデルの性能を低下させるデータの一部(スライスと呼ばれる)を見つけ出し、それを人間が理解しやすい形で示すことを目指しています。

具体的には、Dominoは自然画像に対するCLIP特徴量のようなクロスモーダルな埋め込みを利用し、混合ガウスモデル(クラスタリング手法)を使って性能を低下させるスライスを特定します。この埋め込み表現は、テキスト-画像だけでなく、医用画像にはConVIRTやMIMICを使ったり、医用の時系列データにはEEGを使ったりと、自然画像のCLIP以外のさまざまなモダリティやドメインにも適用できる点が特徴です。

物体検出

次に、CLIPを使った物体検出の研究を見ていきましょう。従来の物体検出では、限られたクラスの中での検出が主でしたが、CLIPをはじめとするVision & Languageの技術のおかげで、オープンボキャブラリーの性能が大幅に向上しています。オープンボキャブラリーでは、言語モデルを利用して、データセットに明確に定義されていないクラスも検出できます。これに対して、クローズドな設定では、例えばCOCOのラベルでは「車」を「car」と検出するだけで、「赤い車」や「黒い車」などの特定のクラスを指定することはできませんでした。

CLIPを物体検出に適用することは比較的簡単で、最も直接的な方法は、スライディングウィンドウを使って画像のパッチごとにCLIPを推論することです。この手法はどんどん改良されており、その進化を見ていきましょう。

ViLD

ViLDは、Vision and Language knowledge Distillation(視覚と言語の知識の蒸留)の略で、オープンボキャブラリー物体検出に取り組んだ研究です。

ViLDでは、オープンボキャブラリーな分類器(例:CLIP/ALIGN)を「教師」とし、物体検出器を「生徒」として扱います。教師と生徒は同じモデル構造を使用します。生徒側では、R-CNNで一般的なRoI-Alignを用いて、物体の候補(Proposal)を切り取ります。そして、切り取られた部分のEmbeddingが教師(CLIP)に近づくように学習させます。性能的にはまだかわいいものです。

GLIP

GLIPは、Grounded Language-Image Pre-trainingの略で、物体検出と言語の関連付けを学習するモデルです。GLIPにはv1とv2の2つのバージョンがあります。v1は物体検出に特化したモデルで、v2はマルチタスクに対応した基盤モデルとして拡張されています。ただし、v2でも物体検出が重要な役割を果たしています。ここでは、v1について説明します。

GLIPは、物体検出と言語との関連付け(グラウンディング)を統合し、物体レベルの対応や意味的な表現を学習することに特化しています。CLIPが画像とテキストの対応学習を行うのに対して、GLIPは領域(物体)とテキストの対応学習を行います。この領域単位の学習により、GLIPは先行研究のDynamic Headを教師あり学習した場合と比較して、1ショット学習で13のデータセットで同等の精度を達成しています。なお、Dynamic HeadはGLIPのネットワーク内にも組み込まれています。

GLIPv2については、「基盤モデル」の項で詳しく説明します。

RegionCLIP

RegionCLIPも、領域単位で学習を進める方法です。

RegionCLIPでは、バックボーンから抽出されたRegion Proposalごとに、CLIP Embeddingが等しくなるようにアラインメントを調整します。これにより、画像領域とテキストの概念を細かく調整し、物体検出の精度を向上させることが目的です。GLIPとRegionCLIPの違いは、RegionCLIPが明示的にCLIPのEmbeddingを学習しているのに対して、GLIPはそれにとらわれないという点です(計算量は明らかにされていませんが、おそらくGLIPの方が事前訓練は重いはずです)。

Region Proposal単位でのCLIP Embeddingの学習は、ViLDと似ています。しかし、RegionCLIPの著者らによると、ViLDとの違いは、同一の事前訓練でゼロショット推論も転移学習もサポートしている点だと述べています。

Detic

次に紹介するのはDetic(detector on image classification data)という手法です。これは、「画像分類のデータを使って、物体検出の分類器を訓練する」というシンプルな考え方に基づいています。

画像分類では、ImageNet 21Kのように、大規模かつ多クラスなデータが利用できます。しかし、物体検出では数万クラスのデータセットが存在せず、クラスの拡張が難しい状況です。Deticのアイデアは、「画像分類のデータを使って、物体検出のクラス分類だけを行い、物体検出のタスクはRegion Proposalの切り出しまでに限定する」というものです。Region Proposalを切り出した後は、画像分類を行うだけなので、物体検出におけるBounding Boxの切り出しとクラス分類が密結合である必要は特にないわけです。

実際に、Deticでは、クラス分類のロス(損失)を「Region Proposalかどうか」の二値分類として扱います。これは「弱教師あり物体検出」としても考えることができます。

数万クラスのクローズドな設定で検出器を作成する場合は、ImageNet 21Kなどで分類器を訓練すればよいですし、オープンボキャブラリーな物体検出を行いたい場合は、分類器の部分をCLIP埋め込みによって出力するようにすればよいのです。Deticの強みは、このような問題設定の違いや、バックボーンの構造に柔軟に対応できることです(Region Proposalの切り出しに特に複雑な処理は必要ないため)。

OWL-ViT

次に紹介するのは、OWL-ViT(Vision Transformer for Open-World Localization)という手法です。

これまでのオープンボキャブラリー物体検出手法では、Region Proposalが必要でしたが、OWL-ViTではそれをやめ、パッチベースのアンカーとして考えます。

図の左側はCLIPの対照学習で、Image Encoderの最後に、ViTのトークンをPoolingと射影のLinear層を追加して、画像全体のImage Embeddingを取得します。一般的に公開されているCLIPの重みはこの状態です。OWL-ViTでは、PoolingとLinear層を取り除き、2つのブランチを作ります。分類方向では新しい射影のLinear層を、回帰方向ではMLP Headを追加し、物体検出の訓練を行います。パッチ単位でクラスと座標を予測するのがOWL-ViTの特徴です。

OWL-ViTの論文には、これまで紹介した物体検出の手法がすべて揃っています。こうしてみると、手法の進化がわかりやすいですね。

VL-PLM

次はVL-PLM(V&L-guided Pseudo-Label Mining)という手法で、これも擬似ラベルベースです。

この手法では、Region Proposalを使用していますが、RoI Headを繰り返し適用することでROIの信頼度を向上させる点が面白いです。これにより、NMSやスレッショルドが不要になります。

Region Proposalの抽出品質が向上すれば、擬似ラベルの品質も良くなり、結果として物体検出の精度が上がるというのが大きなポイントです。この手法は、オープンボキャブラリー物体検出と半教師あり物体検出の両方に対応しています。

Obj2Seq

次に紹介するのは、Obj2Seqという、物体検出とポーズ推定を同時に行うモデルです。オープンボキャブラリーではありませんが、物体検出とポーズ推定を組み合わせた点が興味深いため、取り上げました。

この研究は、DETRシリーズから大きな影響を受けています。オブジェクトの切り出しは、Region ProposalやAnchorではなく、オブジェクトクエリとして表現されています。モデルはTransformerベースで、Attention機構を活用しています。AttentionはQuery-Key-Valueから構成されます。

Sequence Predictorは、図の(c)のように、オブジェクトクエリを入力として、Bounding Boxやキーポイントの座標を出力します。オブジェクトクエリの役割は、Sequence PredictorにBounding Boxやキーポイントの値を問い合わせる(クエリする)ことです。

オブジェクトクエリを使うのはDETRの特徴ですが、クエリは数値表現なので、タスク固有のモデル構造は不要です。つまり、Obj2Seqのような「物体検出とポーズ推定」を組み合わせたタスクでも、オブジェクトクエリが適切に学習してくれます(この研究では、「General Sequence Predictor」と呼ばれています)。

これだけ見ると「CLIPの役割どこなの?」と疑問に思うかもしれません。実は、この研究で新たに導入された「Prompted Visual Indicator」という手法が、CLIPのプロンプトラーニングと関連しています。Prompted Visual Indicatorは、「指定したカテゴリのオブジェクトに注目してね」という指示で、Region Proposalのクラス指定に近い意味を持ちます。この指示自体がプロンプトとして考えられるため、プロンプトラーニングの文脈で実現できるのです。

Obj2Seqの新規性は、特にGeneral Sequence PredictorとPrompted Visual Indicatorの2つにあります。「Region Proposalのような力技ではなく、プロンプトラーニングやクエリを使って学習ベースで、もう少しスマートに定義したよ」というのが、このフレームワークの特徴ではないでしょうか。

Grounding DINO

2023年4月現在、オープンボキャブラリー物体検出で最強モデルとされるGrounding DINOを紹介します。最近注目されているSegment Anythingと光の速さでくっついて話題となりました。Grounding DINOは、テキストでクラスを指定できるオープンボキャブラリー物体検出モデルです。一方、Segment Anythingはセグメンテーションモデルで、オープンボキャブラリーセグメントも理論上可能ですが、2023年4月時点ではその部分の実装が公開されていません。そこで、Grounding DINOがその未公開部分を補完する役割を果たしています。

Grounding DINOの実装は、強力な物体検出モデルであるDINOをベースにしています。DINOはDTER系の派生であり、Grounding DINOでもその実装が洗練された形で取り入れられています。モデル内にはAttentionが随所に配置されており、「Attention is All You Need」を体現したような構造です。オブジェクトクエリの概念も継承されています。Language-guide Query Selectionでは、画像特徴とテキスト情報を融合し、クロスモーダルなクエリを丁寧に作成しています。

ネットワーク全体を見ると、最初はImage BackboneとText Backboneというユニモーダルな特徴が徐々に融合・アップデートされ、クロスモーダルな特徴を獲得していく構造が見られます。

また、Grounding DINOでは、従来のCOCOのようなClosed-Set Detectorの情報もBounding Box予測に利用しているのが大きな特徴です。オープンボキャブラリーで検出する際に、クローズドな情報を参照してアップデートすることで精度が向上するという発想です。クローズドなモデルを徐々に拡張し、オープンボキャブラリーへとつなげていくのがメッセージ性を感じて面白いところです。

Grounding DINOの論文では、これまでに紹介されたオープンボキャブラリー物体検出の手法がいくつも登場し、各手法の仕組みや精度比較が行われています。近年の手法の中にはCLIPベースのものもありますが、Grounding DINOがCLIPなしでこれほど強力になったことから、CLIPの必要性が徐々に薄れているかもしれません。しかし、Vision & Languageによるオープンボキャブラリーの物体検出は当面続きそうです。

セグメンテーション

CLIPを用いたオープンボキャブラリー(ゼロショット)セグメンテーションの研究が増えています。しかし、物体検出と比べると、CLIPからセグメンテーションに一般化できるのは自明ではありません。なぜかというとタスクの粒度が異なるからです。

CLIPは分類モデルであり、画像全体を1つのクラスに分類するタスクです。画像をパッチに分割していくと、ある程度の解像度までは、パッチで細かく分けても何が描かれているかわかります(意味的な情報が残っている)。物体検出は画像をクロップして分類するので、意味的な情報が残る解像度であれば、分類モデルを検出モデルに転用できるのも割りと自明な話です。

しかし、セグメンテーションの場合はどうでしょうか? セグメンテーションはピクセル単位のクラスを求めるタスクです。ここで問題なのは、「画像全体を1ピクセルまで分割したときに、意味的な情報は残っているのか」という点です。犬や猫の画像でも、1ピクセルに縮小すると、ただのRGBの値を持った点になります。カラーコードの値から犬や猫といった意味的な情報はわかりません。これが、分類とセグメンテーションのタスクの粒度が異なり、一般化が難しい理由です。分類や検出に比べて研究が遅れているのは、このような背景があります。

DenseCLIP

DenseCLIPは、セグメンテーションタスクで画像言語のフレームワークや、CLIPの事前訓練が有効であることを示した研究です。

DenseCLIPはシンプルな構造のモデルで、パッチ単位の粗い画像埋め込みを基にして、ピクセル単位の細かいセグメンテーションマップを予測します。DenseCLIPのImage Decoderは、バックボーンの解像度別の特徴を基にセグメンテーションマップを作成します。ただし、DenseCLIPではオープンボキャブラリーセグメンテーションは実現できず、Fine-tuningしたときの精度が画像オンリー・教師ありより良かっただけで、クローズドな問題設定にとどまりました。

CLIPSeg

CLIPSegになると、ようやくゼロショットセグメンテーションに近いことができてきます。

CLIPSegは、ゼロショットセグメンテーションの困難をプロンプトで解決したモデルです。具体的には、サポート画像やセグメンテーションマスクをVisual Promptとして使用し、Text Promptのサポートも受けながら、入力画像をクエリとして推論します。Transformerのデコーダーを使って、意味的な特徴をピクセルベースに変換します。また、Negative Sampleを取り入れる点が斬新なアプローチです。Visual Promptには教師データも使用できるため、参照表現、ゼロショット、ワンショットの3つのフレームワークで、追加の訓練なしに推論が可能です。参照表現のフレームワークとは、自然言語のフレーズでターゲットを指定し、フレーズに一致するすべてのピクセルをセグメンテーションする手法です。

ただ、精度面はPascal-VOCというかなり簡単なデータセットで、データセット全体を未知/既知に分割し、未知10クラスのケースで右下の表の程度(mIoU_Uが未知のクラス)なので、まだかわいいぐらいの性能しか出ませんでした。

LSeg

LSegは、オープンボキャブラリーなセグメンテーションが可能なモデルの一つです。

LSegでは、画像エンコーダから低解像度(入力画像の1/2程度)の特徴量を取得し、パッチ単位のCLIP埋め込みを行います。これはDenseCLIPと似ていますが、デコーダー以降の部分が異なります。

LSegでは、セグメンテーションマップの作成と元の解像度へのアップサンプリングを、「Spatial Regularization Blocks」という手法で行います。これは、DepthwiseConvとBottleneck層を経て、Bilinear法で元の解像度に戻す方法です。DenseCLIPではデコーダーがTransformerでしたが、LSegのデコーダーは空間方向への作用が少なく軽い構造になっています。これは、CLIP埋め込みで空間方向の配置がほぼ決まっているため、それを大きく崩さないようにする意図があるためです。この背景から、論文ではデコーダーではなく「Spatial Regularization Blocks」と呼んでいます。

LSegは0ショット学習を行っており、教師あり1ショット学習と比較して性能が向上していますが、まだまだ遠いです。この論文の特徴は、ファインチューニングによる転移性に逃げずに、純粋な0ショット学習で性能を競っている点です。

ReCo

ReCoは、Retrieve and Co-segment for Zero-shot Transferの略で、検索ベースの画像セグメンテーションです。

検索ベースのセグメンテーションは、ラベル付けされていない画像から、特定の概念に関連する学習データを動的に収集することができます。推論時には、推論対象の画像のImage Encoderの特徴マップに、学習データと類似した埋め込みを適用し、セグメンテーションをさらに精緻化していきます。

「特定の概念に関連する学習データを動的に収集」という点がふわっとしているので少し解説します。CLIPを使えば画像検索ができることは直感的に理解できます。CLIPを使った画像検索では、「テキスト-画像」の検索が可能ですが、ReCoのアイデアは、「画像-セグメンテーションで学習してしまえば、画像に類似したセグメンテーションも検索できるのでは?」ということです。

推論時に埋め込みを適用する理由は、未知のセグメンテーションを「既知の画像特徴」+「検索から取得した埋め込み」から求めたいという考えに基づいています。これは、Transformerベースの物体検出におけるオブジェクトクエリに近いものだと私は理解しています。

OVSeg

OVSegは、まずセグメントマスクを抽出し、その後にマスクを含めたCLIPを学習するというアプローチを採用しています。

これは、オープンボキャブラリーを用いた物体検出でRegion Proposalを行う方法に似ています。しかし、マスクを使用すると、マスクされていない領域のほとんどが0のトークンになってしまい、性能が低下する問題があります。これを解決するために、マスク部分を活用したプロンプトチューニングが実施されています。

性能に関しては、ADE20K150データセットを用いた評価で、オープンボキャブラリーのようなジェネラリストモデルが、2017年の教師あり学習のスペシャリストモデルに匹敵したことを報告しています。

CLIMS

CLIMSは、Class Activation Map(CAM)をベースにしたセグメンテーション手法です。

CLIP関係なく、CAMをとってもセグメンテーションっぽくなるのはよく知られています。CLIMSは、オープンボキャブラリーの状況でCAMを学習ベースに取り入れた手法で、CAMを用いてクラスを分割し、CLIPのような学習フレームワークを適用しています。前景と背景に対して損失を計算し、学習を通じて精度を向上させることを目指しています。

CLIMSは、弱教師ありセグメンテーションを目的としており、CAMを弱教師として利用しています。

Segment Anything

SAM(Segment Anything)は、最近注目されている非常に優れたゼロショットセグメンテーションモデルです。

SAMがこれほど優れている理由は、モデル構造の工夫だけではなく、データの作成方法(Data Engine)に大きな工夫があるからです。モデル自体は比較的シンプルで、SAMではSA-1Bという1.1億枚の画像と10億個以上のマスクからなる大規模なデータセットを作成しています。これほど膨大なセグメンテーションのデータセットは地球上に存在しないので、そこを頑張って作りましたという部分に大きな貢献があります。

データエンジンの最終的な目標は、セグメンテーションデータを自動生成することです。データエンジンは3つの段階があります。1段階目は「手動」で、古典的なインタラクティブセグメンテーションと同様、アノテーターの手動作業を支援します。2つ目の段階は「半自動」で、SAMが位置を指定すると自動的にマスクが生成され、人間が残りの部分をアノテーションしてマスクのバリエーションを作成します。3つ目の段階は「全自動」で、画像に対して規則的なグリッドを与えると、SAMが自動的にマスクを生成します。

動画

動画への拡張もなかなかにアツいジャンルです。動画の分類・検索は、CLIPに時系列要素を追加するだけなので、順当に拡張されていきました。ここでは、オープンセットの動画分類や検索に焦点を当て、動画を入力として質問応答を行う(出力がテキスト)ようなモデルは「基盤モデル」の項で説明します。

CLIP4Clip

CLIP4Clipは、CLIPを時間方向に拡張したシンプルなモデルです。

CLIP4Clipは、画像のフレームごとの特徴量を集めて時系列方向にまとめるシンプルな拡張です。単純に時系列方向の平均を取る「Parameter-free type」でも機能します。直感的には、時系列方向にTransformerを適用すると精度が高くなると思われますが、訓練データが小規模な場合は「Parameter-free type(ただの平均)」が最も性能が良いと報告されています。一方、大規模な訓練データでは、Attention構造が性能向上に寄与しています。

平均の場合でも時系列情報をいくらかキャプチャできるという結果になったのが面白いですね。

ActionCLIP

ActionCLIPは、CLIP4Clipと非常に似たアプローチを取っています。

ActionCLIPは、3つのステップ(事前訓練、プロンプト、Fine-tuning)を経て、追加のラベル付けが不要なゼロショット転移を実現します。ただし、計算資源の制約から、事前訓練にはCLIPを使用しています。プロンプトエンジニアリングが含まれている点が、CLIP4Clipと若干異なります。

Kinetics-400でのFine-tuningの精度を示しています。この図で「Visual Prompt」と言われているのは、Post-Networkの構造です。結果として、CLIP4Clipと非常に似ており、Mean Poolingでもうまくいくが、時間方向のTransformerにすればもう少し上がる程度です。

Bridge Prompt

次の研究はBridge-Promptとよばれるもので、これはなかなか面白い仕組みです。

動画をテキストで説明するためには、多面的な表現が必要です。Bridge-Promptでは、プロンプトエンジニアリングを用いて、モデル内にこれらの表現を組み込んでいます。

例えば、パンを取り、チーズを乗せ、マヨネーズとマスタードをかけるという動作の動画があるとします。統計的なプロンプト(「これは1番目のアクションです」など)や意味的なプロンプト(「最初にこの人はパンを取っています」など)を使って、動画の内容を一文で表現する統合プロンプトを作成します。これは、GPTのChain of Thoughtに似ているかもしれません。これらを順番に橋渡し(Bridge)していくことで、動画の多面性や時系列性を表現し、精度を向上させることが、この手法の重要な貢献です。

さらに、この手法ではASFormerを使用して、Action Segmentation(どの時間からどの時間まで何をしているか)を特定するタスクも行っているのが特徴です。

X-Pool

X-Pool は、CLIP4Clipの仕組みをAttentionを用いてより正確に定義したものです。

従来のCLIPの動画への適用は、CLIP4Clipのようにフレームごとの類似性を計算し、時間軸方向に集約(平均やSelf Attention)する方法でした。しかし、この方法ではテキストに記述されていない誤解を招く画像情報が符号化される可能性が高いことがわかりました。そこで、Cross-Modal Attention「X-Pool」を導入し、テキストと最も意味的に類似したフレームに注目するようにします。

X-Poolに関する補足として示された簡単な実験は示唆に富んでおり、例えば従来のMean PoolingをTop-K Poolingに置き換えるだけで(CLIPの類似性を計算し、時間軸方向にTop-Kを選んで平均する)精度が向上することが報告されています。経験的にはこのような方法がよく使われますが、「意味的に類似したフレームだけに注目する」と言われると納得できるものです。

ECLIPSE

ECLIPSEは、音声を追加情報として利用することで、長時間の動画を素早く検索できる研究です。

これまでのCLIPの動画拡張では、短時間の動画であれば全フレームを推論することができましたが、長時間の動画では計算コストがフレーム数に比例して増加します。フレームを間引くこともできますが、情報が失われることになります。そこでECLIPSEは音声に着目しました。音声は画像(メルスペクトログラム)として表現でき、横軸が時間、縦軸が周波数になります。これにより、メルスペクトログラムは時間的に密な表現が可能です。

ECLIPSEでは、長時間の動画に対してフレームを大幅に間引き、その分の情報をメルスペクトログラムで補うことで、高速かつ高精度な検索が実現できます。実際に、CLIP4Clipで64フレームを参照した場合と比べて、ECLIPSEは32フレームでより高い検索性能を達成しています。

Text4Vis

Text4Visも単純ながら面白いフレームワークで、これも画像とテキストとのモダリティギャップを埋めることが目的です。

CLIPのImage Encoderの末尾に線形分類器を追加し、下流タスクに適応させる「Linear-Probe」という手法を以前紹介しましたが、これがモダリティギャップを埋めるのに効果的だとされています。手順としては、まずImage Encoderの出力を使って分類器を学習させます。次に、分類器を固定し、分類器のロジットに等しくなるように、Text Encoderの埋め込み(Projection Layer)を学習します。これにより、画像とテキストのモダリティギャップが埋まり、下流タスクでの性能が向上することが報告されています。

先行研究とくらべてかなり大きな進歩が見られます。HMDBで50%近くなったというのはなかなかのものです。

X-CLIP

X-CLIPは、Attentionベースの拡張ですが、どちらかという自然言語処理の方向へ拡張しています。。

CLIP4Clipは、「文章-動画」の対照学習だけでしたが、X-CLIPはさらに進化させています。「単語-フレーム」「文章-フレーム」「単語-フレーム」など、さまざまな関係性を考慮することで、精度を向上させることができます。図のように、Xの字状にパスを引いているから「X-CLIP」なのだと思われます。これらの類似度の集約のために、Attention Over Similarity Matrix(AOSM)というモジュールを導入しています。

MV-Adapter

MV-Adapterは、動画に対応したマルチモーダルなAdapterです。

着想としては画像用のAdapterと同様で、「訓練パラメーター数をいかに少なくして、下流タスクに適用できるか」という問題の動画版です。具体的には、Image EncoderとText Encoderの末尾にAdapterを追加し、Cross-Modal Interaction Moduleの部分をクロネッカー積で定義しています。Adapterにクロネッカー積を使用するのは、この記事で紹介されている分類モデルのKronecker Adaptationでもありましたね。

結果はかなり驚きもので、訓練パラメーターを約1/40にしたにも関わらず、MSR-VTTでFine-tuningの精度を超えてしまったとのことです。AdapterがFine-tuningを超えるのは最近ちょくちょく報告されていて、今後の発展が期待されます

生成

画像生成モデルは急速に進化しており、ここで紹介する研究はすでに古くなっています。特にStable DiffusionやImagenの登場によって、CLIPベースの研究の有用性がゆらぎつつありますが、いくつかの研究を簡単に見ていきましょう。

StyleGANベース

2021年には、StyleGANを利用した研究が主流でした。以下では、StyleGANとCLIPを組み合わせた画像編集の研究を紹介します。

StyleCLIP

StyleCLIPは、StyleGANの潜在変数をCLIPベースの損失関数で操作し、画像編集を行う手法です。

StyleCLIPの新規性は、ラベル付けされたデータや潜在空間の自由度の調査、明示的な教師なしで、テキストプロンプトを使って画像を操作できることです。StyleGANの時代には、潜在変数の意味を数値から解釈する必要があり、改造コードのような泥臭い解析が必要でした。今となっては当たり前ですが、いかにテキストプロンプトが画期的だったかがわかるでしょう。

CLIPstyler

CLIPStylerは、StyleGANをベースにしたStyleNetに、パッチ単位のCLIP損失関数を追加することで、スタイル変換を実現しています。

この研究は、StyleGANにCLIPのガイドを追加したStyleGAN-NADAをベースとしています。パッチ単位のCLIP特徴量を利用してスタイル変換を行っています。正直今だと、拡散モデルに置き換えられていると思います。しかし、CLIPStylerの速さは魅力的で、スタイル変換の訓練には2080Tiで約40秒、推論には1枚あたり0.5秒しかかかりません。最近では、GANが拡散モデルの蒸留に使われることが注目されており、再びGANが評価される時代が訪れるかもしれません。ただし、同時に拡散モデルを爆速にする研究も出ています。

HairCLIP

HairCLIPは、StyleCLIPの派生形で髪の毛の入れ替えを可能にしたものです。プロンプトで指定することもできます。

今から見ると「Inversionしてるなら拡散モデルでいいでしょ」と考えたくなりがちですが、かつてはこのような方法で頑張っていた時代もあったのです。

clip2latent

clip2latentは、CLIPのプロンプトをStyleGANの潜在変数に変換する拡散モデルです。

拡散モデルが一般的になっている今からするとかなり違和感がありますが、GANは潜在変数→画像の変換はできても、画像→潜在変数への逆変換が直ちに計算できません(拡散モデルは逆変換が可能です)。そのため、画像からGANの潜在変数を推定する「GAN Inversion」という手法がいくつか開発されました。テキストプロンプトからStyleGANの潜在変数を計算できることは、以前は嬉しかったということです。

ただし、このフレームワークは、デコーダーがStyleGANであることを除けば、ほぼDALL-E 2と同じです(論文中でも引用されています)。本研究は2022年10月の投稿ですが、このタイミングでどの程度のインパクトがあったのかは正直未知数です。

PPE

PPEは、Predict、Prevent、Evaluateの頭文字を取ったもので、StyleCLIPの改良版です。

この研究では、StyleCLIP(間接的にはStyleGAN)ベースながらやや面白いことやっていて、言語モデルのBERTから顔の特徴のカテゴリの階層構造をとってきて、Disentanglementを損失関数に考慮して画像を生成しています。StyleCLIPでは、「前髪を入れて」や「黒髪にして」と指示したときに肌色が変わるなど、属性間のリークが発生していましたが、PPEを使用することで属性の関係性が理解され是正されます。アノテーションがほぼ不要なのも嬉しい。

この研究の興味深い点は、「どのようにしてカテゴリに対応する属性の階層構造を作成したのか?」という部分です。その方法として、BERTに穴埋め問題を解かせています。具体的には、“a face/person with a/an [MASK] [X]”や“the person’s [X] is/are [MASK]”といった形で、Xにカテゴリを入れ、MASKに当てはまる単語を列挙させます(LAMAというツールを使用)。今だとこういうのはGPTで出せばいいので、プロンプトエンジニアリングとして普遍的に使えるテクニックではないかと思います。

StyleGANベース以外

次に、StyleGAN以外の研究を見ていきましょう。拡散モデルが一強となった今、このへんを改めて見ると目新しさはあると思います。

Make-A-Scene

Make-A-Sceneは、自己回帰モデルを使ったText2Imageのモデルです。

この研究では、CogViewやDALL-Eと同様に、自己回帰モデルで画像生成を行っています。自己回帰モデルによる画像生成は、ピクセル間の依存関係を表現するものです。拡散モデルではなく自己回帰モデルを使う理由は、拡散モデルでは高次元の画素空間で直接行われるため、ストーリー生成のような複雑なタスクには適用できず、タスクの複雑さを軽減したかったからです。Make-A-Sceneでは、低次元の作業に変更し、逐次的なストーリー生成に拡張しています(ただし、Stable Diffusionが登場する前の研究なのが注意)。また、Classifier-free Guidanceや、空間や構図などの複雑なシーン制御も行っています。

現在では、「空間制御は拡散モデル+ControlNetでいいじゃん」と考えられがちですが、ControlNetが発表されたのが2023年2月であったのに対し、Make-A-Sceneは2022年3月に発表されたものなので、そこそこ時代の先を行っていた研究です。また、これを用いたストーリーイラストの生成デモも公開されています。

LANIT

LANITは、Unpaired image-to-image translation(例:CycleGAN)や、その応用であるドメイン間の翻訳(例:UNITやStarGAN)に言語要素を追加したものです。

この技術を使うと、テキストで指定された属性に基づいて、画像をあるドメインから別のドメインに翻訳できます。従来の方法では難しかった複数属性の処理が可能で、アノテーションも不要です。さらに、プロンプトラーニングやドメイン正則化の損失関数が導入されています。モデルの基本構造はStarGANv2を使用しています。

ES-CLIP

ES-CLIPは、進化戦略というアルゴリズムを使って幾何学的な模様を配置し、画像を生成する研究です。

幾何学的な模様を使って画像を生成する研究は、これまでにもいくつか行われています。GANや拡散モデルでは、実際の画像を教師データとして学習しますが、幾何学的な模様を使う方法では、三角形や四角形を配置して、最終的に現実世界のような画像を作り出すという考え方です。これは単なる図形であるため、学習データの著作権に関する問題が起こりにくいという利点があります。

この研究では、「三角形の座標や大きさ、色」などを最適化していきます。最適化の部分は進化戦略で行い、出力画像の類似性を評価するためにCLIPを使用しています。最適化を進化戦略と遺伝的アルゴリズムで比較し、進化戦略の方が品質と効率を大幅に向上させることができたと報告しています。幾何学模様と遺伝的アルゴリズムによる画像生成というと、2021年にこういったものがネット上で話題になりましたが、本研究は2021年9月に発表されており、どこかで意識していてもおかしくないなと思わせるような内容です。

基盤モデル

基盤モデル(Foundation Model)とは、藤井(2022)によると、「さまざまなタスクに利活用できるように、大量のデータで学習させた高性能な事前訓練モデル」とされています(『コンピュータビジョン最前線 Summer 2022』のp.9 より引用)。GPTは自然言語処理における基盤モデルの代表例で、指示を与えるだけで、質問応答や要約、ストーリー生成など様々なタスクに対応できます。この記事でずっと紹介し続けたCLIPも基盤モデルの一つです。

基盤モデルは研究的にホットなジャンルで、モデルが改良され、これまでの専門モデルが、精度を上げながら一つの汎用モデルに統合されています。

画像系基盤モデル

ALBEF

まず紹介するのは2021年に発表されたALBEFというモデルです。「Align before Fuse」の頭文字をとってALBEFと呼んでいます。

ALBEFでは、画像とテキストの特徴を揃えるために対照学習を導入し、ユニモーダルなエンコーダーを改善します。CLIPは分類が目的でしたが、ALBEFはテキストの出力も可能で、画像検索やVQAにも対応しています。相互情報量の下限を最大化するため、画像-テキストのペアに対して、ITC(Image-Text Contrastive learning)とMLM(Masked Language Modeling)という異なるビューを損失関数に導入しています。

さらに、ノイズの多い学習データに対応するため、Momentum Distillation(MoD)という自己学習を行っています。MoDはパラメータの移動平均を取ることで、擬似ターゲットを生成し、追加の教師として機能します。これは代替のビューを提供し、相互情報量の下限を最大化する点で、ITMやMLMと同様に効果的に機能していることが理論と実験から示されています。

FLAVA

FLAVAは、ALBEFをより進化させたモデルで、「Foundational Language And Vision Alignment model」の略です。

ALBEFではあくまでクロスモーダル(CV&L)やマルチモーダル(MV&L)のみでしたが、FLAVAは画像やテキストのユニモーダルでも利用できるように改良されています。ユニモーダルのマスクを追加したり、訓練データ数を増やしたりすることで、大まかな流れは変わっていません。「35の下流タスクで良い性能を達成した」と報告されており、基盤モデルとしての特性が強化されています。

画像側の精度はCLIPと同等ですが、NLP側の精度が大幅に向上しています(CLIPと比較したらそれはそう)。

Everything at Once

Everything at Onceは、画像とテキストを扱っていたCLIPを、画像-テキスト-音声の3モダリティに拡張したものです。

新しい特徴は、3つのモダリティのすべての組み合わせで対照学習している点で、推論時には任意の数や長さの異なる要素を処理できます(これはTransformerの特性によるものです)。ゼロショット動画検索だけでなく、Zero-shot Action Localization(行動の空間・時間的な座標を見つけるタスク)でも最先端の結果を出しています(2021年12月時点)。

BLIP

BLIPは、ブートストラップ法を用いた基盤モデルで、ALBEFの改良版です。ただし、BLIPにはv1とv2があり、それぞれでブートストラップの意味が異なるため注意が必要です。v1のBLIPでは、データのデータストラップを指します。

BLIPは、ノイズの多いデータに対して工夫を凝らした訓練方法で精度を向上させています。これは、手作業でアノテーションされた画像-キャプションのデータが限られているため、インターネットからクロールしたalt属性のテキストをデータとして利用することが一般的になっているためです。しかし、alt属性はノイジーなデータであるため、キャプションにブートストラップ法を適用して精度を向上させます。本手法の特徴的な部分に「Multimodal Mixture of Encoder-Decoder」と「CapFilt」があります。

「Multimodal Mixture of Encoder-Decoder」は、効果的なマルチタスクの事前訓練と柔軟な転移学習を実現するためのもので、ユニモーダルのエンコーダ、画像に基づくテキストエンコーダ、画像に基づくテキストデコーダとして機能します。「CapFilt」はブートストラップ法の部分で、キャプション生成を行うか、ノイズの多いキャプションを除去するかを選択する機能を持っています。

MERLOT Reserve

MERLOT Reserveは、MERLOTの改良版で、「Multimodal Event Representation Learning Over Time, with RE-entrant SupERVision of Events」という長い名前の略です。

このモデルの目的は、ビデオに関する質問に答えることができるように、動画のすべての要素(モダリティ)を共通の表現で学習することです。具体的には、動画内のマスクされたテキストやオーディオを復元することで、マスク学習という強力なフレームワークを利用し、従来の手法よりも包括的な理解が可能になります。

「Re-entrant supervision」という言葉は、もともと発達心理学から来ており、「人間が視覚や世界の知識を学ぶ際に、明示的な教師が必要ないことが重要」という仮説に基づいています。機械学習の用語に直せば、動画の全モダリティを使った自己教師あり学習を意味します。

Flamingo

Flamingoとは、GPTに触発されたマルチモーダルFew-shot Learningの基盤モデルです。

GPT(例えばChatGPT)のFew-shot Learningは、従来のFine-tuningベースのFew-shot Learningとは異なり、ニューラルネットワークを学習させる必要がありません。いくつかの質問と正解例を示した後、新しい質問を入力します。GPT(少なくともこの当時に出ていたGPT-3まで)は入力/出力がテキストのみでしたが、Flamingoではこれをマルチモーダルに拡張しています。

Flamingoでは、画像だけでなく動画も入力として扱うことができます。画像と動画ではフレーム数が異なりますが、Perceiver Resamplerを使って固定長の表現に変換します。この表現を、LLM(大規模言語モデル)の中間層にGated Cross Attentionレイヤーとして挿入します。画像モデルも言語モデルも基本的には係数が固定されており、Perceiver ResamplerとGated Cross Attentionの部分だけが訓練されます。

GIT

GITは、「A Generative Image-to-text Transformer for Vision and Language」の略で、これまでの研究のモデルをシンプルにし、1つの画像エンコーダーと1つのテキストデコーダーにまとめたものです。

従来の研究では、画像キャプション生成やVQA(視覚質問応答)に、物体検出器やOCR(光学式文字認識)などの追加モジュールが必要でした。しかし、GITでは事前訓練とFine-tuningを用いて、これらの機能をシンプルな構造にまとめつつ、性能を向上させることができました。この性能向上の理由は、「モデルサイズを大きくし、データサイズも増やした」というシンプルかつ脳筋な解決策にあります。

グラフでは、横軸が訓練データの画像数、縦軸がキャプションの性能やVQAの精度、線がモデルサイズを表しています。無慈悲なまでにスケーリング則が可視化されています。Flamingoではテキストデコーダーが固定されていましたが、GITではすべてのモデルが訓練されています(Flamingoの計算量が、係数固定にした割には軽くなっていないことも関係していると思われます)。

GLIPv2

物体検出の項目で紹介したGLIPが、基盤モデルとなってGLIPv2が登場しました。

GLIPはもともとオープンセットの物体検出モデルでしたが、これが拡張されローカリゼーションタスク(検出とセグメンテーション)と、視覚言語理解タスク(キャプション生成やVQA)と統合されました。対照学習の部分はGLIPの拡張であり、フレーズグラウンディングやMLMが追加されています。下流タスクに適用する際は、タスク固有のヘッドのFine-tuningが必要です。

GLIPv2の特徴は、事前訓練に物体検出が含まれていることです。これはGLIPv1の背景から来ており、物体とフレーズの対応を関連付ける訓練を行うことで、画像とテキストの関係を理解できるようになります。これはキャプション生成やVQAなどのタスクで有益な結果をもたらしています。

Unified IO

Unified IOとは、コンピュータビジョン、自然言語処理、クロスモーダルタスクを1つのフレームワークで実行できる基本モデルです。モデル構造は、自然言語処理モデルであるT5から影響を受けています。

図からわかるように、多くのタスクに対応できるのが特徴で、タスク専用のモジュールはなく、Transformerを使ったシンプルなエンコーダー・デコーダー構造になっています。タスクをテキストで指定するのが独特です。図のSentPieceは、SentencePiece tokenizerを示しています。

Unified IOの賢い部分は、さまざまなタスクを入力/出力のモダリティで類型化したことです。この類型化により、すべての入出力を1つのTransformerに統合することができます。すべてのタスクは、図のような比率で混合されたデータセットで共同で訓練されます。

LST

LSTは、スケルトンベース行動認識のためのフレームワークです。これは、人間の骨格特徴を使って行動を認識するタスクです。

このフレームワークでは、骨格情報を部位ごとに学習し、それぞれの部位の特徴とテキストの関係を把握します。人間の身体の動きをテキストで記述することから、LST(Language Supervised Training)と呼んでいます。このフレームワークは、NW-UCLAやNTU RGB+DなどのデータセットでSoTA(2022年8月時点)を達成しました。

骨格情報のエンコーディングには、Graph Convolution Network(GCN)が使用されています。CLIPが登場してから約1年半後の研究ですが、こういった研究はありそうでなかったものです。

TVLT

TVLTは、動画検索を高速化することを目的とした研究で、テキストを使わずに画像と生の音声入力からVision & Languageの表現を学習します。

従来の動画検索では、自動音声認識(ASR)が速度を遅くしていました。しかし、テキストを使わずにメルスペクトログラムベースで音声を扱うことで、推論速度を28倍に高速化し、パラメーター数を1/3に減らすことができました。

入力モードを「画像+テキスト(V+T)」と「画像+音声(V+A)」で比較してみると、前者はASRを使用しています。精度を重視する場合はテキスト(ASR)が有効ですが、音声に置き換えても精度の低下はそれほど大きくありません。レイテンシーが大幅に改善されるため、高速化を目指す場合には有望な手法です。動画検索での音声活用は、フレーム数を削減(ECLIPSE)や推論速度の向上(本研究)が可能で、思わぬ飛び道具かもしれません。

MedCLIP

MedCLIPは、医用画像に特化したCLIPの訓練方法を提案しています。

医用画像では、画像とテキストのペアデータを作ることが難しく、インターネット上にもほとんど存在しません。また、医用画像には「陽性、陰性」などのラベルが付与されているだけで、テキストは別に存在し、画像と直接関連付けられていません。このような状況で、高価なアノテーションを使わずに訓練を工夫するのがMedCLIPです。

例えば、n個の画像-テキストデータ、m個のラベル付き画像、h個のテキストデータがあるとします(nはm,hに比べて十分に少ない)。画像-テキストデータを分解すると、n個の画像とn個のテキストが得られます。次に、画像とテキストをすべての組み合わせで総当たりします。これにより、(n+m)×(n+h)の組み合わせデータが生成されます。ただし、テキスト側の関連性が不十分なため、外部の医学知識を利用して知識駆動型の意味的類似性を構築することが提案されています。

さらに、異なる患者で同じ症状が偽陰性になる対照学習の問題に対処するため、医学知識に基づく意味的マッチングロスを使用しています。これにより、先行研究のGLoRIAよりも、約1/10のデータで胸部X線データセットに対するゼロショット精度が向上しています。

MAP

MAPは、画像やテキストの不確実性や曖昧さを考慮したマルチモーダルな表現学習です。これは、「Multimodal uncertainty-Aware vision-language Pre-training model」の頭文字を取ってMAPと名付けられています。

例えば、下のアボカドサンドイッチの例は言語の不確実性を示しています。「アボカドサンドイッチ」という言葉は、mealやfoodといった単語に含まれていますが、それらは意味的に全く異なるわけではありません。CLIPのようなモデルでは、単語の表現を点として捉えるため、これらの単語を全く別のものと認識してしまい、不確実性による複雑な関係を捉えることができません。また、この不確実性はユニモーダルだけでなく、クロスモーダルでも発生し、「傘を持った人が歩いている」→「どの人ですか?」といった曖昧さが残ります。

マルチモーダルデータの曖昧さに対処するために、表現を点ではなく確率分布で学習するようにします。具体的には正規分布として学習し、VAEのreparameterization trickを用いてサンプリングします。これをVAEと考えると理解しやすくなります。この方法により、下流タスクの性能が向上し、より少ないデータ数でALBEFを超える性能を達成できたと報告されています。

ZeroCap

ZeroCapは、CLIPモデルとLLM(GPT-2)を組み合わせて、ゼロショットでキャプションを生成する方法です。例えば、「若い女性の写真+王様の写真-男性の写真=a yound queen」といった画像/テキストの算術演算を考慮したキャプションが作れます。

埋め込み空間で計算を行うことで、言語モデルに結果が伝わり、画像間の計算を考慮したキャプションが追加学習なしで生成できます。CLIPのロスを利用している点が特徴です。さらに、「画像+テキスト-テキスト」といったクロスモーダルな計算もできます。

音声系基盤モデル

次に音声版のCLIPなどの音声系基盤モデルについても見ていきます。

CLAP

CLAPとは、CLIPを1文字変えたことからもわかるように、音声版のCLIPです。FSD50k、ClothoV2、AudioCaps、MACSというデータセットで学習されています。音声のエンコーダーには、メルスペクトログラムベースのCNNが使われています。

環境音の分類、楽器の分類、音響シーンの分類、感情分析など、さまざまなタスクで効果が実証されています。(ZS)はゼロショット設定、(Best)はFine-tuning設定を意味します。また、CLAPをさらに大規模に学習させて精度を向上させた研究もあり、オープンソースで公開されています。

MuLan

MuLanは、音楽に特化した対照学習の手法です。CLAPは短い効果音に対して学習を行っていましたが、MuLanは長めの音楽に対して学習を行っています。

研究の動機も少し違っており、CLAPはテキストを使って音声表現を学習することを目指していたのに対して、MuLanは音楽の自動タグ付けや検索を目的として開発されました。MuLanの学習データは、インターネット上の大量のミュージックビデオを利用し、タグ付けやジャンル・アルバム名、動画の説明やコメント、プレイリスト名などの情報をもとにキャプションテキストを作成しています。これにより、音楽分野に特化したデータセットの作成が可能になっています。

さらに、この研究はMusicLMという音楽生成モデルのバックボーンとしても活用されています。

Wav2CLIP

Wav2CLIPは、画像のCLIPを蒸留して共通のCLIP空間に埋め込みを学習し、下流タスクに適用する手法です。

やっていることは単純で、テキストと画像のCLIPを使って、画像エンコーダーを固定し、オーディオエンコーダーを対照学習させます。これにより、少ないデータ量でも教師あり学習に匹敵する精度が得られることが報告されています。YamNetと似た結果が得られますが、Wav2CLIPはアノテーションされたデータが不要であるため、スケーリングが容易だとされています。

また、画像生成の結果が面白くて、Wav2CLIPはクロスモーダルな埋め込みを学習しているため、オーディオやテキストを条件とした画像生成が可能です。例えば、「gun shot」という条件では、テキストと音声に基づく生成画像が大きく異なることが興味深い点です。

その他の応用例

ここでは、CLIPの応用事例を1つ紹介します。

e-CLIP

e-CLIPは、ECサイトでのCLIPの活用例です。

これはNAVERの研究で、ECサイトの商品のテキストと画像を使って、CLIPを大規模に学習させたものです。専門分野に特化したCLIPを作成することで、商品の分類や特徴の抽出などのタスクの性能が向上しています。これにより、ECサイトでのユーザー体験が向上する可能性が示されています。

ただし、性能が極端に向上したわけではなく、通常のCLIPでもそこそこ戦えてるのが興味深いところです。

おわりに

ご紹介した論文は皆様のお役に立てましたでしょうか? CLIPという研究によって、たくさんの新技術が生まれたことが分かると思います。この投稿時点では、CLIPの論文の被引用数は4723と、ディープラーニングの論文の中でもかなり多くなっております。研究の最先端は徐々にCLIPから離れるかもしれませんが、Vision & Languageは引き続き急速に進化していくので、今後とも目が離せないでしょう。

エクサウィザーズでは、Vision & Languageを含め、マルチモーダルな技術の社会実装を推し進めております。興味のある方はぜひこちらをご覧ください。

recruit.exawizards.com

※本記事の作成には、OpenAIの最新のマルチモーダル大規模言語モデルであるGPT-4をフルに活用しました

参考文献

  • Radford, Alec, et al. "Learning transferable visual models from natural language supervision." International conference on machine learning. PMLR, 2021.
  • https://speakerdeck.com/kyoun/deim-tutorial-part-1-nlp
  • https://speakerdeck.com/kyoun/a-tutorial-on-nlp-and-vision-and-language
  • Karras, Tero, et al. "Analyzing and improving the image quality of stylegan." Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. 2020.
  • Gal, Rinon, et al. "StyleGAN-NADA: CLIP-guided domain adaptation of image generators." ACM Transactions on Graphics (TOG) 41.4 (2022): 1-13.
  • Brock, Andrew, Jeff Donahue, and Karen Simonyan. "Large scale GAN training for high fidelity natural image synthesis." arXiv preprint arXiv:1809.11096 (2018).
  • Ramesh, Aditya, et al. "Hierarchical text-conditional image generation with clip latents." arXiv preprint arXiv:2204.06125 (2022).
  • Saharia, Chitwan, et al. "Photorealistic text-to-image diffusion models with deep language understanding." Advances in Neural Information Processing Systems 35 (2022): 36479-36494.
  • Jia, Chao, et al. "Scaling up visual and vision-language representation learning with noisy text supervision." International Conference on Machine Learning. PMLR, 2021.
  • Schuhmann, Christoph, et al. "Laion-5b: An open large-scale dataset for training next generation image-text models." arXiv preprint arXiv:2210.08402 (2022).
  • Zhou, Kaiyang, et al. "Learning to prompt for vision-language models." International Journal of Computer Vision 130.9 (2022): 2337-2348.
  • Sun, Ximeng, Ping Hu, and Kate Saenko. "Dualcoop: Fast adaptation to multi-label recognition with limited annotations." arXiv preprint arXiv:2206.09541 (2022).
  • Jia, Menglin, et al. "Visual prompt tuning." Computer Vision–ECCV 2022: 17th European Conference, Tel Aviv, Israel, October 23–27, 2022, Proceedings, Part XXXIII. Cham: Springer Nature Switzerland, 2022.
  • Bahng, Hyojin, et al. "Visual prompting: Modifying pixel space to adapt pre-trained models." arXiv preprint arXiv:2203.17274 (2022).
  • Shu, Manli, et al. "Test-time prompt tuning for zero-shot generalization in vision-language models." arXiv preprint arXiv:2209.07511 (2022).
  • Khattak, Muhammad Uzair, et al. "Maple: Multi-modal prompt learning." arXiv preprint arXiv:2210.03117 (2022).
  • Huang, Tony, Jack Chu, and Fangyun Wei. "Unsupervised prompt learning for vision-language models." arXiv preprint arXiv:2204.03649 (2022).
  • Hu, Edward J., et al. "Lora: Low-rank adaptation of large language models." arXiv preprint arXiv:2106.09685 (2021).
  • Gao, Peng, et al. "Clip-adapter: Better vision-language models with feature adapters." arXiv preprint arXiv:2110.04544 (2021).
  • Pantazis, Omiros, et al. "SVL-Adapter: Self-Supervised Adapter for Vision-Language Pretrained Models." arXiv preprint arXiv:2210.03794 (2022).
  • Zhang, Renrui, et al. "Tip-adapter: Training-free adaption of clip for few-shot classification." Computer Vision–ECCV 2022: 17th European Conference, Tel Aviv, Israel, October 23–27, 2022, Proceedings, Part XXXV. Cham: Springer Nature Switzerland, 2022.
  • He, Xuehai, et al. "Parameter-efficient fine-tuning for vision transformers." arXiv preprint arXiv:2203.16329 (2022).
  • Jie, Shibo, and Zhi-Hong Deng. "Convolutional bypasses are better vision transformer adapters." arXiv preprint arXiv:2207.07039 (2022).
  • Chen, Hao, et al. "Conv-Adapter: Exploring Parameter Efficient Transfer Learning for ConvNets." arXiv preprint arXiv:2208.07463 (2022).
  • Chen, Shoufa, et al. "Adaptformer: Adapting vision transformers for scalable visual recognition." arXiv preprint arXiv:2205.13535 (2022).
  • Hu, Shell Xu, et al. "Pushing the limits of simple pipelines for few-shot learning: External data and fine-tuning make a difference." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Wortsman, Mitchell, et al. "Robust fine-tuning of zero-shot models." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Ilharco, Gabriel, et al. "Patching open-vocabulary models by interpolating weights." arXiv preprint arXiv:2208.05592 (2022).
  • Li, Junnan, Silvio Savarese, and Steven Hoi. "Masked Unsupervised Self-training for Label-free Image Classification." The Eleventh International Conference on Learning Representations.
  • Tian, Changyao, et al. "Vl-ltr: Learning class-wise visual-linguistic representation for long-tailed visual recognition." Computer Vision–ECCV 2022: 17th European Conference, Tel Aviv, Israel, October 23–27, 2022, Proceedings, Part XXV. Cham: Springer Nature Switzerland, 2022.
  • Eyuboglu, Sabri, et al. "Domino: Discovering systematic errors with cross-modal embeddings." arXiv preprint arXiv:2203.14960 (2022).
  • Gu, Xiuye, et al. "Open-vocabulary object detection via vision and language knowledge distillation." arXiv preprint arXiv:2104.13921 (2021).
  • Li, Liunian Harold, et al. "Grounded language-image pre-training." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Zhong, Yiwu, et al. "Regionclip: Region-based language-image pretraining." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Zhou, Xingyi, et al. "Detecting twenty-thousand classes using image-level supervision." Computer Vision–ECCV 2022: 17th European Conference, Tel Aviv, Israel, October 23–27, 2022, Proceedings, Part IX. Cham: Springer Nature Switzerland, 2022.
  • Minderer, Matthias, et al. "Simple open-vocabulary object detection with vision transformers." arXiv preprint arXiv:2205.06230 (2022).
  • Zhao, Shiyu, et al. "Exploiting unlabeled data with vision and language models for object detection." Computer Vision–ECCV 2022: 17th European Conference, Tel Aviv, Israel, October 23–27, 2022, Proceedings, Part IX. Cham: Springer Nature Switzerland, 2022.
  • Chen, Zhiyang, et al. "Obj2Seq: Formatting Objects as Sequences with Class Prompt for Visual Tasks." arXiv preprint arXiv:2209.13948 (2022).
  • Zhang, Hao, et al. "Dino: Detr with improved denoising anchor boxes for end-to-end object detection." arXiv preprint arXiv:2203.03605 (2022).
  • Liu, Shilong, et al. "Grounding DINO: Marrying DINO with Grounded Pre-Training for Open-Set Object Detection." arXiv preprint arXiv:2303.05499 (2023).
  • Rao, Yongming, et al. "Denseclip: Language-guided dense prediction with context-aware prompting." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Lüddecke, Timo, and Alexander Ecker. "Image segmentation using text and image prompts." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Li, Boyi, et al. "Language-driven semantic segmentation." arXiv preprint arXiv:2201.03546 (2022).
  • Shin, Gyungin, Weidi Xie, and Samuel Albanie. "Reco: Retrieve and co-segment for zero-shot transfer." arXiv preprint arXiv:2206.07045 (2022).
  • Liang, Feng, et al. "Open-vocabulary semantic segmentation with mask-adapted clip." arXiv preprint arXiv:2210.04150 (2022).
  • Xie, Jinheng, et al. "CLIMS: cross language image matching for weakly supervised semantic segmentation." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Kirillov, Alexander, et al. "Segment anything." arXiv preprint arXiv:2304.02643 (2023).
  • Luo, Huaishao, et al. "CLIP4Clip: An empirical study of CLIP for end to end video clip retrieval and captioning." Neurocomputing 508 (2022): 293-304.
  • Wang, Mengmeng, Jiazheng Xing, and Yong Liu. "Actionclip: A new paradigm for video action recognition." arXiv preprint arXiv:2109.08472 (2021).
  • Li, Muheng, et al. "Bridge-prompt: Towards ordinal action understanding in instructional videos." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Gorti, Satya Krishna, et al. "X-pool: Cross-modal language-video attention for text-video retrieval." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Lin, Yan-Bo, et al. "Eclipse: Efficient long-range video retrieval using sight and sound." Computer Vision–ECCV 2022: 17th European Conference, Tel Aviv, Israel, October 23–27, 2022, Proceedings, Part XXXIV. Cham: Springer Nature Switzerland, 2022.
  • Wu, Wenhao, Zhun Sun, and Wanli Ouyang. "Revisiting classifier: Transferring vision-language models for video recognition." Proceedings of the AAAI, Washington, DC, USA (2023): 7-8.
  • Ma, Yiwei, et al. "X-CLIP: End-to-End Multi-grained Contrastive Learning for Video-Text Retrieval." Proceedings of the 30th ACM International Conference on Multimedia. 2022.
  • Zhang, Bowen, et al. "Multimodal Video Adapter for Parameter Efficient Video Text Retrieval." arXiv preprint arXiv:2301.07868 (2023).
  • Patashnik, Or, et al. "Styleclip: Text-driven manipulation of stylegan imagery." Proceedings of the IEEE/CVF International Conference on Computer Vision. 2021.
  • Kwon, Gihyun, and Jong Chul Ye. "Clipstyler: Image style transfer with a single text condition." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Wei, Tianyi, et al. "Hairclip: Design your hair by text and reference image." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Pinkney, Justin NM, and Chuan Li. "clip2latent: Text driven sampling of a pre-trained StyleGAN using denoising diffusion and CLIP." arXiv preprint arXiv:2210.02347 (2022).
  • Xu, Zipeng, et al. "Predict, prevent, and evaluate: Disentangled text-driven image manipulation empowered by pre-trained vision-language model." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Gafni, Oran, et al. "Make-a-scene: Scene-based text-to-image generation with human priors." Computer Vision–ECCV 2022: 17th European Conference, Tel Aviv, Israel, October 23–27, 2022, Proceedings, Part XV. Cham: Springer Nature Switzerland, 2022.
  • Park, Jihye, et al. "LANIT: Language-Driven Image-to-Image Translation for Unlabeled Data." arXiv preprint arXiv:2208.14889 (2022).
  • Tian, Yingtao, and David Ha. "Modern evolution strategies for creativity: Fitting concrete images and abstract concepts." Artificial Intelligence in Music, Sound, Art and Design: 11th International Conference, EvoMUSART 2022, Held as Part of EvoStar 2022, Madrid, Spain, April 20–22, 2022, Proceedings. Cham: Springer International Publishing, 2022.
  • 井尻善久, 牛久祥孝, 片岡裕雄, 藤吉弘亘. コンピュータービジョンの最前線 Summer 2022. 共立出版. 2022.
  • Li, Junnan, et al. "Align before fuse: Vision and language representation learning with momentum distillation." Advances in neural information processing systems 34 (2021): 9694-9705.
  • Singh, Amanpreet, et al. "Flava: A foundational language and vision alignment model." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Shvetsova, Nina, et al. "Everything at once-multi-modal fusion transformer for video retrieval." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Li, Junnan, et al. "Blip: Bootstrapping language-image pre-training for unified vision-language understanding and generation." International Conference on Machine Learning. PMLR, 2022.
  • Zellers, Rowan, et al. "Merlot reserve: Neural script knowledge through vision and language and sound." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Zellers, Rowan, et al. "Merlot: Multimodal neural script knowledge models." Advances in Neural Information Processing Systems 34 (2021): 23634-23651.
  • Alayrac, Jean-Baptiste, et al. "Flamingo: a visual language model for few-shot learning." Advances in Neural Information Processing Systems 35 (2022): 23716-23736.
  • Wang, Jianfeng, et al. "Git: A generative image-to-text transformer for vision and language." arXiv preprint arXiv:2205.14100 (2022).
  • Zhang, Haotian, et al. "Glipv2: Unifying localization and vision-language understanding." Advances in Neural Information Processing Systems 35 (2022): 36067-36080.
  • Lu, Jiasen, et al. "Unified-io: A unified model for vision, language, and multi-modal tasks." arXiv preprint arXiv:2206.08916 (2022).
  • Raffel, Colin, et al. "Exploring the limits of transfer learning with a unified text-to-text transformer." The Journal of Machine Learning Research 21.1 (2020): 5485-5551.
  • Xiang, Wangmeng, et al. "Language supervised training for skeleton-based action recognition." arXiv preprint arXiv:2208.05318 (2022).
  • Tang, Zineng, et al. "TVLT: Textless Vision-Language Transformer." arXiv preprint arXiv:2209.14156 (2022).
  • Wang, Zifeng, et al. "Medclip: Contrastive learning from unpaired medical images and text." arXiv preprint arXiv:2210.10163 (2022).
  • Ji, Yatai, et al. "MAP: Modality-Agnostic Uncertainty-Aware Vision-Language Pre-training Model." arXiv preprint arXiv:2210.05335 (2022).
  • Tewel, Yoad, et al. "Zerocap: Zero-shot image-to-text generation for visual-semantic arithmetic." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2022.
  • Elizalde, Benjamin, et al. "Clap: Learning audio concepts from natural language supervision." arXiv preprint arXiv:2206.04769 (2022).
  • Huang, Qingqing, et al. "Mulan: A joint embedding of music audio and natural language." arXiv preprint arXiv:2208.12415 (2022).
  • Wu, Ho-Hsiang, et al. "Wav2clip: Learning robust audio representations from clip." ICASSP 2022-2022 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP). IEEE, 2022.
  • Shin, Wonyoung, et al. "e-CLIP: Large-Scale Vision-Language Representation Learning in E-commerce." Proceedings of the 31st ACM International Conference on Information & Knowledge Management. 2022.

AI技術を社会実装して課題解決に挑むチームの「技術を理解する&伝える」お話

はじめに

  • こんにちは。AIインキュベーション室で室長をしています長谷川大貴と申します。 エクサウィザーズに入ってそろそろ5年が経過しようとしておりまして、ずっと事業開発やAIを活用したプロジェクトの立ち上げと推進等を数多く実施しておりました。 元々理系出身ではありますが、非エンジニア職です。
  • これまでどんなことしてたの?と言うことをちょろっと知れる記事はこちらにありますので、もしご興味とお時間がある方があればご参照ください。
  • 記事①:「実はここがエクサウィザーズの起源!」AIP西日本事業部ってどんなことやってるの?https://www.wantedly.com/companies/exawizards/post_articles/383676#=
  • 記事②:垣根を超え、想いを形に--。関西からAIの力で情報・教育格差を無くしたい。https://note.exawizards.com/n/n602eb1dcb253

  • そんな非エンジニアがなぜこのブログに!?と思われるところかと思いますが、ずっと事業開発やビジネス側で活動してきたのですが、2022年10月から技術統括側のAIインキュベーション室と言う組織を立ち上げさせていただきエンジニアリングチームとして活動しています。そこで、「技術とビジネスを結び付け、新しい価値を創造する」と言う事に取り組んでおります。

  • 例えば、エンジニアが「新しい技術シーズを開発したけどこれって課題解決に活用できないかな?」と言う技術を理解してニーズに繋いだり、企業等のニーズに対してそう言った技術シーズの蓄積から解決につながる提案を実施したりしています。
  • 私自身はAIの研究者でもゴリゴリコーディングをしているわけでもないのですが、何件もの技術を理解し、活用のアイデアを検討しています。今回は技術を理解する時に私が心がけていることを少しシェアさせていただければなと思っております。 ビジネス側の方は「そういう感じで技術理解しているのかー」と思っていただいたり、エンジニア側の方は「こう言う観点で技術を伝えたら理解されやすいのかな―」と言うヒントに使っていただけると嬉しいです。

「こんな技術があるんですけど!」と言われたら(言いたくなったら)

  • 技術の紹介や相談を受けるとき個人的に大事に思ってるのは双方のリスペクトと知的好奇心の強さです。リスペクトは仕事をするうえでは当たり前なのですが、ビジネスパーソンとエンジニアだったり、経営者とエンジニアだったりは一般的には思考方法が異なるので、双方向の尊重は前提にあるべきだと考えています。
  • 知的好奇心については、私自身は元々強い方だと思っているのですが、「え?面白いじゃないですか!どんな技術なの!?どんどん質問しちゃっていいですか!?」と言うスタンスで聞けることが大事かなと思ってます。その方が、技術の理解や深堀のスピードが上がりますし、活用のアイデアも技術をヒアリングしていく中で出やすい雰囲気になると思います。技術を紹介する時に何となく相手が引いているなと思ったら、相手の知的好奇心をくすぐる情報をアイスブレイク的に放り込むのもありだと思います。
  • 私がお客さんに技術を伝える時、知的好奇心をくすぐるために「ついつい誰かに話したくなる一ネタを入れる」と言う事をよくやっています。 例えば、最近話題のChatGPTは非常に高い精度でテキスト生成をすると言うモデルですが、文章を生成すると言う事は1文字誤りやノイズがあれば、意味が変わってくるケースがあります。実際にあった答えなのですが、「免疫力をつけるにはどうすれば良いですか?」と言う問いの答えの一つに「週あたり7時間以上の睡眠を心がけましょう」と言うのがありました。恐らく正しくは「日あたり7時間以上の睡眠を心がけましょう」だと思うのですが、1文字違うと途端にハードコアな働き方を要求する文章に変わってしまいます。 生成にはこう言う誤った情報を生成してしまうリスクが少なからずあるので、もし可能な限り情報ソースに忠実な文章要約をAIで実現したい場合は、上記のような生成系の技術ではなく抽出型要約(元の文章を抽出して要約を作る方法)を選択することもできますよと言う少しクスッとするエピソードとともに紹介すると「確かに」と技術選択の腹落ち感が増したり、「この要約AIのポイントは元の文章は維持して意味が伝わる内容に要約することなんですよ。生成系技術を使うとこんなリスクもあるんですよ~」とお客さんも社内に技術の特徴を紹介しやすくなりますので、そう言う一ネタを入れることをよくやってます。

「どんな技術か」を理解する(伝える)

  • 前述のようなスタンスで技術をヒアリングしたり議論したりする中で私がおおよそいつも聞くようにしている要素を参考にご紹介します。技術の種類によっては他の色々なことももちろん聞きますが、下記の要素はどう言ったものであっても知っておくべきポイントだと思って聞いています。
  • それぞれを網羅的に聞いていくというよりは、できるだけ自分も知的好奇心を満たすような流れで自然に埋めていけるようにしようとするコミュニケーションの工夫もよくしています。「その技術って○○○ができるんですよね?それって×××みたいな用途でも使えたりしますか??・・・でももしかしたらこういう時は使えないですか??」等々

1. 【概要】どんな技術?何ができる?

  • その技術ができることやどのような技術であるかを自分の言葉で説明できる程度に理解したい。伝聞で自分が説明した時でもユーザーに技術の良さと面白さを伝えられるようにしたいと思い聞いています。

2. 【背景】なぜこの技術に注目した?開発しようと思った背景は?

  • エンジニアがときめいたポイントや良いと思った背景があれば聞きたい。もしくはその他の理由「得意な技術だから、過去にしっかり実装した経験があるから等」があるのであれば、バックグラウンドを理解して、技術の魅力を伝えられるようになりたいと思い聞いています。

3. 【原理】技術や手法の仕組みやポイントは?

  • 技術の原理や仕組み、実装の工夫やポイント等を理解し、技術ができることをある程度の深さの原理のレベルで理解したい。そうすることでユースケースを考えるときの実現性やユーザーの納得感「確かにそういう技術であれば実現できそうだ」を付加するために聞いています。

4. 【強み】既存技術と比べてすごい点はある?特徴はある?

  • 技術の強みや特徴、他との差別化ポイントを把握して、「これまでできなかったことができるようになった」だったり、「これまで実現しようと思ったら時間や費用が掛かっていたものが少なくて済むようになった」等の技術の強みによってユースケースの付加価値が高いポイントを作るために聞いています。

5. 【限界】利用の前提や制約、注意点はある?

  • できないことを明確にし、前提や条件がある時にはそれを含めてユーザーに提案し「嘘」や「誇張」、「過剰な期待値」を生まない誠実な提案を作成するために注意して聞くようにしています。

6. 【応用】技術の応用範囲は?こんなことには使える?あんなことには使える?

  • こう言う用途にも使えるか?工夫したらこう言うこともできるか?等々技術を応用可能な幅を具体的なユースケースの案を仮説として提示しながらできる範囲をイメージしていきます。ここでどの程度の幅でアイデアが出るか、具体的にユーザーが欲しいと思うものの仮説を出せるかがビジネスパーソンが普段接している顧客のニーズの質や量に差が出て面白い部分だと思います。

  • 数が出て議論が盛り上がるケースもあれば、とても具体的かつ根が深いニーズにマッチして盛り上がるケースもあるので、ある程度色々なメンバーで議論した方が面白い応用案が出て良いと個人的には感じています。例えば、エクサウィザーズと言う会社は面白い会社で多様なバックグラウンドの方が多くいらっしゃるのですが、ビジネスとエンジニアで議論していて煮詰まった時に、ケア事業を実施している介護士の方から「その技術であればこういうことにもしかして使えないか?」と言う発言をいただいて思いもよらなかったユースケースが誕生することもあったりします。

「何に使えるか」を想像し、実際にユーザーに提案をしに行く

  • 上記までで技術の概要が理解できれば、技術の強みや限界を加味したうえで、今まで蓄積しているニーズ等を思い返しながら「こう言ったユースケースだと欲しい人がいるのでは」と言う仮説を作ります。その仮説を作ったら実際ターゲットに思い描いていたユーザーに実際に提案しに行ったりディスカッションによってニーズを確認しに行きます。 そこで一発で「欲しい!」となることは稀で、反応を踏まえて提案のチューニングをしたり仮説の修正をしたりすることが多いです。結構この辺りが大変なのですが、こちらはまた機会があれば・・・

まとめ

  • 技術をビジネスに転換していく際にはエンジニア、ビジネスパーソン、デザイナー等々のそれぞれのメンバーが技術と相手をリスペクトし、知的好奇心を持ってディスカッションを実施した方がポジティブな議論ができる
  • 技術を伝える&聞くときにはある程度普遍的に共有した方が良い内容は事前に整理したり意識しながら共有した方が理解は加速する
  • もし技術&ビジネス間の連携のお話やエクサウィザーズと言う会社に少しでもご興味を持っていただけましたら、下記サイトをご覧くださいhttps://hrmos.co/pages/exawizards/jobs?

Apolloを利用したGraphQLリクエストのパフォーマンスをFirebase Performance Monitoring で測定する

こんにちは。エクサウィザーズの介護記録AIアプリ「CareWiz ハナスト」(以下ハナスト)でiOSアプリ開発を担当している伊賀(@iganin_dev)です。

ハナストのテックリードの原のブログ記事にもありましたように、ハナストではAPI通信にGraphQLを利用しています。 本稿ではiOSアプリの通信ライブラリとしてApolloを用いた場合のGraphQLリクエストのパフォーマンスをFirebase Performance Monitoring(以下FPM)を使用して測定する方法に関して記載します。

環境

本稿記載の内容は以下環境を前提に記載しています。

  • Xcode 14.0
  • apollo-ios 0.51.0 (※ 1.0.0へのバージョンアップ検証中)
  • firebase-ios-sdk 9.6.0

ハナストについて

本題に入る前に「CareWiz ハナスト」に関して簡単にご紹介します。 ハナストは簡単に言うと「音声入力で介護の記録をするアプリ」です。

以下のLPによくまとまっています。 利用シーンを紹介するデモビデオもありますので、是非ご覧ください。

hanasuto.carewiz.ai

Firebase Performance Monitoringとは

FPMはGoogleが提供しているFirebaseの機能群の一つです。 ネットワークリクエストをはじめ、さまざまな処理にかかった時間や処理の結果(成功・エラー)などを記録、集計しGUIを通してグラフィカルに確認することができます。

ライブラリを追加するのみでアプリの起動時間や画面の滞在時間などを測定してくれる非常に便利なツールです。 ネットワークリクエストも自動的に計測し、カスタムURLパターンを作成すれば特定のリクエストの計測もできます。

FPMでGraphQLリクエストのパフォーマンスを測定する場合の問題点

非常に便利なFPMですが、GraphQLのリクエストを測定しようとした場合に問題が発生します。GraphQLのリクエストは一般的には同一エンドポイントへのPOSTリクエストとなります。例えば、 https://sample.com/graphql のようなPOSTリクエストのBodyにQueryやMutationのGraphQLドキュメントをのせリクエストを送ります。

FPMではリクエストのパフォーマンスをURLのパスを元に分類します。従って、GraphQLリクエストのパフォーマンスを測定しようとした場合、そのままではすべての計測結果がsample.com/graphqlのような単一のエンドポイントに集約されてしまい、各リクエストのパフォーマンスを別々に見ることができません。それではどのようにすれば、GraphQLリクエストのパフォーマンスをFPMで測定できるのでしょうか。

カスタムネットワークリクエストトレースについて

FPMでは自動収集するリクエストトレース以外に、開発者にて実装できるカスタムネットワークリクエストトレースを用意しています。HTTPMetricをurlとhttpMethodを引数で渡して初期化し、start()stop()を呼び出すことで、start()からstop()を呼び出すまでの時間を計測することができます。FPMのGUI上ではここで指定したurlがパスの分類に使用されます。また、HTTPMetricにはリクエストやレスポンスのペイロードサイズ、レスポンスのステータスコードを登録することもできます。

guard let metric = HTTPMetric(url: url, httpMethod: .post) else { return }
metric.start()
metric.requestPayloadSize = requestPayloadSize

Task {
    do {
        // ネットワークリクエスト実行
        let (data, response) = try await URLSession.shared.data(from: url)
        metric.responsePayloadSize = responsePayloadSize
        metric.responseCode = response.httpResponse.statusCode
        metric.stop()
    } catch let error {
        // エラーハンドリング
        metric.stop()
    }
}

ハナストでの解決方法

先ほどの全てのGraphQLリクエストの計測結果がまとめて集約されてしまうという問題に対して、ハナストではカスタムネットワークリクエストトレースの仕組みを活用して対処しています。基本的な解決方法としてはリクエストごとにURLのパスを分けるというものです。

Apolloを用いて自動生成されたQueryやMutationのclassにおいて、そのOperationの名称をoperationNameで取得することができます。例えば、以下のようなQueryをベースにSampleQuery.graphql.swiftのようなファイルが生成されます。

query sample {
    user {
        id
        name
    }
}
public final class SampleQuery: GraphQLQuery {
    ...
    public let operationName: String = "sample"
    ...
}

このoperationNameは作成したGraphQLファイルのqueryやmutationの名称と1対1で対応するため、operationNameをもとにリクエストを一意に特定することができます。そのため、さきほどのHTTPMetric初期化時に渡すurlにこのoperationNameを付与することでリクエストのPathがGraphQLのリクエストごとに異なるようになり、FPMのGUI上で各リクエストのパフォーマンスを測定することができるようになります。

実装

先ほどoperationNameをURLに付与してリクエストを一意に特定することで問題に対処するとお伝えしました。次にApolloを用いた場合の実装例をご紹介します。具体的な実装に入る前に、Apolloを用いた実装を確認しておきましょう。 まず、Apolloの初期化は下記のように行われます。

let cache = InMemoryNormalizedCache()
let store = ApolloStore(cache: cache)
let client = URLSessionClient()
let transport = RequestChainNetworkTransport(
        interceptorProvider: SampleInterceptorProvider(
            store: store,
            client: client,
            apiConfig: apiConfig
        ),
        endpointURL: endpointURL
    )
ApolloClient(networkTransport: transport, store: store)

Apolloからリクエストを送ると、interceptorProviderfunc interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor]メソッドが提供するApolloInterceptorが通信リクエストの内容とレスポンスにさまざまな処理を加えたり、それらをもとにさまざまな処理を行い、最終的な返却値を返します。なお挙動を確認したところ、func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor]はApolloからリクエストを送る都度呼び出されていました。

final class SampleInterceptorProvider {
    func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor] {
        var interceptors: [ApolloInterceptor] = []
        // pre fetch interceptors - fetch前に行いたい処理のinterceptorをここに実装します
        interceptors.append(NetworkFetchInterceptor(client: client))
        // post fetch interceptors - fetch後に行いたい処理のinterceptorをここに実装します
    }
}

今回の実装では、FPMの計測開始を担当するStartFPMMetricInterceptorとFPMの計測終了を担当するStopFPMMetricInterceptorを実装し、interceptorsNetworkFetchInterceptorの前後に追加することでリクエストの都度カスタムネットワークリクエストトレースが行われるようにしています。 StartFPMMetricInterceptorの実装は下記のようになっています。

final class StartFPMMetricInterceptor: ApolloInterceptor {
    init(performanceMonitor: any NetworkPerformanceMonitorable) {
        self.performanceMonitor = performanceMonitor
    }

    private let performanceMonitor: any NetworkPerformanceMonitorable

    func interceptAsync<Operation>(
        chain: RequestChain,
        request: HTTPRequest<Operation>,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) where Operation: GraphQLOperation {
        let operationName = request.operation.operationName
        let url = request.graphQLEndpoint.appendingPathComponent("/\(operationName)")
        let requestPayloadSize = try? request.toURLRequest().urlRequest?.httpBody?.count
        performanceMonitor.start(url: url, method: .post, requestPayloadSize: requestPayloadSize)
        chain.proceedAsync(request: request, response: response, completion: completion)
    }
}

ApolloInterceptorに準拠した場合、interceptAsync<Operation>メソッドを実装する必要があります。 このメソッド内で、requestからoperationNameを取得し、urlのパスにoperationNameを追加しています。 StopFPMMetricInterceptorの実装は下記のようになっています。

final class StopFPMMetricInterceptor: ApolloInterceptor {
    init(performanceMonitor: any NetworkPerformanceMonitorable) {
        self.performanceMonitor = performanceMonitor
    }

    private let performanceMonitor: NetworkPerformanceMonitorable

    func interceptAsync<Operation>(
        chain: RequestChain,
        request: HTTPRequest<Operation>,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) where Operation: GraphQLOperation {
        let statusCode = response?.httpResponse.statusCode ?? 0
        let responsePayloadSize = response?.rawData.count
        performanceMonitor.stop(statusCode: statusCode, responsePayloadSize: responsePayloadSize)
        chain.proceedAsync(request: request, response: response, completion: completion)
    }
}

ネットワークのレスポンスからstatusCodeやレスポンスペイロードサイズを取得し、HTTPMetricに渡しています。 NetworkPerformanceMonitorableはFPMのHTTPMetricの生成や保持、startstopの呼び出しを責務に持つプロトコルです。 NetworkPerformanceMonitorableと、それに準拠したFirebaseNetworkPerformanceMonitorの実装は下記のようになっています。

計測の開始時点で呼び出すstart(url:,method:)でurlとリクエストのペイロードサイズを渡し、そのタイミングでHTTPMetricを生成して保持しています。その後、計測の終了時点で呼び出すstop(statusCode:, responsePayloadSize:)にてレスポンスのペイロードサイズとstatusCodeをわたし、HTTPMetricstopを呼び出しています。

public protocol NetworkPerformanceMonitorable {
    func start(url: URL, method: PerformanceMonitorHttpMethod, requestPayloadSize: Int?)
    func stop(statusCode: Int, responsePayloadSize: Int?)
    func cancel()
}

public final class FirebaseNetworkPerformanceMonitor: NetworkPerformanceMonitorable {
    public init() {}
    private var metric: HTTPMetric?
    
    public func start(url: URL, method: PerformanceMonitorHttpMethod, requestPayloadSize: Int?) {
        let metric = HTTPMetric(url: url, httpMethod: method.convertToFirebaseHttpMethod())
        metric?.requestPayloadSize = requestPayloadSize ?? 0
        self.metric = metric
        metric?.start()
    }
    
    public func stop(statusCode: Int, responsePayloadSize: Int?) {
        guard let metric else { return }
        metric.responsePayloadSize = responsePayloadSize ?? 0
        metric.responseCode = statusCode >= 0 ?  statusCode : nil
        metric.stop()
    }
    
    public func cancel() {
        metric = nil
    }
}

最後にInterceptorProviderinterceptorsメソッドの返却値に上述のクラス群を追加すれば完成です。

final class SampleInterceptorProvider {
    func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor] {
        let performanceMonitor = FirebaseNetworkPerformanceMonitor()
        var interceptors: [ApolloInterceptor] = []
        // pre fetch interceptors - fetch前に行いたい処理のinterceptorをここに実装します
        interceptors.append(StartFPMMetricInterceptor(performanceMonitor: networkPerformanceMonitor))

        interceptors.append(NetworkFetchInterceptor(client: client))

        // post fetch interceptors - fetch後に行いたい処理のinterceptorをここに実装します
        nterceptors.append(StopFPMMetricInterceptor(performanceMonitor: networkPerformanceMonitor))
    }
}

これでApolloを用いたGraphQLリクエストの都度StartFPMMetricInterceptorStopFPMMetricInterceptorで計測のstartとstopが呼ばれるようになり、リクエストにかかった時間を計測できるようになります。 GUI上では例えば下記のように表示されます。

補足

今回の実装はGraphQLリクエストのCachePolicyとして.fetchIgnoringCacheCompletelyを指定した場合を想定しています。 他のCachePolicyを使用した場合はさらに考慮が必要になるだろうと思います。

まとめ

今回はハナストでGraphQLリクエストのパフォーマンスをどのように測定しているのかをご紹介しました。

本稿ではご紹介できませんでしたが、エッジでの音声認識、ウェイクワード検知、フルSwiftUIでのアプリ開発、Swift Concurrencyの実践的導入などハナストiOSアプリの開発はチャレンジングで面白い課題に日々挑戦しています。

CareWiz事業部およびエクサウィザーズでは社会課題の解決に一緒に取り組む仲間を募集しています。 介護をより良くするプロダクトの開発、あるいはAIで社会課題を解決するエクサウィザーズに少しでも興味がありましたら、是非ご応募ください!

hrmos.co

hrmos.co

open.talentio.com

PreloadResolverという仕組みを作ってGraphQLのN+1問題に対応した話

エクサウィザーズでハナスト開発チームのTLをしている原です。

ハナストは「音声入力で介護の記録をするアプリ」で、こちらのページでプロダクトの紹介をしています。

hanasuto.carewiz.ai

以前は、ハナストAPIのテストについてこちらの記事で書きました。

techblog.exawizards.com

今回の記事ではハナストのAPIで実践している、PrelaodResolverというGraphQLのN+1問題対応の仕組みを紹介します。

GraphQLのN+1問題

まず、GraphQLのN+1問題がどのように発生するかを簡単に説明します。

例えば

type Query {
  books: [Book!]!
}

type Book {
  id: ID!
  author: Author!
}

type Author {
  id: ID!
  name: String!
}

というスキーマがあったとします。

ここで、BookResolverが

class BookResolver {
  async author(book: Book): Promise<Author> {
    return findAuthorById(book.authorId)
  }

  ...
}

のような実装になっていたとすると

books {
  author { name { name }  }
}

というクエリでデータを取得した時に、Bookの数だけfindAuthorByIdが実行されてしまいます。

できればAuthorは

findAllAuthorsByIds(books.map((_) => _.authorId))

のような処理でまとめて取得するべきなのですが、そうではなく取得したBookの数だけfind処理が実行されてしまうのがN+1問題です。

DataLoaderを使う場合

GraphQL のN+1問題の対処にはDataLoaderを使うのが一般的です。

DataLoaderを使うと上記の例だと

const authorLoader = async (books: Book[]): Promise<Author[]> => {
  const authorIds = books.map(books.map((_) => _.authorId))
  const authors = await findAllAuthorsByIds(books)
  const authorMap = arrayToMap(authors)
  return books.map((book) => authorMap[book.authorId])
}

というDataLoaderを用意して、こちらをBookResolverに関連付けて、Book#authorが必要な場合はこのDataLoaderを呼んで、その結果からAuthorを取得するようになります。

実際の組み込み方はDataLoaderのライブラリによりますが、大まかな処理はこのようになります。

ハナストGraphQL APIでのN+1問題対策

DataLoaderでも一通りのN+1問題対策はできるのですが、ハナストではPreload Resolverという仕組みを作って、こちらでN+1問題の対策をしています。

PreloadResolverでのN+1問題対策

PreloadResolverを使った仮想コードはこのようになります。

class BookPreloadResolver {
  async preload(
    books: Book[],
    path: string[],
    info: GraphQLResolveInfo
  ): { authors: Author[] } {
    let authors: Author[] = []

    // GraphQLスキーマを分析して、Book#authorの取得が必要か判定する
    if(findFieldInSchema(path, 'author', info)) {
      authors = await this.preloadAuthor(books)
    }

    return { authors }
  }

  private async preloadAuthor(books: Book[]): Promise<Author[]> {
    const authorIds = books.map(books.map((_) => _.authorId))
    authors = await findAllAuthorsByIds(authorIds)
    const authorMap = arrayToMap(authors)

    books.forEach((book) => {
      const author = authorMap[book.authorId]
      book.preloadAuthor(author)
    })
  }

  ...
}

class Book {
  private preloadedAuthor: Author | undefined = undefined

  preloadAuthor(author: Author): void {
    this.preloadedAuthor = author
  }

  loadAuthor(): Author {
    if(this.preloadedAuthor) {
      return this.preloadedAuthor
    } else {
      // preloadされていない場合は例外を投げる
      throw new Error('author is not preloaded.')
    }
  }
}

class BookResolver {
  author(book: Book): Author {
    // preload済みのAuthorオブジェクトを取得する
    return book.loadAuthor()
  }
}

class QueryResolver {
  constructor(
    private readonly bookPreloadResolver: BookPreloadResolver,
  ) {
  }

  async books(info: GraphQLResolveInfo): Promise<Book[]> {
    const books = await findAllBooks()
    await bookPreloadResolver.preload(books, ['books'], info)
    return books
  }
}

PreloadResolverではDataLoaderとは異なり、BookオブジェクトにAuthorをpreloadしています。

参照側はpreload済みのAuthorオブジェクトを返して、preloadされていない場合は例外を投げるようにしています。

このような仕組みにすることで

  • ネストしたN+1問題への対処
  • GraphQL field毎のpreload可否の設定

がやりやすくなります。

以下で、それぞれについて詳しく説明します。

PreloadResolverによるネストしたN+1問題への対処

例えば

type Author {
  location: Location!
}
type Location {
  address: String!
}

のようにBook#authorsからさらにネストしてAuthor#locationを取得する必要がある場合は、以下のようにPreloadResolverを呼び出します。

class QueryResolver {
  constructor(
    private readonly bookPreloadResolver: BookPreloadResolver,
    private readonly authorPreloadResolver: AuthorPreloadResolver,
  ) {
  }

  async books(info: GraphQLResolveInfo): Promise<Book[]> {
    const books = await findAllBooks()
    const { authors } = await bookPreloadResolver.preload(books, ['books'], info)
    await authorPreloadResolver.preload(authors, ['books', 'author'], info)
    return books
  }
}

class AuthorPreloadResolver {
  // locationのpreload処理を実装
}

class AuthorResolver {
  // preload済みのlocation取得処理を実装
}

こうすることでBook#authorだけでなく、Author#Locationもpreloadされます。

GraphQL field毎のpreload可否の設定

例えば

type Query {
  books: [Book!]!
  authors: [Author!]!
}
type Book {
  author: Author!
  category: Category!
}
type Author {
  books: [Book!]
}
type Category {
  id: ID!
  name: String!
}

のようなGraphQLスキーマとなっていて、

  • books { author { name } }
  • authors { books { category { name } } }

はOKだけど、

  • authors { books { author { name } } }

のような循環参照のpreloadは禁止したいというケースがあります。

このような場合、まずPreloadResolverをこのように実装します。

type BookPreloadFields = {
  author?: AuthorPreloadFields
  category?: CategoryPreloadFields
}

type AuthorPreloadFields = {
  books?: BookPreloadFields
}

type CategoryPreloadFields = {}

class BookPreloadResolver {
  async books(
    path: string[],
    info: GraphQLResolveInfo,
    options: { fields: BookPreloadFields }
  ): Promise<{ authors: Author[], categories: Category[] }> {
    let authors: Author[] = []
    let categories: Category[] = []

    // GraphQLスキーマを分析して、authorの取得が必要か判定する
    if(findFieldInSchema(path, 'author', info)) {

      // authorの取得が禁止されていたら例外を投げる
      if(options.fields.author == undefined) {
        throw new Error(`Book#author preload is forbidden at ${path.join('.')}`)
      }

      // 許可されていたらauthorをpreloadする
      authors = await this.preloadAuthor(books)
    }

    // GraphQLスキーマを分析してcategoryの取得が必要か判定する
    if(findFieldInSchema(path, 'category', info)) {

      // categoryの取得が禁止されていたら例外を投げる
      if(options.fields.category == undefined) {
        throw new Error(`Book#category preload is forbidden at ${path.join('.')}`)
      }

      // 許可されていたらcategoryをpreloadする
      categories = await this.preloadCategory(books)
    }

    return { authors, categories }
  }
}

このQueryResolver側では以下のようにPreloadResolverを使います。

class QueryResolver {
  ...

  async books(info: GraphQLResolveInfo): Promise<Book[]> {
    const books = await findAllBooks()

    await this.bookPreloadResolver.preload(
      books,
      ['books'],
      info,
      // books { author { * } category { * } } を許可する
      { fields: { author: {}, category: {} } }
    )
    return books
  }

  async authors(info: GraphQLResolveInfo): Promise<Author[]> {
    const authors = await findAllAuthors()
    const { books } = await this.authorPreloadResolver.preload(authors, ['authors'], info)
    await this.bookPreloadResolver.preload(
      books,
      ['authors', 'books'],
      info,
      {
        fields: {
          // authors { books { author { * } } } は禁止する
          author: undefined,
          // authors { books { category { * }  } } を許可する
          category: {}
        }
      }
    )
    return authors
  }
}

PreloadResolverを使った再帰的なpreload

PreloadResolverを使って再帰的なpreload処理を行うことも可能です。

例えばBookとAuthorを再帰的にpreloadする処理はこのようになります。

class GraphQLPreloadResolver {
  constructor(
    private readonly bookPreloadResolver: BookPreloadResolver,
    private readonly authorPreloadResolver: AuthorPreloadResolver,
  } {
  }

  async preloadBook(
    books: Book[],
    path: string[],
    info: GraphQLResolveInfo,
    options: { fields: BookPreloadFields }
  ): Promise<void> {
    // booksが空の場合は再帰処理を終了
    if(books.length == 0) { return }

    const { authors } = await this.bookPreloadResolver.preload(books, path, info, options)

    // 再帰的にauthorsのpreload処理を行う
    await this.preloadAuthor(
      authors,
      path.concat(['author']),
      info,
      { fields: options.fields.author ?? {} }
    )
  }

  async preloadAuthor(
    authors: Author[],
    path: string[],
    info: GraphQLResolveInfo,
    options: { fields: BookPreloadFields }
  ): Promise<void> {
    // authorsが空の場合は再帰処理を終了
    if(authors.length == 0) { return }

    const { books } = await this.authorPreloadResolver.preload(authors, path, info, options)

    // 再帰的にbooksのpreload処理を行う
    await this.preloadBook(
      books,
      path.concat(['author']),
      info,
      { fields: options.fields.books ?? {} }
    )
  }
}

このGraphQLPreloadResolverを使って

  • book(id: 1) { author { books { name } } }
  • books { author { name } }
  • authors { books { name } }

というpreloadをしようとする場合は、QueryResolverをこのように書きます。

class QueryResolver {
  constructor(
    private readonly graphQLPreloadResolver: GraphQLPreloadResolver
  ) {}

  async book(args: { id: string }, info: GraphQLResolveInfo): Promise<Book | null> {
    const book = await findBookById(args.id)
    if(!book) { return null }
    await this.graphQLPreloadResolver.preloadBook(
      [book],
      ['book'],
      info,
      // books { author { books { * } } }を許可する
      { fields: { author: { books: {} } } }
    )
    return book
  }

  async books(_args: {}, info: GraphQLResolveInfo): Promise<Book[]> {
    const books = await findAllBooks()
    await this.graphQLPreloadResolver.preloadBook(
      books,
      ['books'],
      info,
      // books { author { * } }を許可する
      { fields: { author: {} } }
    )
    return books
  }

  async authors(_args: {}, info: GraphQLResolveInfo): Promise<Author[]> {
    const authors = await findAllAuthors()
    await this.graphQLPreloadResolver.preloadAuthor(
      authors,
      ['authors'],
      info,
      // authors { books { * } }を許可する
      { fields: { books: {} } }
    )
    return authors
  }
}

再帰処理で実装することで、冗長な処理が少なく記述できているかと思います。

PreloadResolverによるN+1問題対応のメリット&デメリット

個人的にはPreloadResolverには以下のようなメリットがあると思っています。

  • 処理の流れがわかりやすい
  • ネストしたN+1問題に再帰処理で対応できる
  • field毎のpreloadの許可・禁止を設定しやすい

一方で、DataLoaderで困らない用途であればDataLoaderを使った方が記述量は少なく済むかと思うので、その辺りは用途に応じて使い分けるのが良いかと思います。

まとめ

今回はハナストのGraphQL APIでのN+1問題への対応としてPreloadResolverというアプローチをご紹介しました。

ハナストチームではGraphQL技術を活用して介護領域での音声AIサービスの開発を行なっており、一緒に働いていただける方を積極的に募集しています。

GraphQL技術を使った社会課題の解決などに少しでも興味がありましたら、ハナストチームおよびエクサウィザーズに是非ご応募ください。

hrmos.co

日経コンピュータ・日経xTECHで、エクサウィザーズの機械学習エンジニアによる連載を掲載しています

 日経コンピュータの5月26日号(日経BP)から、エクサウィザーズの機械学習エンジニアを中心とした著者による長期連載が始まりました。AI技術の最新動向と応用事例について解説していきます。

●5回目はエクサウィザーズ 機械学習エンジニアのサヒリ・モハメッド、浅谷 学嗣が担当しました。

「AIモデルと処理の軽量化 エッジデバイスで必須に」

IoTでエッジデバイスにおけるAI(人工知能)活用が広がっている。コストと性能を両立させるために必須なのがAIモデルの軽量化だ。ただし手法が数多くあり、選択や活用に注意が必要だ。

▽詳しくは下記をご覧ください(外部リンク、2ページ目以降有料)

xtech.nikkei.com

●4回目はエクサウィザーズ 機械学習エンジニアの石丸 裕吾、西日本事業部/エネルギー環境企画部 事業部長の長谷川 大貴が担当しました。

「数理最適化で意思決定 予測×制約で導き出す」

AIのビジネス活用において数理最適化の重要性が高まっている。現場や経営で必要とされる制約条件を考慮できるからだ。成果を得るための意思決定においてさまざまな分野で活用され始めている。

▽詳しくは下記をご覧ください(外部リンク、2ページ目以降有料)

xtech.nikkei.com

●3回目はエクサウィザーズ 機械学習エンジニアの神戸宏之が担当しました。

「追加学習が不要な「GPT-3」 文章生成などビジネス活用も」

「GPT-3」は自然言語処理分野にパラダイム変化をもたらした。テキストを入力するだけで、それに「答える文章」の予測が可能になったからだ。課題は多いが、マーケティング文章の生成などビジネス活用が始まっている。

▽詳しくは下記をご覧ください(外部リンク、2ページ目以降有料)

xtech.nikkei.com

●2回目はエクサウィザーズ 機械学習エンジニアの小野晃司が担当しました。

プライバシー保護の切り札 「連合学習」が普及期に

「連合学習」はデータそのものを収集せず機械学習モデルを作成できる。携帯の予測変換やクッキー代替などでの活用が始まっている。プライバシー重視のヘルスケアや金融などでの活用が有望視されている。

▽詳しくは下記をご覧ください(外部リンク、2ページ目以降有料)

xtech.nikkei.com

●初回はエクサウィザーズ AI技術統括の遠藤太一郎が担当しました。

「AIの精度を左右する3技術 4ギルドで体制づくり」

AIの精度を左右する最新動向として3つの技術を紹介する。「自己教師あり学習」「マルチモーダル」「MLOps」をうまく取り入れる必要がある。4つのギルドから成る組織体制が、最新動向のキャッチアップに欠かせない。DX(デジタルトランスフォーメーション)の推進が企業の重要課題となっており、その差異化の手段としてのAI(人工知能)に対する期待は高まるばかりだ。AIの主たる要素である機械学習は近年どのように進展し、ビジネスに活用されるようになっているのか。本連載では機械学習のビジネス応用を専門とする筆者が、最新動向と企業事例について解説する。

▽詳しくは下記をご覧ください(外部リンク、2ページ目以降有料) xtech.nikkei.com

エクサウィザーズのTLが実践する、開発が遅くならないテストの書き方

この記事について

この記事ではエクサウィザーズの介護記録AIアプリ「CareWiz ハナスト」(以下ハナスト)の開発スピードを維持するために、どのようにテストを書いているかをご紹介します。

内容としては基本的なことかと思うので、ハナスト開発ではどのような基本に則ってテストしているかという感じで読んでいただければ良いかと思います。

書いているのは誰?

この記事はハナスト開発チームのテックリードをしている原(@haracane)が書いています。

ハナストチームでは主にNode.js&TypeScriptでバックエンドAPIを開発していてテストにはJestを使っています。

ちなみにこれまではKotlin&JUnitやRuby on Rails&Rspecなどで開発&テストをしたりしてました。

ハナストについて

ハナストは簡単に言うと「音声入力で介護の記録をするアプリ」です。

以下の動画を見ていただくと、大体どんなアプリかわかるかと思います。

vimeo.com

ハナストの開発プロセス

やや脱線しますが、ハナストの開発プロセスについてはこちらのnote記事によくまとまっています。

note.exawizards.com

とても良いことが書いてあるので是非読んでいただきたいのですが、この記事に関係するところで言うと、ハナストの開発はあくまで「仮説検証のためにやっている」というところがポイントです。

効果的に仮説検証を進めるには開発スピードを維持することが重要になります。

そのためにどのような工夫をしているか、ということをこの記事にはまとめています。

ハナストの構成

続いてハナストの実装について紹介すると、ハナストは大きく分けて

  • バックエンド API
  • 音声認識 AI
  • iOS アプリ

の3つで構成しています。

今回は主にハナストAPIでどのようにテストしているかをご紹介します。

ハナストAPIのテスト

ハナストAPIはGraphQL APIとして提供していて、Clean Architectureで設計しています。

テストは主にGraphQLのリクエスト&レスポンスのテストを書いています。

例えば記録を作成するGraphQL APIのテストだとこんな感じです。

describe('createCard', () => {
  describe('with food input', () => {
    let response

    beforeEach(async () => {
      response = await graphQLRequest(`
        mutation createCard {
          createCard(type: "food", amount: 10) {
            id
            type
            amount
          }
        }
      `)
    })


    it('creates food record & renders food record', async () => {
      const cardId = response.data.createCard.id
      const card = await new CardRepository().findById(cardId)
      expect(card).toMatchObject({
        type: 'food',
        amount: 10,
      })

      expect(response).toEqual({
        data: {
          createCard: {
            id: cardId,
            type: 'food',
            amount: 10,
          }
        }
      })
    })
  })
})

逆にユニットテストは極力書かないようにしています。

上記のような記録作成の例だと、Layer毎に

  • GraphQL Layer
    • MutationResolver#createCard
  • UseCase Layer
    • CardService#create
  • Database Layer
    • CardRepository#create

のようなクラス&メソッドを実装していますが、各メソッドのユニットテストは書いていません。

その理由についてご説明する前に、ハナスト APIのテストの役割についてご説明します。

ハナスト開発におけるテストの役割

ハナスト開発でのテストの役割は

  • API仕様と異なる実装を検出すること
  • 開発スピードを落とさないこと

としています。

この二つに優先順位はなく、品質担保と同様に開発スピードの維持も重要な役割としています。

これはハナスト開発のフェーズが仮説検証を繰り返している段階で、素早く機能を開発する必要があるからです。

必要のない機能は作らない

テストを書く、書かない以前に開発スピードを落とさないためには必要のない機能を作らないということが大事です。

基本的な方針として、ハナスト開発ではAPIの仕様を決める時に

  • 必要のない入力を受け付けない
  • 必要のない出力をしない

ようにしています。

API仕様に必要のない入出力があると、その仕様のテストも必要になりますし、後々変更をする際にもその仕様を守るために余計な工数がかかってきて開発スピードが落ちてしまいます。

また、必要のないテストをしないことも同様に重要です。

同じようなテストがいくつもあると、そのテスト作成の工数だけでなく、テスト変更時の工数も増えてしまいます。

ハナストAPIのリクエストテストを書く理由と、ユニットテストを書かない理由

現在のハナストのように開発が活発な状態だと外部APIおよび内部APIの仕様変更も頻繁に発生します。

リクエストテストは外部APIの実装が壊れないようにするためのもので、これは必要です。

リクエストテストがないと、外部APIが仕様通りに動いていない場合に気付くことができません。

一方ユニットテストについては内部APIの実装が壊れないようにするためのものになります。

ハナストの開発ではリファクタリングによって内部APIの仕様を変えるというケースは頻繁に発生します。

ユニットテストが書いてある場合は、内部APIの仕様を変えた時には関係するユニットテストも合わせて修正する必要があります。

リクエストテストは問題なく通っていて、API全体の動作としては問題ない場合でも、ユニットテストが壊れている場合は直さなくてはいけません。

これは「開発スピードを落とさない」という観点からは許容できません。

そのため、ハナストでは基本的にリクエストテストを書いて、原則としてユニットテストを書かないようにしています。

ユニットテストを書く場合

「原則」ユニットテストは書いていませんが、場合によっては書く場合があります。

一つはユニットテストレベルでTDD(テスト駆動開発)をした方が実装しやすい場合です。

よくあるのは値を変換する汎用ロジックのテストです。

例えばUTCの時刻からJSTの日付を取得するようなメソッドなどが該当します。

このようなメソッドはユニットテストを書いてから実装コードを書くTDDスタイルの方が実装しやすいので、ユニットテストを用意しています。

もう一つは他のテストでスタブしているメソッドのテストです。

リクエストテストでメソッドをスタブしている場合、スタブしているメソッドのユニットテストがないと、そのメソッドが全くテストされません。

ハナストの開発ではテストされないロジックは許容していないので、そのような場合はユニットテストを用意しています。

スタブを使う場合と使わない場合

スタブについても「開発スピードを落とさない」ことを考えて使うかどうかを決めています。

前述したように、スタブを用意した場合はユニットテストが必要になるのでスタブも極力使わないようにしています。

これはS3やSQSなどについても同様で、minioやelasticmqなどのクローンを使ってなるべくスタブはしていません。

ただし、

  • クローンが用意されていない外部リソースを利用する場合
  • 外部リソースのエラー時の挙動をテストする場合
  • 外部リソースの状態変化をテストする場合

といったケースでは外部リソースのスタブを許容しています。

この場合、スタブ対象が十分にテストされていることが望ましいので、なるべくライブラリのクライアントなどを直接スタブするようにしています。

Factoryを積極的に使う

テスト用のデータ(例えばUserデータなど)についてもやはりモックは使いません。

これは、安易にモックしてしまうと矛盾のあるデータが作られてしまいやすく、テストが不安定になるからです。

その代わりにハナストの開発ではテスト用のデータ作成用のFactoryクラスを用意しています。

例えばUserFactoryなら

new UserFactory().create()

という感じで呼び出すと、デフォルトのパラメタでUserデータを作成します。

テストのためにパラメタの指定が必要な場合は

new UserFactory().create({ familyName: 'ハナスト', givenName: '太郎' })

のようにパラメタを指定します。

Factory側では依存するレコードの作成なども含めて、矛盾のないデータを作成するようにしています。

CIは並列実行する

  • 基本的に(ユニットテストではなく)リクエストテストを書く
  • スタブはしない

という方針でテストを書いていると、テストの実行時間は長くなりがちです。

その結果CIの待ち時間が長くなってしまうと、やはり開発スピードが落ちてしまうので、CIに時間がかかるようになってきたら適宜並列化して、全体で5分程度で終わるようにしています。

並列化の方法はシンプルで、例えばテスト用のディレクトリ構成が

  • spec
    • entity
    • usecase
    • request

のような構成になっていたら、entity・usecase・requestディレクトリのテストをそれぞれ並列に実行します。

ハナストのCIはGithub Actionを使っているので、上記のような例ですと3並列のワークフローを定義してCIを実行すればOKです。

まとめ

今回はハナストの開発スピードを落とさないための基本的なテスト戦略についてご紹介しました。

他にもハナストでは音声認識技術や音声入力のインタフェースなどをチームメンバーが各自の役割を発揮して開発していますが、介護の世界でやるべきことはまだまだたくさんあります。

そのような社会的な課題の解決をより進めるために、ハナストチームおよびエクサウィザーズでは一緒に働く人を募集しています。

介護をより良くするハナストの開発、あるいはAIで社会課題を解決するエクサウィザーズに少しでも興味がありましたら、是非ご応募ください!

hrmos.co