HEROZ Tech Blog

日本将棋連盟公認「将棋ウォーズ」や、AIを活用したシステム企画・開発を行う、AI企業HEROZの公式テックブログです。

社内向けHEROZ ASKにSLMのPhi-3.5を入れてみた

はじめに

昨今、小規模言語モデル(SLM, Small Language Model)の話が生成AI界隈で賑わせています。
SLMはgpt-4oのような大規模言語モデル(LLM, Large Language Model)と比較して小型軽量である故に以下のような特徴があるとされています。

  1. エッジデバイスやオンプレサーバーで動作させることができる。動作させてもコストが大きくならない。
    • セキュリティーやプライバシーの問題で海外のサーバーへプロンプトを送ることに対しての抵抗は根強くあると思います。
  2. 応答までのレイテンシーが短い
    • 特に音声会話のような場合にはミリ秒(ms)を争うので、レイテンシーが低いに越したことはないです。
  3. ドメイン特化のためのファインチューニングを実施しやすい。
    • 従来の中規模や大規模のモデルと比べてはるかに少ないGPU枚数でファインチューニングができるので、敷居が下がることを期待できます。

弊社でもSLMの動向は追っていましたが、以前の実験のように小規模のモデル(この時は10B前後)だと大した精度が出ないと考えていましたので、スルーしていました。
ところが、最近のSLMは以前のgpt-3.5-turboに匹敵する精度が出るという話や、精度が多少劣っても使い所があるかもしれないという話を聞き、遅ればせながら社内向けの環境に導入して、いろいろ検証しようと思いました。

検証に使用したSLMは今年の8月にリリースされたMicrosoft社のPhi-3.5の中で最も軽量なPhi-3.5 mini instructとしました。Phi-3.5 mini instructは3.8Bとかなり軽量です。
これをAzure Machine Learning サーバーレスAPIで動作させました。Phi-3.5 mini instructは1000トークンあたり$0.00013で推論できます。

HEROZ ASKに組み込むにあたってはlangchainで動作するようにしなければならないのですが、Azure Machine Learning サーバーレスAPIへの接続に関する情報がほとんどありませんでしたので、こちらに書こうと思います。

モデルのデプロイ

langchainからの呼び出しにはAzureMLChatOnlineEndpointを使用するのですが、こちらはAzure Machine Learning Studioでデプロイしたモデルにしか対応していませんでした。
先日のIgnite 2024でデビューしたAzure AI Foundryでデプロイしたモデルにはつながらず、少しハマりました。

以下がMachine Learning Studioでのモデルのデプロイ方法です。

1. ワークスペースの作成

Azure Machine Learning Studioにログインし、ワークスペースを作成する。

ワークスペースの作成

2. サーバーレスエンドポイントの作成

作成したワークスペースに入り、エンドポイントページのサーバーレスエンドポイントタブからサーバーレスエンドポイントを作成する。

サーバーレスエンドポイントの作成

3. エンドポイントの詳細

作成したエンドポイントの詳細ページを開くと、エンドポイントのURLとAPIキーを取得できます。この時に「APIルート」のパネルがあることがポイントです。

エンドポイントの詳細

Azure AI Foundryで作成したエンドポイントとの比較

Azure AI Foundryで作成したエンドポイントの詳細には「APIルート」が存在しません。

AI Foundryで作成したエンドポイント

Azure AI Foundryのエンドポイント一覧では、Machine Learning Studioで作成したOKの方のエンドポイントは「サーバーレス」となっていて、AI Foundryで作成したNGの方のエンドポイントは「Azure AI Services」となっています。

エンドポイントの一覧

langchainからの呼び出しコード

langchainからは以下のコードにて呼び出すことができます。

!pip install langchain langchain_community

from langchain_core.prompts import ChatPromptTemplate
from langchain_community.chat_models.azureml_endpoint import AzureMLChatOnlineEndpoint, AzureMLEndpointApiType, CustomOpenAIChatContentFormatter

AZURE_PHI3_ENDPOINT="(エンドポイント)"
AZURE_PHI3_API_KEY="(APIキー)"

human = "あなたは何者ですか?"
prompt = ChatPromptTemplate.from_messages([("human", human)])

chat = AzureMLChatOnlineEndpoint(
    endpoint_url=AZURE_PHI3_ENDPOINT,
    endpoint_api_type=AzureMLEndpointApiType.serverless,
    endpoint_api_key=AZURE_PHI3_API_KEY,
    content_formatter=CustomOpenAIChatContentFormatter(),
)

chain = prompt | chat
chain.invoke({})
→ AIMessage(content='私はMicrosoftのAIアシスタントです。...

Azure AI Foundryで作成したエンドポイントもlangchain_azure_aiに含まれるAzureAIChatCompletionsModelを使えば呼び出せるような記事はありました。ただし、現時点(2024/12/12)ではlangchain_azure_aiはままだ非公開のようです。

learn.microsoft.com

実行結果

HEROZ ASKの社内環境に組み込み、無事に動作しました。

Phi-3.5の動作

以下、実行して気になったことです。

モデルの精度と推論速度

モデルの精度については、プロンプトの些細な違いによっては壊れた回答になったり、Few-shotの例に引きづられすぎたりといった小中規模のオープンソースのモデルにありがちな間違いが発生することもありますが、プロンプトが合っていれば正しい回答を得られることは確認できました。

また、Azure Machine Learning サーバーレスAPIによる推論速度はモデルサイズの割には速くはなかったです。
やはりサーバーレスタイプですと、起動のオーバーヘッドがそれなりにあるのかもしれません。

ストリーミング

AzureMLChatOnlineEndpoint_(a)generate()関数から呼ばれる時にはストリーミングにならないことが分かりました。
そこで、以下のようなラッピング関数を作成して、_(a)stream()関数に分岐するようにしました。

コードを見る

from typing import Any, List, Optional

from langchain_core.callbacks import (
    AsyncCallbackManagerForLLMRun,
    CallbackManagerForLLMRun,
)
from langchain_core.language_models.chat_models import (
    agenerate_from_stream,
    generate_from_stream,
)
from langchain_core.messages import BaseMessage
from langchain_core.outputs import ChatResult

class AzureMLChatOnlineEndpointStreaming(AzureMLChatOnlineEndpoint):
    streaming: bool = False

    def _generate(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        run_manager: Optional[CallbackManagerForLLMRun] = None,
        stream: Optional[bool] = None,
        **kwargs: Any,
    ) -> ChatResult:
        should_stream = stream if stream is not None else self.streaming
        if should_stream:
            stream_iter = self._stream(
                messages, stop=stop, run_manager=run_manager, **kwargs
            )
            return generate_from_stream(stream_iter)
        return super()._generate(messages, stop, run_manager, *kwargs)

    async def _agenerate(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
        **kwargs: Any,
    ) -> ChatResult:
        should_stream = self.streaming
        if should_stream:
            stream_iter = self._astream(
                messages, stop=stop, run_manager=run_manager, **kwargs
            )
            return await agenerate_from_stream(stream_iter)
        return await super()._agenerate(messages, stop, run_manager, *kwargs)

使用トークン数の取得

AzureMLChatOnlineEndpointならびにAzure Machine Learning サーバーレスAPI上のPhi-3.5モデルは"stream_usage"に対応していないようですので、使用トークン数が取得できませんでした。
今回は社内向けなので使用トークン数は気にしなくてもよいですが、本番サービス向けには使用トークン数を取得する方法を検討する必要がありそうです。
(デバッグしている限りではストリーミングの途中では使用トークン数を配信しているようですが、AzureMLChatOnlineEndpointが拾っていないようです。また、回答完了時に使用トークン数を返すこともなかったです)

おわりに

今回は話題のSLMについて、Phi-3.5をHEROZ ASKの社内環境で動作させることに成功しました。
まだ多くは触っていないですが、社内で精度限界の検証ならびにユースケースの発掘を引き続き進めていきたいと思います。 何か分かりましたら、別途記事にしようと思います。

NoSQL(mongoDB) 導入ガイド

はじめに

NoSQL の利用を促進したいという意識のもとこの記事を作成しました。 今迄、数多くの案件に関わってきましたが、DB といえば大体 RDB でした。NoSQL を扱っている案件もあるのですが数少ないです。 NoSQL にしておけばこんなに苦労することはなかったんだろうなというケースも多々あったので、とりあえず使い慣れた RDB を使うといった風潮に異を唱えたく思いました。 ロールの多様化によって DB に触れる機会があまりないエンジニアも増えてきた昨今、利用促進には情報発信したり説得材料を作るのが肝要だと思います。ビギナーでも手軽に試せるクイックスタート、どういった局面で利用すべきかといったガイドライン、現実に発生し得る要求に対処できるようなケーススタディなどを書いたブログ記事を作成することで、その一助とできればと願います。

環境構築

GitHub リポジトリに本記事で扱う環境やソースコードを全て格納しています。

Docker(docker-compose)を利用できる環境であれば、コマンド一つで環境を構築できます。

docker-compose up -d

コマンドを実行すると以下の 3 つのコンテナが起動します。

  • backend
    • リポジトリに配置しているファイルが/usr/src/app ディレクトリにマウントされます
    • Python(Jupyter)を実行できる環境です。
    • VSCode などでリモートログインして利用してください。
  • mongo
    • mongo DB 本体です。
    • ユーザーやパスワードなどは docker-compose.yml に書いてあります。
  • mongo-express
    • mongo DB をブラウザの GUI でメンテナンスできるツールです。
    • ブラウザでこのリンクを開いてください。

導入

pymongoでmongo DBを操作します。接続と動作確認を行うJupyterスクリプトを以下にGistで貼り付けます。

hello mongo

ケーススタディ: ユーザー情報を管理する

階層構造データの基本的なCRUD操作をしてみます。

mongo case study manage user info

おわりに

今回は導入部分の説明のみとしました。今後、より実践的な内容にも踏み込んでいきたいと思っています。

GraphRAGを試してみた

はじめに

GraphRAGはLLMによってナレッジ(知識)グラフを生成することで、複雑な情報のドキュメントから質疑応答を行う際の精度を向上させることができる手法ならびにソフトウェアです。
GraphRAGはマイクロソフトが開発しましたが、先日オープンソース化が発表されましたので、早速試してみました。
GraphRAGはAzureで動作させる方法(GraphRAG Accelerator)もありますが、今回はローカル(手元のMac)で動作させました。

GraphRAGが使用しているナレッジグラフは、ドキュメント内の複雑な情報の検索に向いていると言われています。
HEROZ ASKのドキュメント検索でも使用しているエンべディング(embedding)を用いた検索は、文意に基づく曖昧検索には向いているのですが、複雑な情報の検索ではカバーしきれないがあって気になりました。

インストール

インストールは公式のGet Startedや解説してくださっている動画(英語)の通りに実施すると、わりと簡単にできました。
python:3のdockerイメージ上で実施して気になったのは以下です。

  • Rustのインストールは個別で実施する必要がある
  • ragtest/.env: GRAPHRAG_API_KEYにOpenAIのAPIキーを書く(名前が紛らわしい)
  • ragtest/settings.yaml: GRAPHRAG_LLM_MODEL=gpt-4-turbo-previewをgpt-4oに変更する

実行結果

とりあえずは桃太郎の文章を読み込ませて、Get Startedの質問(What are the top themes in this story?)を和訳した「この物語の主なテーマは何ですか?」を問い合わせてみました。

# python -m graphrag.query --root ./ragtest --method global "この物語の主なテーマは何ですか?"

  :

SUCCESS: Global Search Response: ### 物語の主なテーマ

物語「桃太郎」にはいくつかの重要なテーマが含まれています。それぞれのテーマは物語の異なる側面を強調し、全体として豊かな物語を形成しています。

#### 勇気とリーダーシップ

桃太郎の鬼ヶ島征伐の使命は、勇気とリーダーシップのテーマを中心に展開されています。桃太郎の卓越した強さ、戦略的な手腕、そして仲間を鼓舞し導く能力は、物語の中で重要な役割を果たしています [Data: Reports (2, 4, 5)]。

#### 団結とチームワーク

桃太郎と彼の忠実な家来である犬、猿、キジの協力は、団結とチームワークのテーマを強調しています。彼らの結束したスキルと献身は、鬼たちがもたらす困難を乗り越えるために不可欠です [Data: Reports (2, 4, 5)]。

#### 家族の絆と育成

桃太郎と彼を育てた老夫婦との関係は、家族の絆と育成のテーマを象徴しています。老夫婦の支援と愛情は、桃太郎が旅に出るための準備において重要な役割を果たしています [Data: Reports (1, 2)]。

#### 神の介入と運命

老夫婦が桃太郎を神の贈り物と信じることから、物語には神の介入と運命のテーマが含まれています。これにより、物語に霊的な次元が加わり、運命と神の祝福のテーマが強調されます [Data: Reports (1)]。

#### 文化的意義

「桃太郎」の物語は日本の民間伝承に深く根ざしており、強さ、リーダーシップ、団結の力といったテーマを祝うものです。また、きびだんごなどの文化的要素も含まれています [Data: Reports (2)]。

### 結論

「桃太郎」の物語は、勇気、リーダーシップ、団結、家族の絆、神の介入、そして文化的意義といった多くのテーマを通じて、豊かで多層的な物語を提供しています。これらのテーマは、物語のキャラクターや出来事を通じて巧みに織り交ぜられ、読者に深い印象を与えます。

やや冗長ではありますが、かなり的を得た回答になっています。
そして、日本語でも大丈夫そうでした。

気づいたこと

思考言語

インデックス作成時の画面出力を見る限りでは、nodeのdescriptionが英語になっていたりするので、英語で思考している可能性があります。
付属のプロンプトは指示も例示も全て英語なので、プロンプトを改良すると日本語にできるかもしれません。

🚀 create_final_nodes
    level      title      type                                        description  ... graph_embedding                 top_level_node_id  x y
0       0    "おじいさん"  "PERSON"  おじいさん (Grandfather) is an elderly character wh...  ...            None  b45241d70f0e43fca764df95b2b81f77  0  0
1       0    "おばあさん"  "PERSON"  おばあさん is an elderly woman who plays a signific...  ...            None  4119fd06010c494caa07f439b333f4c5  0  0
2       0        "川"     "GEO"  "川 (river) is a geographical feature where おばあ...  ...            None  d3835bf3dda84ead99deadbeac5d0d7d  0  0
3       0        "桃"   "EVENT"  "\u6843" (peach) is a significant element in t...  ...            None  077d2820ae1845bcbb1803379a3d1eae  0  0

既存システムへの組み込み

GraphRAGは以下のような点で、既存のシステムに組み込むのは現段階では難しそうです。

  • Azureでの実行を前提としている部分が多い
  • データの保存形式がメモリ、ファイル(.parquet)、Azure blobの3択であり、既存のデータベースに載せる場合には自分で改造する必要がある
  • 検索もlangchainが対応していないので、自力でchainを定義する必要がある

おわりに

今回は出たばかりのGraphRAGを試してみましたが、思ったりよりも苦労せずに動作して良かったです。
まだ単純な例文でしか試していないですが、もう少し込み入った文章も入力してみて、従来のエンべディングの場合と比較してみようと思います。

また、GraphRAGはプロンプトの指示文や、例示文を書き換えることで、ドメイン適用も可能そうですので、そちらも試してみたいと思います。

HEROZ ASK を支えるインフラ技術(第2回)

はじめに

こんにちは、HEROZ ASK の開発チームです。

herozask.ai

今回のポストでは、このプロダクトの開発で活用しているインフラ技術を紹介したいと思います。

前回の記事

heroz-tech.hatenablog.jp

『BRIDGE』掲載記事

HEROZ ASK について取り上げていただきました。

Microsoft Azure』の情報収集

皆さんは、どのようにAzureを勉強していますか?今回は情報のキャッチアップ方法について紹介したいと思います。

Microsoft Build』

先日、米国 Microsoft 主催の年次開発者会議「Microsoft Build」(2024年5⽉21⽇‐23⽇ 米国時間)が開催されました。

build.microsoft.com

Microsoft Build Book of News」に、発表された主要なニュース項目がまとまっています。

news.microsoft.com

ただ、発表内容が多いため、キャッチアップが大変ですね

Microsoft Build Japan』

日本の開発者向けに「Microsoft Build Japan」(2024年6月27日‐28日)が開催されます。 1ヵ月程度遅れますが、まとまった日本語情報を提供していただけるのは助かります。

Microsoft Ignite

冬には、「Microsoft Ignite」(2024年11⽉18⽇‐22⽇ 米国時間)が開催されます。

ignite.microsoft.com

2023年は、「MICROSOFT IGNITE BOOK OF NEWS」に、発表された主要なニュース項目がまとまっています。

news.microsoft.com

Microsoft Ignite Japan』

2023年と同様に、日本の開発者向けに「Microsoft Ignite Japan」も開催されることでしょう。

Microsoft (有志)』の記事

Zennでは、Microsoft Azureをはじめとする最新技術情報が提供されています。 米国 Microsoft 主催の大規模なイベント直後は、Microsoft社員の方々により、一斉更新が始まります。

zenn.dev

Azure OpenAI Service の GPT-4o 対応をするのに、以下のブログ記事が参考になりました。

zenn.dev zenn.dev zenn.dev

Azure AI Searchについては、花ヶ﨑さんのQiitaが参考になります。

qiita.com qiita.com qiita.com

Azure OpenAI Serviceについては、蒲生さんのSpeaker Deckが参考になります。

『Azureの新機能』

AzureやAzure OpenAI Service の新機能をまとめたページがあります。 ただし、「Microsoft Build」などの大規模なイベントで発表された新機能の反映は多少遅れる印象があります。 また、日本語への翻訳が遅れる場合もあるため、英語ドキュメントのチェックも必要です。

azure.microsoft.com learn.microsoft.com

Microsoft Learn』

製品、キャリア パス別のトレーニングなど、公式ドキュメントがとても充実しています。

learn.microsoft.com

ただし、

  • あまりに膨大なため、何から手をつけていいかわからない。
  • 英語の自動翻訳のため、あまり頭に入ってこない。
  • どこまで勉強すれば、商用サービスを構築・運用できるのかわからない。

など、迷子になってしまうことはあるかもしれません。

ドキュメントと睨めっこしながら、実際に手を動かして、Azureのサービスを触ってみるということが大事だと思います。

『Azure コマンド ライン インターフェイス (CLI)』

Azure Portal から手を動かしたことは、なるべく効率化したいですよね。 Azure CLIチュートリアルやサンプルで自動化できないか試すことをお勧めします。

learn.microsoft.com

注意点としては、

  • Azure Portal から手作業でしか設定できない項目の方が多い
  • 新機能のアップデートで仕様が変わり、エラーになる場合がある
  • 削除したリソース次第で、論理削除(完全に削除されていない)となり、再作成するとエラーになる場合がある

などがあります。

コマンドラインで出来ること、手作業でしか設定できないことを把握した上で、どれくらい労力をかけるのが構築/運用フェーズでコスパがいいか、見極めが重要です。 Azure のリソースをコードで管理できるか試すのは、それからでも遅くないのではないかと思います。

learn.microsoft.com learn.microsoft.com

『日本マイクロソフト サポート情報』

他のクラウドでは一般的ですが、「Microsoft Learn」の記載にないことをやりたい場合、情報が不足していて困ることがあります。 そんな時に役立つのが、「Microsoft Azure テクニカルサポートチーム」のブログ記事です。

cssjpn.github.io

「Azure OpenAI Service」であれば、「Japan Azure Cognitive Services サポートチーム」のブログが参考になります。 自分が困っていることは、皆さん困っている印象があります。

jpaiblog.github.io

どうしてもわからなければ、あまり悩まずにAzure サポートにサポートケースを起票するのも手かもしれません。 ただし、漠然とした理解で質問しても、有効な回答は得られないかもしれません。質問の前に下調べして、十分に理解する努力は必要です。

azure.microsoft.com

最後に

今後も HEROZ は、クラウド技術と生成 AI を活用したサービス提供について、今後も技術的にさらに踏み込んだ内容を発信して行きたいと思います。本ブログをご愛顧いただければ幸いです。最後までお付き合いいただきありがとうございました。

HEROZ ASK プロトタイプでマルチモーダルRAGを動かしてみた

はじめに

HEROZ ASKのプロトタイプに今年(2024年)に来ると言われているマルチモーダル(Multi modal) RAGを組み込みましたので、その結果について書いていきたいと思います。 HEROZ ASKのプロトタイプは実験やデモ用に新機能や新モデルを使えるようにした試作品です。
なお、本記事はRAGやlangchainのことをある程度理解している人を対象としています。

マルチモーダルRAGの実現方式

マルチモーダルRAGの実現方式はlangchainのblogで解説されています。
この記事によると実現方式には以下の3種類があるとされています。

  • Option 1: Retrieve raw image
  • Option 2: Retrieve image summary
  • Option 3: Retrieve image summary but pass raw image to LLM fir synthesis

https://blog.langchain.dev/content/images/size/w1000/2023/10/image-22.png (図はlangchainのblogに掲載されているもの)

Option 1はOpenCLIPのようなテキストとイメージをシームレスに取り扱えるマルチモーダルエンベンディング(Multi modal embedding)を使用して、エンべディングと検索を行います。
Option 2とOption 3はイメージを一度GPT-4oGPT-4Vのようなマルチモーダル対応LLMでサマリーのテキストを取得して、それに対してエンべディングと検索を行います。 Option 2は検索後の回答生成でも検索結果としてサマリーのテキストをそのまま使用しますが、Option 3は検索結果として元のイメージをマルチモーダル対応LLMに送信します。

今回の組み込みではOption 1とOption 3に対応することにしました。

改造ポイント

ドキュメントの保存

ドキュメントの分解にはunstructuredを使用しました。

unstructuredのpartition_pdf()chunking_strategyの設定によってチャンキングも行えます。その場合には、CompositeElementというelementで内容を取得します。
また、extract_images_in_pdfをTrueにすると、image_output_dir_pathに切り出したイメージが出力されます。extract_element_typesにTableも入れますと、表も切り出されます。

切り出したイメージはbase64にして保存します。アイコンのような小さなイメージも切り出されますので、20kb以下のイメージは無視するようにしています。
Option3ならGPT-4oやGPT-4Vでサマリーを取得します。
Option1の場合には、embeddingに含まれるembed_image()を使用してベクトル化します。今回はOption1用のembeddingとしてOpenClipのマルチリンガルモデルであるCLIP-ViT-H-14-frozen-xlm-roberta-large-laion5B-s13B-b90kを使用しました。

コードを見る

!apt-get install libgl1-mesa-dev poppler-utils tesseract-ocr tesseract-ocr-jpn
!pip install cmake
!pip install unstructured[pdf]==0.11.8 langchain openai langchain-openai

from unstructured.partition.pdf import partition_pdf
from langchain.docstore.document import Document
from langchain_openai import ChatOpenAI
from langchain.schema.messages import HumanMessage
import tempfile
import base64
import os
from io import BytesIO

os.environ["OPENAI_API_KEY"] = "(OpenAIのキー)"

filename = "(ファイル名)"
option_mode = "option3"

def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

def summarize_image(image_base64):
    prompt = """あなたは画像の内容を説明する役割をもっています。
入力された画像の内容を詳細に説明してください。
基本的には日本語で回答してほしいですが、専門用語や固有名詞を用いて説明をする際には英語のままで構いません。
"""
    chat = ChatOpenAI(model="gpt-4-vision-preview", max_tokens=1024)
    human_message = [
        HumanMessage(content=[
            {"type": "text", "text": prompt},
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}}
        ])
    ]
    msg = chat.invoke(human_message)
    return msg.content

docs = []
with tempfile.TemporaryDirectory() as image_dir:
    elements = partition_pdf(
        filename,
        strategy="hi_res",
        languages=['jpn', 'eng'],
        extract_images_in_pdf=True,
        extract_element_types=["Image", "Table"],
        infer_table_structure=True,
        chunking_strategy="by_title",
        max_characters=800,    # チャンクサイズ
        new_after_n_chars=760,
        image_output_dir_path=image_dir,
    )
    for element in elements:
        if "unstructured.documents.elements.CompositeElement" in str(type(element)):
            element_text = str(element)
            # テキストをいろいろ加工
            metadata = {"source": filename, "type": "text"}
            docs.append(Document(page_content=element_text, metadata=metadata))
    for image_file in sorted(os.listdir(image_dir)):
        if image_file.endswith('.jpg'):
            image_path = os.path.join(image_dir, image_file)
            if os.path.getsize(image_path) <= 20 * 1024:
                # 20kb以下のファイルは無視
                continue
            image_base64 = encode_image(image_path)
            if option_mode == "option1":
                metadata = {
                    "source": filename,
                    "type": "image",
                }
                docs.append(Document(page_content=image_base64, metadata=metadata))
            elif option_mode == "option3":
                image_text = summarize_image(image_base64)
                metadata = {
                    "source": filename,
                    "type": "summary_image",
                    "original": image_base64,
                }
                docs.append(Document(page_content=image_text, metadata=metadata))

vectorstore = (何かしらのベクトルストア)
for doc in docs:
    if not "type" in doc.metadata \
        or doc.metadata["type"] in ["text", "summary_image"]:
        vectorstore.add_documents([doc])
    elif doc.metadata["type"] == "image":
        embeddings = vectorstore.embedding_function.embed_image(
            [BytesIO(base64.b64decode(doc.page_content))]
        )
        vectorstore.add_embeddings(
            texts=[doc.page_content],
            embeddings=embeddings,
            metadatas=[doc.metadata],
        )

retrieve結果の集約

langchainのConversationalRetrievalChainチェインを使用してRAGを行っている場合には、retrieverで取得した情報をCombineDocumentsChainチェインで一つのinputsにまとめてからLLMへの問い合わせを行っています。

この部分もマルチモーダル対応をする必要があるため、CombineDocumentsChainチェインの一つであるStuffDocumentsChainチェインに含まれる_get_inputs()を改造しました。
冒頭のテキスト部分はそのままで、途中にイメージ関係の処理を追加しています。 retrieverで取得したイメージについては配列にして、キー"image"でinputsに追加するようにしました。また、input_variablesにもキー"image"を追加しています。

コードを見る

class CustomStuffDocumentsChain(StuffDocumentsChain):
    def _get_inputs(self, docs: List[Document], **kwargs: Any) -> dict:
        """Construct inputs from kwargs and docs."""
        # Format each document according to the prompt
        doc_strings = [format_document(doc, self.document_prompt) for doc in docs if not "type" in doc.metadata or doc.metadata["type"] == "text"]
        # Join the documents together to put them in the prompt.
        inputs = {
            k: v
            for k, v in kwargs.items()
            if k in self.llm_chain.prompt.input_variables
        }
        inputs[self.document_variable_name] = self.document_separator.join(doc_strings)
        
        # images
        image_docs = list(filter(lambda doc: "type" in doc.metadata and doc.metadata["type"] in ["summary_image", "image"], docs))
        if (hasattr(self.llm_chain.llm, "model_name") \
            and self.llm_chain.llm.model_name in ["gpt-4-vision-preview", "gpt-4o"] \
            and len(image_docs) > 0:
            images = []
            for doc in image_docs:
                if doc.metadata["type"] == "summary_image":
                    metadata = {
                        "file_id": doc.metadata["file_id"],
                        "source": doc.metadata["source"],
                        "type": doc.metadata["type"],
                        "summary": doc.page_content,
                    }
                    images.append(Document(page_content=doc.metadata["original"], metadata=metadata))
                elif doc.metadata["type"] == "image":
                    images.append(doc)
            inputs["images"] = images
            if not "images" in self.llm_chain.prompt.input_variables:
                self.llm_chain.prompt.input_variables.append("images")
        return inputs

マルチモーダル形式への変更

langchainのプロンプトテンプレートはGPT-4oやGPT-4VといったマルチモーダルLLMの入力には対応していないようでしたので、その部分も改造するようにしました。以下はHumanMessagePromptTemplateに含まれるformat()を改造した時の例です。

テキストのみの場合には従来の処理をして終了します。
マルチモーダルの場合には配列にテキストとイメージを順番にdict形式で追加していきます。

コードを見る

class CustomHumanMessagePromptTemplate(HumanMessagePromptTemplate):
    """Human message prompt template. This is a message sent from the user."""

    model_name: str

    def format(self, **kwargs: Any) -> BaseMessage:
        """Format the prompt template."""
        text = self.prompt.format(**kwargs)
        if not self.model_name in [
            "gpt-4-vision-preview",
            "gpt-4o",
        ]:
            return HumanMessage(content=text, additional_kwargs=self.additional_kwargs)
        
        content = []
        content.append({"type": "text", "text": text})
        if "images" in kwargs:
            for image in kwargs["images"]:
                content.append({
                    "type": "image_url" ,
                    "image_url": {"url": "data:image/jpeg;base64," + image.page_content }
                })
        return HumanMessage(content=content, additional_kwargs=self.additional_kwargs)

組み込み結果

上記の改造をlangchain側に施し、UIを調整すると以下のように無事にマルチモーダルのRAGを実現することができました。
この例では資料として有名なTransformerの論文を読み込ませた上で、質問しています。
論文内のTransformerのアーキテクチャー図を元にした解説が返ってきました。
意外とイメージ中の文字を正しくスキャンできていたり、表の構造を正しく理解していて驚きました。 特にGPT-4oになってからは文字の認識精度は格段に上がっています。 そして、HEROZ ASKの機能である参照文の表示でもイメージを表示できるようにしています。

マルチモーダルRAGの結果

マルチモーダルRAGは成功しましたが、以下のような課題(苦労話)もありました。

トークン数の計算

langchainのConversationalRetrievalChainチェインではretrieverで取得した情報のトークン数が最大トークン数より多い場合には、スコアが低いチャンクを切り捨てる_reduce_tokens_below_limit()という関数があります。
イメージをマルチモーダル対応LLMに送る場合にはトークン数の計算が異なるため、この関数も改造する必要がありました。
特にOption1の場合には本文(page_content)にbase64エンコードしたイメージを格納しているため、そのままだとすぐに最大トークン数を超過してしまい、1枚もイメージが送られなかったことがありました。

Option1の精度

Option1でマルチモーダルRAGを試したところ、テキストとイメージが混在しているドキュメントではイメージの部分がヒットしないという現象がありました。
調査してみますと、テキストとイメージでretrieveのスコアが4倍ぐらい異なっていることが分かりました(イメージのスコアがテキストのスコアの1/4)。

OpenClipのようなマルチモーダルエンベンディング(Multi modal embedding)はテキストとイメージがシームレスに取り扱えるかと思っていましたが、現実的には同じスケールで評価されないようです。
Option3はイメージを一度サマリーにして検索するので情報の抜け落ちが気になりますが、テキストとイメージが混在している場合はOption3でしかうまく動作しませんでした。
テキストとイメージがよりシームレスに評価されるマルチモーダルエンベンディングが登場することが待ち望まれます。

図表のタイトル付与

マルチモーダルRAGも動作するHEROZ ASKのプロトタイプは第8回 AI・人工知能EXPO【春】で展示しており、パワーポイントで作成されたグラフや表入りの決算資料を検索するデモを準備していました。
ところが、unstructuredで切り出したグラフや表は、以下のようにグラフや表の単体になってしまい、前後の文脈である何に対するものかが分からないものになっていました。 このため、マルチモーダルRAGでうまく検索できない問題が発生しました。
例のグラフは弊社決算資料のものです。

売上高グラフ(改良前)

こちらについては、ヘッダーやタイトルも同時に切り出して、元のイメージに無理やりそれらを合成することで解消しました。
以下の改良後のグラフには「売上高」という文字列が含まれるようになったため、「2024年3Qの売上高は?」のような質問も正しく回答するようになりました。(正解はグラフ中の1,296百万円)

売上高グラフ(改良後)

今回は展示会のデモ用に限定的な条件で動作するようにしましたが、より汎用的に使えるようにすることは大きな課題だと思います。

おわりに

今回はマルチモーダルRAGをHEROZ ASKのプロトタイプに組み込み、無事に図表を含めて検索できることに成功しました。
そして、これを第8回 AI・人工知能EXPO【春】の弊社ブースで展示し、多くのお客様から驚きの反応を頂くことができました。

一方で、上記にも書いたように、

  • マルチモーダル対応LLMの認識精度
  • 前後の文脈を補完する方法

あたりはまだまだ課題であることが分かりました。
今後は、この辺りの課題をLLMの発展や技術改良により解決して、製品化できるようにしたいと思います。

HEROZ ASKのGPT-4o対応について

はじめに

当社では、ChatGPTのAPIを活用した「HEROZ ASK」というサービスを提供しています。この度、リリースされたばかりのGPT-4oに対応したことで、RAG(Retrieval-Augmented Generation)機能を大幅に強化しました。本記事では、GPT-4oの特徴や「HEROZ ASK」における具体的な活用方法、そして新しい機能がどのようにお客様の業務効率化に寄与するかについて解説します。

GPT-4oとは

GPT-4oは、OpenAIが開発した最新の言語モデルであり、従来のGPT-4に比べて以下の点で改良されています。

  • マルチモーダル: テキスト、音声、画像、動画の入力を受け付け、テキスト、音声、画像の出力を生成できる。
  • 高速: 音声入力に対して平均320ミリ秒で応答可能(テキストに対しても高速)。
  • コスト: GPT-4 Turboと同等の性能だが、APIでは50%安価で提供。
  • 多言語: 英語以外のテキスト処理能力が大幅に向上。新しいトークナイザーにより言語ファミリー間でトークン数を大幅に削減。
  • 視覚・聴覚理解の強化: 既存モデルと比べ、視覚と聴覚の理解力が特に優れている。
  • 安全性: トレーニングデータのフィルタリングやポストトレーニングにより、モダリティ間でセーフティが設計段階から組み込まれている。

HEROZ ASKでは、主にテキストを対象としてRAGの用途で使う場合が多いため、テキストの性能に焦点を当てて解説します。

テキストの性能

下記のグラフは、GPT-4oのテキストの性能を様々なベンチマークスコアで比較したものです。 GPT-4oが多くの評価項目において他のモデルよりも高いパフォーマンスを示しています。

https://openai.com/index/hello-gpt-4o/

日本語のRAGの性能

GPT-4oを日本語の文章に対してRAG用途で使用した場合に性能が向上するかを、具体的な例で確認してみます。 検証には、GPT-4oに対応したHEROZ ASKを使用します。

RAG(Retrieval-Augmented Generation)とは

RAGは、情報検索と生成モデルを組み合わせたアプローチです。 具体的には、

  1. 情報検索: 知識ベース(ベクトルデータベースなど)から関連情報を検索
  2. 生成: 検索された情報を基にテキストを生成

を組み合わせます。 これにより、言語モデルが学習していない最新の情報や企業の固有の情報を活用した応答生成が可能となります。

データソース

青空文庫から「走れメロス」をテキストとして保存して、HEROZ ASKのデータソースにアップロードを行います。 アップロードしたファイルは、設定に従ってチャンクに分割され、ベクトルデータベースに格納されます。

  • デフォルトのチャンク分割設定

  • アップロードされた状態

AIアシスタント作成

作成したデータソースを使用して、質問に回答するAIアシスタントを作成します。 AIアシスタントの作成は、プロジェクト作成から行います。 HEROZ ASKでは、チェインタイプを選択することで、AIアシスタントの振る舞いを変えることができます。 今回は、ドキュメント検索を選択します。 ドキュメント検索は、RAGの機能を実現します。 なお、独自のチェインを構築してAIアシスタントの振る舞いをカスタマイズすることも可能です。

  • プロジェクトの詳細

プロンプトはデフォルトで用意しているプロンプトを使用します。 Few-shot Learningで、ユーザの質問に対して、回答と、回答に使用したドキュメントを答えるようになっています。 プロンプトをカスタマイズすることも可能です。

モデルごとのRAGの性能確認

作成したAIアシスタントを使って、モデルを切り替えながら、質問に正確に答えられるか確認します。

質問文と想定回答文は、以下の通りです。

  • 質問文

「メロスは王に対して何を約束しましたか?」

  • 想定回答文

「メロスは王に対して、妹の結婚式を行うために三日間の猶予をもらい、その間に市に戻ってくることを約束しました。もし約束を破って戻ってこなかった場合、身代わりとして友人のセリヌンティウスを処刑しても構わないと誓いました。」

GPT-3.5

GPT-3.5の回答は以下の通りです。 間違った内容を回答しています。

GPT-4

GPT 4も同じように間違った回答をしています。

GPT-4o

GPT-4oの回答は以下の通りです。 想定通り回答できています。

まとめ

HEROZ ASKを使って、日本語の文章で、モデルごとのRAGの精度を比較しました。 この記事で記載したのは1つの例だけですが、GPT-3.5やGPT-4で間違って回答していた質問にも、GPT-4oを使うことで正しく回答できることが確認できました。 OpenAIの公表通り「英語以外のテキスト処理能力が大幅に向上」が確認できる結果となりました。

また、回答の生成の速度もGPT-4と比べて体感できるほど速くなっています。

GPT-4oの導入により、「HEROZ ASK」は日本語のRAG性能を大幅に向上させました。 これにより、お客様はより正確な情報検索と生成が可能となります。 今後も更なる改善を続け、皆様のビジネスに貢献できるよう努めてまいります。 ぜひ、新しいGPT-4oを使用して「HEROZ ASK」を活用してください。

herozask.ai

RAGとMulti Query Retriever: 社内ナレッジ検索結果の精度向上の鍵

はじめに

こんにちは、HEROZ ASK の開発チームです。

herozask.ai

今回のポストでは、このプロダクトの開発で活用している検索精度の向上技術についてお話します。

知識抽出におけるRAGの役割

そもそも現在公開されているLLMをそのまま用いて社内ナレッジについて質問すると、事実に基づかない文章を生成してしまう、いわゆる『ハルシネーション』が起きてしまいます。
このハルシネーションに対抗する手段のひとつがRAGです。RAG(Retrieval Augmented Generation)は、検索ベースのモデル(Retrieval)と生成ベースのモデル(Generation)を組み合わせたアプローチです。RAGは特定のクエリに関連する情報を集め、その情報を元に詳細な回答を生成する仕組みのため、ハルシネーションの軽減に貢献します。
HEROZ ASKでは、ベクトル型データベースを用いて関連する情報をドキュメントから取得し、それを元に回答を生成しています。RAGを通じて、生成される回答がドメインの内容に沿った一貫性を確保できると期待されます。

RAGの限界

シンプルなRAGで検索対象にできるドキュメントの範囲は、ユーザが入力したクエリの内容を超えられません。例えば「旅行の予約方法」とだけ入力されたとき、LLMは航空券やホテルの予約方法について一般的な回答が行えます。しかしユーザが実際にはツアーパックの予約方法について知りたがったり、割引チケットの購入方法に知りたいときにLLMはそれらの情報を提供することができません。

LLMによるクエリ拡張

クエリ拡張とは、検索クエリに関連するキーワードやフレーズを追加してより適切な検索結果を得るための手法です。
一般的なクエリ拡張の手法としてはシソーラスを用いたものなどがあります。シソーラスによるクエリ拡張は同義語や関連語を検索クエリに追加し、より幅広い検索結果を引き出します。しかしながら、シソーラスによるクエリ拡張はドメイン知識を必要とするため、すべてのクエリに対して効果的とは限らない問題があります。
これに対してLLMを用いるクエリ拡張では、広範な知識を開発者の実装なしに利用できます。与えられたクエリから複数パターンの質問を生成することで、より適切なドキュメントを検索対象に取りやすくなります。LLMによるクエリ拡張の最大の利点は柔軟性と精度の両立です。多くの発表されているLLMは様々な文脈やトピックに対応する能力を持ち、ドメインに関する知識を必要としません。
しかし、LLMによるクエリ拡張には生成するクエリが必ずしもユーザの意図を正確に反映するとは限らないという問題があります。この問題を克服するため、たとえばLLMからの返答に対してユーザがgood/bad評価でフィードバックするRLHF(人間のフィードバックによる強化学習)を採り入れるなどの施策が必要になります。

Multi Query Retrieverによるクエリ拡張

LLMによるクエリ拡張手法のひとつに、Multi Query Retrieverがあります。
Multi Query Retrieverはユーザが入力したクエリに対してLLMを用いてクエリを複数パターンに拡張する手法です。

(参考)MultiQueryRetriever | 🦜️🔗 Langchain

今回LLMへ渡すテンプレートは上記リンク先にあるものの日本語訳です。

あなたは AI 言語モデルのアシスタントです。 あなたのタスクは、指定されたユーザーの質問から5つの異なるバージョンを生成し、 ベクトルデータベースから関連するドキュメントを取得することです。 ユーザーの質問に対して複数の視点を生成することで、 ユーザーは、距離ベースの類似性検索の制限の一部を克服します。 これらの代替質問を改行で区切って入力してください。 元の質問: {question}

Multi Query Retrieverを利用したクエリ拡張時の性能とコストの評価

シンプルなRAGとMulti Query Retrieverをクエリ拡張に用いた場合の性能とコストを比較していきます。
実験では当社の社内wikiに該当するドキュメントが存在する、または無関係な25の設問に対してRAGを用いて知識抽出を行いました。正しい文章を生成している、または該当するドキュメントが存在しないときに回答しないケースを正答としています。
使用モデルはGPT-4で、LangChainのDebugモード下で実行しています。
下表はMulti Query RetrieverでのRAGとシンプルなRAGに対する比を掲載しています。

シンプルなRAG Multi Query Retriever 比率
正答数 19/25 24/25 126.32%
平均処理速度(s) 25.4 26.4 103.94%
平均消費トークン数 2821.6 2832.1 100.37%
  • LLM生成によるクエリ拡張を施したMulti Query Retrieverでは、シンプルなRAGに対して26.32%精度が向上しました。
  • 処理速度は大きく変わらない。
    • これは処理速度がLLM APIサーバ側の処理に律速されるためだと考えられます。
  • Multi Query Retrieverでクエリ拡張を行っても、全体の消費トークン数は大きく跳ね上がることはないと言えます。

まとめ

以上から、多少の処理速度と消費トークン数の増加と引き換えに、LLM生成によるクエリ拡張によって精度を向上させることができたと言えます。
本実験の時点ではチャンク分割には段落などを気にせず文字列を分割する RecursiveCharacterTextSplitter を利用していました。その後ドキュメントのチャンク分割にMarkdownのヘッダー構造をメタデータに持てる MarkdownHeaderTextSplitter を用いることでドキュメント内の意味的階層構造をLLMが解釈できるようになり、検索精度の向上に繋がることが分かりました。

別の実験でEmbeddingモデルを切り替えることでも精度が向上する場合があると分かったため、今後はドキュメントの読み込み方法を工夫しつつ、プロンプトを改善して更なる精度向上を目指していきます。