LoginSignup
33
31

【ChatGPT】マルチモーダルAIのリファレンス実装 ~多様な情報源から一貫性のある結果を引き出す~

Last updated at Posted at 2024-02-28

本記事は日本オラクルが運営する下記Meetupで発表予定の内容になります。発表までに今後、内容は予告なく変更される可能性があることをあらかじめご了承ください。

セミナー実施の動画

はじめに

2012年、カナダのトロント大学の研究チームが画像認識の世界大会「ImageNet(イメージネット)」チャレンジで、驚異的な成果を上げました。このコンテストは膨大な画像データから特定の対象物を認識するタスクの精度を競う大会で、その年の優勝者である同チームが生み出した画像認識モデル「AlexNet」ではCNN(現在の深層学習で使われるニューラルネットワークの一種)と呼ばれるアルゴリズムが使われており、そのあまりの精度の高さからこの深層学習という手法が注目されることになりました。

筆者の知る限りこれが深層学習元年、つまり現在のAIブームの幕開けと言っていいと思います。その後、画像系AIは目覚ましい進化を遂げ、今日では様々な産業で利用されるものの、もう一つの重要な非構造化データである自然言語データのAIでは長らく技術革新がありませんでした。

そして、画像AIに遅れること10年、2022年にOpenAI社のChatGPTリリースでようやくブレイク(正確には2017年Transformer(Attention is All you need)の発明がきっかけだが、当時は難解なライブラリとしてのみ実装されていたため、その革新性を認知していたのは自然言語処理を専門とする一部の技術者だけだった)、その後、全世界を空前のAIブームに巻き込み、ようやく、画像とテキストというビジネスシーンで多用されるデータの高度なAIが揃った状況だと言えます。

しかし、大規模言語モデル含め、現在主流の機械学習モデルは、ある特定のデータから特定のデータを推論(生成含む)するというAI。 例としては、様々な数値情報から、商品Aの売り上げを予測するAIなど、数値をもとに数値を推論するモデルや、大規模言語モデルのように入力テキストから出力テキストを推論(生成)するというシングルモーダルのAIが現在の主流です。

image.png

もし一種類の情報だけからだけではなく、二種類、三種類という多様な情報を同時に分析し判断することができれば、その結果はよりよいものになるということを私達は経験で知っています。 そのような背景から、大規模言語モデルの登場以降数年たった現在、AIの先端技術は、単一のデータモダリティ(例えばテキストデータ)だけではなく多様なデータモダリティ(画像データ、動画データ、音声データ)から推論が行える「マルチモーダルAI」に移行しつつあります。

このマルチモーダルAIのコンセプトは、しばしば人間が五感から得られる情報をもとに様々な判断をし、それに基づいて行動していることに例えられます。人は会話をしているときに、相手が話している内容、声の大きさやトーン、顔の表情や身振り手振りなど言語、非言語のあらゆる情報から相手の感情を察して会話しています。

image.png

仮に、対面ではなくメールやチャットで会話をしている場合、得られる情報は「会話内容のみ」という、対面のときと比較して大幅に少ない情報から相手の状態を推測することになり、その結果、意図せず相手を怒らせてしまうメッセージを送ってしまった、などという経験は誰にでもあると思います。これはまさに「会話内容のみ」という単一のデータモダリティから推論するシングルモーダルAIの典型的なデメリットの例です。つまり、マルチモーダルAIとは、二つ以上の情報を組み合わせて同時に分析し、より包括的な結論を導き出すことができる柔軟性のあるAIのことであり、大規模言語モデルに次ぐイノベーションとして様々な応用領域での活用が期待されています。

要素技術

本記事ではマルチモーダルAIの実装で理解しておきたい下記6つの要素技術と、これらの要素技術のサンプル実装コードを概説します。

  • マルチモーダル大規模言語モデル(MLLM : Multi-modal LLM)
  • 検索拡張生成(RAG:Retrieval Augmented Generation)
  • セマンティック検索、類似検索
  • CLIP(Contrastive Language-Image Pretraining)
  • ベクトルデータベース
  • オーケストレーションツール

(1)マルチモーダル大規模言語モデル(MLLM : Multi-modal LLM)

現在主流の大規模言語モデル(LLM)は text in text out(テキストデータを入力し、テキストデータを出力)のシングルモーダルAIです。上述した通り、マルチモーダルAIではテキストデータ以外の様々なデータを入出力できる必要があり、このようなモデルをマルチモーダル大規模言語モデル(Multi-modal LLM)や大規模マルチモーダルモデル(Large Multi-modal Model) と呼んでいます。

image.png

MLLMは既に様々な企業から製品としてリリースされおり有名なものだと以下のようなものがあります。

  • GPT4-V(OpenAI)
  • PaLM-E(Google)
  • KOSMOS(Microsoft)
  • BLIP(Salesforce)

Stable Diffusion、Dall-E、Midjourneyなどテキストから画像へのモデルはマルチモーダルですが、これらのモデルは言語モデルのコンポーネントがないのでマルチモーダルモデルにはカテゴライズされません。マルチモーダルモデルの正確な定義はありませんが、基本的には下記要素を備えているモデルがマルチモーダルモデルとなります。

  • 入力と出力で異なるデータモダリティが可能 (例: テキストから画像へ、画像からテキストへ、テキストから音声へ)
  • 入力はマルチモーダル (例: テキストと画像の両方を処理できるシステム)
  • 出力はマルチモーダル (例: テキストと画像の両方を生成できるシステム)
  • 言語コンポーネントを備えているモデル

これらのことから、基本的にMLLMはLLMに追加のモダリティを組み込む(学習させる)ことで開発される、LLMをベースとした「LLMの上位互換モデル」 として位置づけられています。

このMLLMを使った簡単なサンプルコードを見てみましょう。下記wikipediaの画像データと、プロンプトのテキストデータをMLLMであるGPT4-Vに入力し、その結果をテキストで出力してみます。

image.png

# APIキーの定義
import os
import getpass
os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI API Key:")
from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4-vision-preview",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "この画像には何が描かれていますか?"},
                {
                    "type": "image_url",
                    "image_url": {
                        "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
                    },
                },
            ],
        }
    ],
    max_tokens=300,
)

print(response.choices[0])

プロンプトに対する応答は下記の通りで、この画像を正確に描写していることが確認できます。

image.png

更に、別のモデル(TTS : text to speech)を使って、上記の出力テキストデータをもとに音声データを生成することも可能です。

# tts-1を使ったテキストデータから音声データの生成

text = response.choices[0].message.content

response = client.audio.speech.create(
    model='tts-1',
    voice='alloy',
    input=text
)
response.stream_to_file('./speech.mp3')

※こちらの記事「GPT4 Turbo with Vision で様々な画像データの分析」では様々な画像データをGPT4-Vで分析してみた結果集となっていますので興味ある方は参考にされてください。

上述のサンプルコードはMLLMを使ったコードになりますが、これらMLLMを組み込んだAIは既に皆さまの身近にサービスとして存在します。OpenAI社やGoogle社は既に彼らのチャットボットサービスであるChatGPT、GeminiにMLLMを組み込んでおり、既に私たちが利用できる状態です。

image.png

今後もこれらのサービスはどんどんモダリティを追加しながらより便利なサービスに変貌し続けるでしょう。となると、このようなマルチモーダルAIのシステムはもう自社で開発する必要もなく、このような企業に任せておけばいいのでは?と思ってしまいがちですが、最先端の技術を持つGoogleやOpenAIのような企業にも絶対にできないことが一つあります。むしろ皆様のような企業にしか絶対にできないこと、それは「データを生み出すこと」です。 機械学習では纏まったデータがなければ何もできません。どんなに画期的なアルゴリズムを開発できる技術力があったとしても、そこにデータがなければビジネスで価値を生み出すことができないのです。皆さまの企業が日々生み出すプライベートなデータ(インターネットに公開されていないデータ)をAIに組み込むことは皆様の企業にしかできなことなのです。そして、このプライベートなデータをマルチモーダルAIに組み込むプラクティスの一つが「検索拡張生成(RAG:Retrieval Augmented Generation)」と呼ばれている技術です。

(2)検索拡張生成(RAG:Retrieval Augmented Generation)

RAGとは端的にいうと、LLMとその他の知識ベース(例えばベクトルデータベース)を組み合わせて、ユーザーのプロンプトに対する応答を生成する構成のことを指します。

image.png

ご存じのように、LLMやMLLMはインターネットに公開されているデータを学習データとしていますので、一般公開されていないデータ(例えば企業内のデータ)などは学習していないモデルです。したがって、企業内データのようなプライベートデータに関する応答ができないのです。その問題を解決するために、企業内データをベクトルデータベースに保持しておいて、LLMと連携させることにより、LLMにとって未知のデータを含めた応答をできるようにするシステムがRAGと呼ばれる構成です。さらにLLMをMLLMに置き換えることでこれまでのシングルモーダルRAGがマルチモーダルRAGの構成へとアップグレードされます。

シングルモーダルRAGについては以前にセミナー記事【ChatGPT】とベクトルデータベースによる企業内データの活用(いわゆるRAG構成)でその実装を紹介していますので興味のある方は参考にされてください。

この際、LLMが学習していない未知の情報をベクトルデータベースに問い合わせるしくみとしてセマンティック検索や類似検索 という検索技術があります。

(3)セマンティック検索、類似検索

セマンティック検索、類似検索含め、LLM周辺で使われるほぼ全ての技術は、その基礎となる埋め込み(Embedding) と呼ばれる処理を理解していることが重要となります。

埋め込み(Embedding)

LLMで使われるデータは「テキストデータ」、つまり言葉は、そのままでは統計関数などで計算することができません。 そのため、「言葉」という質的データは、「数値」という量的データに変換する必要があります。そのための処理を「埋め込み(Embedding)」と呼んでいます。

image.png

この埋め込みの処理には、埋め込み用の機械学習モデルを使います。昨今は有償のものからOSSまで数十を超える沢山のモデルが市場に出回っていますが、もっとも有名なモデルはOpenAIのモデル(text-embedding-ada002)でしょう。これらのモデルはインターネットから収集されたデータをもとに、単語や文章の共起頻度などから、入力されたテキストを数値(ベクトル)に変換することができます。テキストをベクトルに変換することでテキストという数値でないデータが計算できるようになります。

そして、この「テキスト」の計算とは、単に単語や文章を計算できるというということではなく、その「意味」を計算できるようになるというのが重要な視点です。単語の意味を計算する例として下記はあまりにも有名です。

image.png

これにより、LLMでは「AIが人間の言葉の意味を理解している」という状況を作り出しています。

埋め込みの処理については、以前のセミナー記事ChatGPTのコア技術「GPT」をざっくり理解するでご紹介していますので興味のある方は参照されてください。

上述した通り、埋め込み処理によってテキストデータはベクトルに変換され、その意味が計算できるようになります。とはいえ、埋め込みモデルが生成するこのベクトルをどのようにイメージすればいいのでしょう。それに役立つ概念が、埋め込みモデルが持つ「意味空間」 とよばれるものです。

意味空間

人間の脳の中にもたくさんの単語や文章が記憶されているのと同様に、埋め込みモデルもインターネットから集められた様々な単語や文章が体系だって記録されています。その記録の空間を「意味空間」 と呼んでいます。

image.png

埋め込みによってテキストデータが変換されたベクトルの値とは、この意味空間の中での単語や文章の「位置」 と言えます。そして、このベクトルの値が近ければ近いほど、その単語や文章は近い意味を持つことになります。 図は埋め込みモデルが持つ意味空間を二次元で表したものですが、単語や文章はそのコンテキストにより様々な意味をもちますので、実際には二次元で表すことはできず、大抵のモデルは753次元から2048次元という次元数を持ちます。

埋め込み処理では似た意味をもつ単語や文章は近しい値のベクトル値が割り当てられますが、この ベクトル値が近い、遠いという状況をどのように計算しているのか? その計算がまさに セマンティック検索、類似検索の処理で行われているベクトル演算 ということになります。

セマンティック検索、類似検索のベクトル演算

私達は高校の代数・幾何学の授業でベクトルとは「向き」と「大きさ」を持つ量的データ と習ったはずです。ということは、ベクトルの「向き」と「大きさ」を計算しさえすればベクトルの類似度を測れるということになり、ひいては意味の近い単語や文章を検索していることになるということがわかります。

image.png

この向きと大きさの計算に使われる公式には様々なものがありますが、よく使われるのはコサイン類似度と呼ばれるものです。(※その他、ドット積、ユークリッド距離、マンハッタン距離など様々な計算手法があります。いずれも非常にシンプルな計算です。)

ざっくりいうと、コサイン類似度とは

  • ベクトルの内角に関係する計算をしている
  • 内角が小さいほど似たような「向き」となり、近しいベクトルとなる
  • ベクトル(行列)の内積をベースに計算している

というもので、更にざっくりいうと、2つのベクトルの持つ角度でその近さを判断している と考えるとわかりやすいと思います。

後述するベクトルデータベースでは、このようなコサイン類似度の計算を行うことにより、ベクトルデータベースにため込まれたテキストデータや画像データの中から目的のデータを検索する処理を行っています。

RAG全体の構成の中では、LLMにとっての未知データをベクトルデータベースに検索する際に、セマンティック検索、類似検索によってこの計算が行われ、類似したテキストを取り出す処理をしているということになります。

テキストデータの埋め込みやセマンティック検索、類似検索の処理は上述した通りですが、マルチモーダルAIではテキストだけではなく画像も重要なデータとなります。テキストデータ同様、画像データも埋め込みを行いベクトルに変換し、画像データのセマンティック検索、類似検索 をできるようにする必要があり、その埋め込みモデルとして下記2つがよく利用されます。

  • CLIP(OpenAI)
  • Flamingo(DeepMind)

本記事ではCLIPをご紹介します。

(4)CLIP(Contrastive Language-Image Pretraining)

CLIPは画像データの埋め込みに利用するモデルです。画像データはテキストデータよりも技術が先行していたため、実に沢山のモデルが存在しますが、マルチモーダルAIで使う画像埋め込みモデルはどれを使ってもいいというわけにはいきません。

そもそも、テキストデータ用のモデルで埋め込み処理を行ったベクトルと、画像データ用のモデルで埋め込み処理を行ったベクトルは、それぞれ異なる意味空間にマッピングされています。この状態では、画像データとテキストデータのベクトル値が近いのか遠いのかをコサイン類似度で計算することができないからです。意味空間の異なるベクトル値(異なるモデルで埋め込まれたベクトル値)は全く異なる体系で作られたベクトル値のため比較する意味がない のです。それは例えるなら、「50円と50リットルはどちらが大きいのか?」という計算をするようなものです。

image.png

従って、この意味空間を共通にする(言わば、単位を揃える)ことで、この問題を解決する技術がCLIP(Contrastive Language-Image Pretraining)と呼んでいるモデルです。このCLIPというモデルを知るには、CLIPがどのような学習処理を経て作られたモデルかを理解するといいと思います。

image.png

このモデルの学習では、テキストデータ、画像データともに別々のモデルで埋め込みを行います。そして、このそれぞれのベクトル値に、射影行列(projection matrix) を掛けたベクトル値をそれぞれ作り出し、そのベクトルの値が近くなる(コサイン類似度の値が近くなる)ように、この射影行列の値を調整(学習処理)します。 学習データとして、沢山のテキストデータとそれに対応する画像データのデータセットを使ってこの調整処理(学習処理)を繰り返し行い、最終的には最適な値を持つ射影行列が作られたモデルがCLIPです。つまり、このCLIPというモデルの重みパラメータはこの射影行列ということになります。このCLIPにより、画像データはテキストデータと同じ意味空間にマッピングできるベクトル値へと変換されるため、単一の意味空間の中でテキストデータと画像データの類似検索(コサイン類似度の計算)ができるようになるという仕組みです。

上述したCLIPの学習手法は、機械学習では比較的古くから使われる「対照学習(Contrastive Learning)」と呼ばれる学習手法で、CLIP(Contrastive Language-Image Pretraining)の名称から、言葉(Language)と画像(Image)を対照学習(Contrastive Learning)にかけたモデルだとわかります。

そして、これら埋め込み処理によって得られたベクトルはそのままコードの中で利用することができますが、大量のドキュメントデータや画像データなどはそのベクトル値を永続化し、検索(セマンティック検索、類似検索)するためデータベースを使います。このベクトルを貯めこむデータベースが文字通り、ベクトルデータベースということになります。

(5)ベクトルデータベース

ベクトルデータベースは呼称に「データベース」が付いていますので、もちろん、データを貯め、検索して必要なデータを取り出すことを目的としています。ただ、一般的なデータベース(いわゆるRDBなど)とは異なり貯めこむデータがベクトルデータとなり、それに付随して様々な違いがあります。

image.png

今では、この表には記載できないほどたくさんのベクトルデータベースが存在し、それぞれのベクトルデータベースは様々な生い立ちを持っています。

  • ベクトルネイティブ系
    • Pinecone, Chroma, LanceDB, Qdrant, vespa, marqo, Milvus, Weaviate
  • リレーショナルデータベース系
    • Oracle Database(BETA), Pgvector, MS SQL Server, DB2
  • 全文検索エンジン系
    • OpenSearch、Elasticsearch

そしてRDBとの一番の違いは、クエリの仕組みです。

image.png

RDBのクエリは、クエリにある検索キーワードの完全一致か部分一致により、データベースの中にある大量のデータから目的のデータを見つけます。それに対して、ベクトルDBのクエリは、上述した、セマンティック検索や類似検索処理というベクトル演算の処理となります。これにより、通常のRDBではキーワード検索(意味を考慮せず、ただ単にキーワードが一致する部分だけを検索する)というセマンティクスを考慮した検索ができませんが、ベクトルデータベースでは、キーワード検索だけではなく、そのキーワードの意味を考慮した検索、つまりセマンティック検索が可能になります。また、昨今のベクトルデータベースでは、一つのクエリでキーワード検索とセマンティック検索の両方を実行でき、これを「ハイブリッド検索」と呼んでいます。

(6)オーケストレーションツール

ここまで上述してきたような技術を使って、マルチモーダルなRAG構成が可能になるのですが、このRAG構成をより容易に構成するためオーケストレーションツール(もしくはエージェントツール)と呼ばれるフレームワークを使うことが主流になっています。

著名なフレームワークとしては

  • LangChain
  • LlamaIndex
  • Semantic Kernel

などがあり、いずれもオープンソースのフレームワークで手軽に利用できます。

RAGの構成要素はこれまで説明してきた通り、LLM(もしくはMLLM)とベクトルデータベースということになります。LLMが知らない知識はベクトルデータベースにセマンティック検索を実行し、それをLLMに連携して最終的な応答を生成します。しかし、これらの処理はLLMやベクトルデータベースを構成するだけで自動的に連携してくれるわけではありません。ユーザー自身がこのロジックを考えてサービスや業務で使うアプリケーションに実装してゆく必要があります。

image.png

それでは少々大変でしょうということで、この定型的な処理を比較的簡単に実装できるようAPIにしたものが上述したオーケストレーションツール で、ライブラリ形式で配布されています。

image.png

このオーケストレーションツールをLLMや、ベクトルデータベースの手前に配置することで、各コンポーネントとのやり取りは全てオーケストレーションツールにお任せ状態となります。つまり、ユーザーはその部分のロジックを実装する必要がなくなり、コードとしてはかなり簡素化されたものになるというのがオーケストレーションツールを使う最大のメリットです。

image.png

因みに、RAGはLLMとベクトルデータベースを連携させる構成として語られることが多いですが、かならずしもその組み合わせだけではありません。LangChainやLlamaIndexは実に様々な製品やサービスとの連携が可能になっており、そのエコシステムは興味深いものになっています。

以上、マルチモーダルAIを構成するための6つの要素技術について解説しました。ここからはこの6つの要素技術の実装のお話です。

実装サンプル

まず実装する構成は下記の通りです。

  • オーケストレーション:LlamaIndex
  • MLLM : GPT4-V
  • ベクトルデータベース:Qdrant
  • 画像データ埋め込みモデル:CLIP
  • テキストデータ埋め込みモデル:Ada(LlamaIndexから内部で利用)

画像データ、テキストデータの埋め込みを行って、ベクトルデータベースにロードするフローは下図の通り。

image.png

マルチモーダルクエリを実行するフローは下図の通り。

image.png

サンプルコード概説

必要なライブラリをインストール

!pip install llama-index-multi-modal-llms-openai
!pip install llama-index-vector-stores-qdrant
!pip install llama_index ftfy regex tqdm
!pip install git+https://github.com/openai/CLIP.git
!pip install torch torchvision
!pip install matplotlib scikit-image
!pip install -U qdrant_client

OpenAIのAPIキーをセット

import os
import getpass
import warnings

warnings.filterwarnings('ignore')
os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI API Key:")

画像データ保存用にディレクトリを作成

# imput_images ディレクトリの作成
from pathlib import Path

input_image_path = Path("input_images")
if not input_image_path.exists():
    Path.mkdir(input_image_path)

GPT4V 画像推論用の画像を Tesla Web サイトからダウンロードし、imput_imagesディレクトリに保存。この画像データをマルチモーダルクエリに利用します。(ベクトルデータベースにロードする画像ではないので勘違いのないようにご注意を)

!wget "https://docs.google.com/uc?export=download&id=1nUhsBRiSWxcVQv8t8Cvvro8HJZ88LCzj" -O ./input_images/long_range_spec.png
!wget "https://docs.google.com/uc?export=download&id=19pLwx0nVqsop7lo0ubUSYTzQfMtKJJtJ" -O ./input_images/model_y.png
!wget "https://docs.google.com/uc?export=download&id=1utu3iD9XEgR5Sb7PrbtMf1qw8T1WdNmF" -O ./input_images/performance_spec.png
!wget "https://docs.google.com/uc?export=download&id=1dpUakWMqaXR4Jjn1kHuZfB0pAXvjn2-i" -O ./input_images/price.png
!wget "https://docs.google.com/uc?export=download&id=1qNeT201QAesnAP5va1ty0Ky5Q_jKkguV" -O ./input_images/real_wheel_spec.png

imput_images のディレクトリに保存された画像を確認します。

from PIL import Image
import matplotlib.pyplot as plt
import os

image_paths = []
for img_path in os.listdir("./input_images"):
    image_paths.append(str(os.path.join("./input_images", img_path)))


def plot_images(image_paths):
    images_shown = 0
    plt.figure(figsize=(16, 9))
    for img_path in image_paths:
        if os.path.isfile(img_path):
            image = Image.open(img_path)

            plt.subplot(2, 3, images_shown + 1)
            plt.imshow(image)
            plt.xticks([])
            plt.yticks([])

            images_shown += 1
            if images_shown >= 9:
                break


plot_images(image_paths)

下記のような車やスペックの画像データ(pngファイル)が保存されていることがわかります。(※繰り返しになりますが、上図の画像データはプロンプトを入力する際に同時に入力する画像データです。ベクトルデータベースにロードする画像データではないので勘違いのないようご注意を。)

image.png

そして、ベクトルデータベースにロードする画像データやテキストデータをwikipediaから収集します。

# 指定されたWikipediaの記事から画像のURLを取得する関数 get_wikipedia_images を定義 

import requests

def get_wikipedia_images(title):
    response = requests.get(
        "https://en.wikipedia.org/w/api.php",
        params={
            "action": "query",
            "format": "json",
            "titles": title,
            "prop": "imageinfo",
            "iiprop": "url|dimensions|mime",
            "generator": "images",
            "gimlimit": "50",
        },
    ).json()
    image_urls = []
    for page in response["query"]["pages"].values():
        if page["imageinfo"][0]["url"].endswith(".jpg") or page["imageinfo"][
            0
        ]["url"].endswith(".png"):
            image_urls.append(page["imageinfo"][0]["url"])
    return image_urls

Wikipediaから指定した複数の記事のテキストと画像を取得し、それぞれの記事のテキストと画像を指定されたフォルダに保存します。(様々な自動車メーカーの車種の画像やテキストデータを採取しています。)

from pathlib import Path
import requests
import urllib.request

image_uuid = 0
# image_metadata_dict stores images metadata including image uuid, filename and path
image_metadata_dict = {}
MAX_IMAGES_PER_WIKI = 20

wiki_titles = {
    "Tesla Model Y",
    "Tesla Model X",
    "Tesla Model 3",
    "Tesla Model S",
    "Kia EV6",
    "BMW i3",
    "Audi e-tron",
    "Ford Mustang",
    "Porsche Taycan",
    "Rivian",
    "Polestar",
}


data_path = Path("mixed_wiki")
if not data_path.exists():
    Path.mkdir(data_path)

for title in wiki_titles:
    response = requests.get(
        "https://en.wikipedia.org/w/api.php",
        params={
            "action": "query",
            "format": "json",
            "titles": title,
            "prop": "extracts",
            "explaintext": True,
        },
    ).json()
    page = next(iter(response["query"]["pages"].values()))
    wiki_text = page["extract"]

    with open(data_path / f"{title}.txt", "w") as fp:
        fp.write(wiki_text)

    images_per_wiki = 0
    try:
        # page_py = wikipedia.page(title)
        list_img_urls = get_wikipedia_images(title)
        # print(list_img_urls)

        for url in list_img_urls:
            if (
                url.endswith(".jpg")
                or url.endswith(".png")
                or url.endswith(".svg")
            ):
                image_uuid += 1
                # image_file_name = title + "_" + url.split("/")[-1]

                urllib.request.urlretrieve(
                    url, data_path / f"{image_uuid}.jpg"
                )
                images_per_wiki += 1
                # Limit the number of images downloaded per wiki page to 15
                if images_per_wiki > MAX_IMAGES_PER_WIKI:
                    break
    except:
        print(str(Exception("No images found for Wikipedia page: ")) + title)
        continue

htmlファイルも収集。

!wget "https://www.dropbox.com/scl/fi/mlaymdy1ni1ovyeykhhuk/tesla_2021_10k.htm?rlkey=qf9k4zn0ejrbm716j0gg7r802&dl=1" -O ./mixed_wiki/tesla_2021_10k.htm

下記のように、いくつかのJEPGファイル、txtファイル、htmファイルが保存されていることがわかります。いずれも上記のプログラムで指定した様々な自動車メーカーの画像データとテキストーデータです。これらがベクトルデータベースにロードするデータとなります。

(base) bash-4.2$ ls ./mixed_wiki/
100.jpg  117.jpg  21.jpg  38.jpg  54.jpg  70.jpg  87.jpg            Kia EV6.txt
101.jpg  118.jpg  22.jpg  39.jpg  55.jpg  71.jpg  88.jpg            Pablo Picasso.txt
102.jpg  119.jpg  23.jpg  3.jpg   56.jpg  72.jpg  89.jpg            Polestar.txt
103.jpg  11.jpg   24.jpg  40.jpg  57.jpg  73.jpg  8.jpg             Porsche Taycan.txt
104.jpg  120.jpg  25.jpg  41.jpg  58.jpg  74.jpg  90.jpg            Rivian.txt
105.jpg  121.jpg  26.jpg  42.jpg  59.jpg  75.jpg  91.jpg            tesla_2021_10k.htm
106.jpg  122.jpg  27.jpg  43.jpg  5.jpg   76.jpg  92.jpg            Tesla Model 3.txt
107.jpg  123.jpg  28.jpg  44.jpg  60.jpg  77.jpg  93.jpg            Tesla Model S.txt
108.jpg  12.jpg   29.jpg  45.jpg  61.jpg  78.jpg  94.jpg            Tesla Model X.txt
109.jpg  13.jpg   2.jpg   46.jpg  62.jpg  79.jpg  95.jpg            Tesla Model Y.txt
10.jpg   14.jpg   30.jpg  47.jpg  63.jpg  7.jpg   96.jpg            The Lord of the Rings.txt
110.jpg  15.jpg   31.jpg  48.jpg  64.jpg  80.jpg  97.jpg            The Matrix.txt
111.jpg  16.jpg   32.jpg  49.jpg  65.jpg  81.jpg  98.jpg            The Simpsons.txt
112.jpg  17.jpg   33.jpg  4.jpg   66.jpg  82.jpg  99.jpg
113.jpg  18.jpg   34.jpg  50.jpg  67.jpg  83.jpg  9.jpg
114.jpg  19.jpg   35.jpg  51.jpg  68.jpg  84.jpg  Audi e-tron.txt
115.jpg  1.jpg    36.jpg  52.jpg  69.jpg  85.jpg  BMW i3.txt
116.jpg  20.jpg   37.jpg  53.jpg  6.jpg   86.jpg  Ford Mustang.txt

ベクトルストア(Qdrant)を作成し、マルチモーダルインデックスを構築します。ベクトル化してQdrantにロードするデータは先ほどのwikipediaから収集したテキストデータと画像データです。

from llama_index.core.indices import MultiModalVectorStoreIndex
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.core import SimpleDirectoryReader, StorageContext

import qdrant_client
from llama_index.core import SimpleDirectoryReader

# Qdrantのベクトルデータベースを作成
client = qdrant_client.QdrantClient(path="qdrant_mm_db")

text_store = QdrantVectorStore(
    client=client, collection_name="text_collection"
)

image_store = QdrantVectorStore(
    client=client, collection_name="image_collection"
)

storage_context = StorageContext.from_defaults(
    vector_store=text_store, image_store=image_store
)

# mixed_wikiのディレクトリにある画像データとテキストデータをベクトル変換し、Qdrantにロード
documents = SimpleDirectoryReader("./mixed_wiki/").load_data()

index = MultiModalVectorStoreIndex.from_documents(
    documents,
    storage_context=storage_context,
)

これでマルチモーダルクエリ実行のための準備完了です。ここまで見ていただいてわかると思いますが、ここまでのコードはほとんどがデータを収集するためのコードでこのRAGの構成には直接関係はありません。ベクトルデータベースにデータをロードする部分の数行がこのRAG構成のコードであり、かつLlamaIndexの最も重要なコードだと言っていいと思います。

  • SimpleDirectoryReaderクラスのload_data()
    • jpg, txt, htmという様々なタイプのファイルを一つのディレクトリに無造作に放り込んでおいてもこの関数で整理してベクトルデータベースにロードしてくれるインテリジェントなドキュメントローダーです。このクラスには他にも便利そうな関数がありそうですね。
  • MultiModalVectorStoreIndexクラスのfrom_documents()
    • 事実上、この関数一つで読み込んだデータから埋め込み表現を得て、それをベクトルデータベースにロードしています。

いずれも抽象度の高い関数でLlamaIndexの有用性を感じますね。

これでベクトルデータベースにインデックが構築できているので、ここからマルチモーダルクエリを実行する準備です。まずはMLLMのGPT4-Vを定義します。

勘違いのないように収集したデータをどう使っているかを最後にもう一度下記に記載しておきます。(しつこいですが、ここを勘違いしていると何をやっているのかわからないサンプルコードになりますから念のためです。)

  • wikipediaから収集したデータ
    • 様々な自動車メーカーの車のデータ(jpg, txt, htmファイル)です。これらはベクトルデータベースにロードしました。
    • ベクトルデータベースに入っていますのでクエリされる側のデータです。
  • Teslaのサイトからダウンロードしたデータ
    • Teslaの画像ファイルです。Model Yという車種のスペックやシャシーの形状がわかるような画像データ
    • プロンプトと同時に入力し、クエリする側のデータです。
from llama_index.multi_modal_llms.openai import OpenAIMultiModal
from llama_index.core import SimpleDirectoryReader

# put your local directore here
image_documents = SimpleDirectoryReader("./input_images").load_data()

openai_mm_llm = OpenAIMultiModal(
     model="gpt-4-vision-preview", max_new_tokens=1500
)

そしてプロンプトを入力します。このコード実行時の動作としては、ざっくりと説明すると

  1. ローカルの画像データ(車の画像や諸元の画像データ)とプロンプトのテキストをMLLMに入力
  2. MLLMが車種や諸元を理解し
  3. ベクトルデータベースへセマンティック検索
  4. 検索結果をMLLMに連携
  5. MLLMが最終的なテキストを生成

となり、この流れをLlamaIndexが下記数行のコードで内部的に代行しているというという感じです。

response_1_ja = openai_mm_llm.complete(
    prompt="この画像の車とスペックが似ている車を教えてください。",
    image_documents=image_documents,
)

print(response_1_ja)

出力がこちら。

image.png

他のクエリも実行してみます。

response_2_ja = openai_mm_llm.complete(
    prompt="この画像の車と同じ価格帯の車を教えてください。",
    image_documents=image_documents,
)

print(response_2_ja)

出力がこちら。

image.png

いずれも、画像データ、テキストデータという複数のモダリティを入力し、MLLMとベクトルデータベース内のデータの連携によって最終的なテキストが生成されています。

おわりに

新しいコンセプトやアイデアととらえられがちなこのマルチモーダルですが、AI全体を俯瞰して考えてみるとみると、ごく必然かつ合理的な考え方であることがわかります。

機械学習システムを構築するうえで、学習のための特徴量が足りているのかどうか?という点は最も重要かつ常に意識している必要があります。

ある系において、自分が考えてもいなかった特徴量が実は目的の予測に大きく影響していたということはよくある話です。その系のあらゆる情報を取得することができれば精度の高いモデルを作ることができ、さらに取得できる情報が多岐にわたるほど、これまで不可能だった予測が可能になってくるという考え方そのものは機械学習の常識ですよね。

今はテキストデータに画像データが追加されただけですが、それでも、このインパクトは大きく計り知れない将来性を感じます。マルチモーダルAIとはまさに人間の利便性を追求する姿そのものなのかもしれません。

本記事のサンプルコードでは様々な自動車メーカーの画像データとテキストデータが使われています。これらはインターネットに公開されているデータですが、このデータを皆さまの会社の社内にしかないデータに置き換えて考えてみてください。皆さまの日々の業務をさ支える社内システムや、会社が運営しているサービスにとって有用な何かが思いつくのではないでしょうか。

本記事のサンプル実装概説により、マルチモーダルAIの有用性が理解され皆さまの日々の業務やサービスを支えるシステムの実装のヒントになれば幸いです。

大規模言語モデル関連のその他の記事

33
31
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
31