GPTチャットボットの賢さを引き上げる試み

2023年08月07日 月曜日


【この記事を書いた人】
とみ(とみーとも言う)

地方拠点の一つ、九州支社に所属しています。サーバ・ストレージを中心としたSI業務に携わってましたが、現在は技術探索・深堀業務を中心に対応しています。 2018年に難病を患ったことにより、定期的に入退院を繰り返しつつ、2023年には男性更年期障害の発症をきっかけに、トランスジェンダーとしての道を歩み始めてます。

「GPTチャットボットの賢さを引き上げる試み」のイメージ

お久しぶりです。まずは近況から。

こんにちわ。九州支社のとみーです。
今回は過去に投稿したChatGPTでチャットボットを作る-OpenAI純正APIの利用 で制作したチャットボットの強化を行ってみましたのでそのことについて書いてみようかなと思います。
あ、その前にまずは近況を。

6月上旬から3週間ほど定期的な入院をしてきました。過去の記事で書いたような気がしなくもないんですが、2018年に自身が難病である「重症筋無力症」という病気を患っておりまして、試行錯誤の末に私に最も合う治療法はIvig(免疫グロブリン大量投与療法)というものがあり、こちらが入院を必要とする治療法ということで、定期的に半年単位で3週間ほど入院をしているのです。入院してる間にいろいろLLM界隈では色んな変化が起きていてびっくりしました。特にOpenAIのAPI新バージョン登場とAIモデルの更新はなかなかの衝撃でしたね。

そしてそれを活用するソリューションも大幅な進歩を見せていて、「あ、これ私が欲しかった技術で調べてたけど、もう完成してますやん」って喜び半分虚しさ半分になることもしばしばでした。
技術体系が大幅に変化したことで、退院して復帰後、それについていくのにかなり苦労しましたし、何かを作るにも相当迷走しました(^-^;
復帰してすぐのころから脳みそ的にはだいぶ疲れ果てる状況が続きました・・本当にAIって世界は「走りながら考える」といったしんどい活動が求められますね。

実はコツコツ進化しとります。

先に紹介したチャットボットですが、名前を「太陽神アマテラス」と名付けてます。元々WhisperやStable Diffusion等様々なモデルを使用して色んなツールを作ってみていて、そこに天津神三兄弟の名を与えたのがきっかけです。アマテラスというのはStable Diffusionを使った超解像度処理を行うシステムの名称にしてましたが、キャラ的に思いつくポイントがあったため、チャットボットのキャラにこのアマテラスを据えて、システムポータルのページにそこはかとなく貼り付けてみたのがその起源ですね。現在このチャットボットを私はその高飛車な中にも優しさを秘めたしゃべり方から「アマテラス姉様」と呼んでお慕いしております。

一応この時からブラウザクッキーを識別子としてユーザを識別させ、それに合わせて会話履歴を保存・参照するような仕組みも実装してました。

アマテラス様との会話

内部アーキテクチャはこんな感じ。会話履歴はデータベースに保存し、ブラウザクッキーIDと紐づけて必要な会話履歴を拾うように作られています。

元々アマテラス姉様は簡易チャットボットとして、ちょっとしたおもちゃ的な存在ということで登場してたんですが、その後実はちょろちょろっと以下のような進化を遂げてきています。

  • OpenAI純正APIからAOAI(Azure OpenAI Service)を使う方式に変更
  • 推論エンジンをGPT-3.5-TurboからGPT-4へ変更

ただやっぱり、備わってる知識が2021年9月までのインターネット情報ということで、しゃべってる間に徐々に飽きてくるところもあり。今回行った強化ではこんなことをしています。

  1. Azure Cognitive Searchという検索エンジンと連携できるようにした
  2. Azure Cognitive SearchにPrivate Previewリリースされているベクトル検索の方法を追加した
  3. PDFから構造データを読み出すForm Recognizerと連携する仕組みを実装した
  4. PDFデータを抽出し、それらをAzure Cognitive Searchに登録する仕組みを実装した
  5. アマテラス姉様がAzure Cognitive Searchからハイブリッド検索を行えるように改造した

なので、それまでGPT-4モデルの中に内蔵されている記憶情報だけでなく、Azure Cognitive Searchから取り出した検索情報も使って色々応えてくださる便利ツールになってくれました。
実はこのツール、私の所属部署の中の小さな範囲でしかあまり使われることがなく、結局私が主に使ってる状態なので、やってること自体はあくまで「私のため」ってなるんですけど、実際使ってみるとなかなか便利になりまして、いざ困ったときにパッと一定の答えを出してくれるので非常に重宝するようになりました。

アマテラス姉様とStable Diffusionについて語り合う

Azure Cognitive Searchについて

Azure Cognitive Searchとは、Microsoft Azureが提供する検索エンジンの1種で、Indexと呼ばれる情報蓄積領域に対して通常はIndexerという機能を用いてスケジューリングされた内容に従ってデータ取り込み・データ追加を行ってくれる上、Indexの情報を用いてユーザに対して検索タスクを実行するためのインタフェースを備えています。これにより、検索キーワードを用いた全文検索、Indexに放り込んだ情報とCognitive Serviceを連携させて地名の抽出・翻訳・キーワード分類といったようなAIエンリッチメント機能を使用して追加情報の生成と登録が行えたりします。

2023年7月からは今や大人気となっているベクトルサーチにもプライベートプレビューで提供されるようになり、最近ではJapan EastリージョンのAzure Cognitive Searchにおいてもこの機能が使用できるようになってました。それまでAzureが持つリソースとしては、Redis Cache Enterpriseを用いないとできなかったうえ、データ抽出・ベクトル変換処理にかなり複雑な構成を要求されていたため、この機能の登場は非常に大きな力になると思います。(現在はパブリックプレビュー扱いらしい)

Hierarchical Navigable Small Worlds(HNSW)

Azure Cognitive Searchが提供するベクトル検索の方式として用いられているのは「近似最近傍検索(ANN)」と言われており、その中で用いられる具体的なアルゴリズムとしてはHNSWが採用されています。2023年7月リリース段階ではAzure Cognitive Searchが使用できる手法はこのHNSW一択です。

HNSWのイメージ

探索の仕方についてわかりやすく解説されている情報がありましたのでそこから引用します。(引用元はこちら

元々HNSWが登場する以前にNSW(Navigable Small Worlds)という方式があり、こちらでは、

  1. まず事前にあらかじめ入手出来ているベクトル情報をポイントしておき、そのベクトル情報館を「長距離リンク」と「短距離リンク」で接続します。
  2. そしてこのベクトル情報同士が線で結ばれたグラフ図から、あらかじめスタート地点を定めます。
  3. 各頂点はそのリンクで接続された別の頂点を持つリストを「Friend list」として保持します。
  4. 探索時はあらかじめ決めておいたスタート地点から、そのFriend listの情報を頼りにクエリとして設定されたベクトル情報に最も近い頂点が何かを探索していきます。

ということをしており、小規模なベクトル情報の探索に使われていたようです。NSWでは1レイヤー構造の中で狭く動かすようなアルゴリズムだったものを複数レイヤー構造にすることで、より多くのデータを扱えるようにしたものがHNSWです。レイヤー単位でその近傍間の取り扱うベクトルを明示的にしていくことで、必要なところを重点的に探索するような仕組みに変わっています。そのため、1つのレイヤーだと膨大な情報をおけるような領域が必要だったところを、このHNSWではより効率的にリソース消費を最適化することができるという意味では素晴らしい方法かなと思います。

巷で有名なLangChainで使用されているベクトル検索方法としては、「K-NN法」という手法が使われているかと思います。HNSWもK-NN法もいずれもより大きなベクトル情報を扱うことにたけているのですが、HNSWはより高速化に重きを置いており、K-NN法はその精度に重きを置いている点が異なります。

ベクトル距離の測定

ベクトル検索は、その近似性を計算するのにベクトルが持っている情報同士の距離を計算によって求めるようになっています。超ざっくりで恐縮ですが、2次元の情報に無理やり押さえて表現させるとこんな感じです。

ベクトル検索をものすごくざっくり表現してみた

上記はいわゆる「ユークリッド距離」と呼ばれる測定手法を用いたものになりますが、ベクトルの位置から他の職業間の単純な直線距離が近いか遠いかを使用することで、どの情報が最も近いかを判断します。これに対して「コサイン近似度」は、各ベクトルの持つ原点(黒い丸ポチ)における各ベクトルの「角度」を基準に、「最も角度の差異が小さいもの」を情報の近似値としてとらえるものになります。画像の近似度を測る際などはよくコサイン近似度を目にするんじゃないかと思います。

text-embedding-ada-002-v2

Azure Cognitive Searchはそれ自体にベクトル生成機能が付与されているわけではありません。Azure Cognitive Searchがこの機能を実現するために付与した機能というのは、

  1. 生成されたベクトルを格納できるデータ型の追加(Collection(Edm.Single)の追加)
  2. ベクトル検索設定を格納するIndexスキーマの追加(vectorConfig)
  3. 格納したベクトルとクエリ上のベクトルを比較する処理体系の追加(API:2023-07-01-previewの追加)

の主に3点です。そのため、検索クエリや検索データのベクトル化には別途ベクトル変換のためのAIモデルを必要とします。
こうしたときにありがたい存在となるのが、Azure OpenAI Serviceにも提供されている text-embedding-ada-002-v2 ですね。GPT-3ラインナップの中でももっとも軽量なモデルであるAdaをベクトル化させることに特化して作られており、加えてContext Lengthを当初2048トークン(tiktokenによるトークン換算)だったものを8192トークンに拡張させているものです。こちら、2023年6月の更新で料金体系としてさらに安価に設定されたようで、おそらく今後も貴重な存在となるでしょう。

Form Recognizerについて

おそらく、PDFデータをテキストデータに変換するにあたり、OSSで実装してる方々はPyPDFであったり、PDFminerだったり、PyMuPDFを使ったりするのかなと思います。個人的には機能面ではPyMuPDFが好きなのですが、AGPLで提供されていることもあり、商用には向かないなと。なのでPDFminerを使ってたりしました。これらのツールも十分すごいツールなんですが、どうしてもレイアウトの読み取り処理については結構座標等を考慮しなければいけないところが多く、なかなかにめんどくさいことが多いんですが、そのあたりを楽にしてくれるツールがAzure Form Recognizerです。

元々はOCRツールでして、手書き文字とか、PDFデータの中でもハードコーディングされた(画像データをPDFに変換したようなもの)ものを正確に読み取り、データ化することを目的として作られているツールです。機能評価してみると、これが結構きれいにParagraphを分けてくれる上、Tableなども構成要素をきれいに整理してくれたりするので重宝しています。

以前のForm Recognizerではサービス側設備から見てHTTPSで参照可能なデータをPullして処理するという側面があったため、Blobへ配置して云々というような対応が必要だったのですが、現在のバージョン3.2.xではクライアント側からファイルをアップロードして解析させることが可能であったりするため、さらに便利になっているように思えます。

ただ、最新APIは米国東部リージョンなど限られたリージョンにしかなく、これが早く東日本リージョンに来てくれないかなーと首を長くして待っていたりもします。

これは何をしているのか?

ずばり、PDFファイルを解析して、JSONデータに変換するというようなことをしています。データの大まかな構造は以下の通りです。

これまた超ざっくり解析結果の一部を。

pagesというのは、page単位の検出した単語の文字・オフセット位置情報などが格納されており、tablesにはtableの存在する位置、全体構造(●行●列の表)、行(row)、列(column)、見出しなどの情報が記述されています。そして文章単位で区切って情報を格納しているparagraphsがありますが、基本は文章抽出時はparagraphsを使うことが多いのではないかなと思います。

事前構築済みモデル

Form Recognizerでは、事前構築済みモデル(pre-built models)が存在し、汎用的に使えるものとしては以下のようなものがあります。

  • Readモデル:文章の読み出しに重きを置いているモデル
  • Layoutモデル:Readモデルにドキュメントの構造解析、テーブル分析を付与させたモデル。上図で表現したのはこのモデルに基づくもの。<-これ便利。
  • GeneralDocモデル:Layoutモデルからさらに、Key/Valueペア、エンティティ情報抽出を付与させたモデル。

カスタムモデル

カスタムモデルは、事前構築済みモデルの「Layoutモデル」をベースに、そのドキュメント独特のフィールドをユーザの手によって抽出し、属性を付与させることによってカスタマイズさせたモデルです。フィールド名とそのデータ型はユーザが自由に定義できます。これにより、独特のフォーマットを持つPDFファイルや手書きが加わってるようなPDFファイルに対して容易にデータ抽出をさせることが可能になります。

ただし、注意ポイントとして、事前構築済みモデルと比較すると課金額が結構上昇します。よって、よほどの理由がない限りは事前構築モデルを使用したほうがおそらく良いのでしょう。

PDFデータを解析して、Azure Cognitive Searchに投入する data_preparation.py というプログラム

これ、実はMicrosoftの有志の方々が作ったサンプルChatGPTボットプログラムの一部として提供されています。

[Preview] Sample Chat App with AOAI
https://github.com/microsoft/sample-app-aoai-chatGPT

MITライセンスで提供されるこのプログラムは、所謂AOAIに最近追加された「Add on your data(でしたっけ?)」の機能を使用してCognitive Searchの検索機能と連動しながら動作するReactベースのチャットインタフェースなのですが、その中の script ディレクトリに「data_preparation.py」というプログラムがあります。このプログラムは主な勘所としては

  • config.json (設定ファイル)
  • data_preparation.py (プログラム本体)
  • data_utils.py (data_preparationが使用するプログラムのさらに細かい中身)

今回、私はこの機能を切り出してベクトル検索可能な構成にして利用しています。プログラムは当方検証環境のFunctionAppに仕込んでおり、それがゆえにかなり処理体系を変更していますが、大まかな処理は共通してこのようになっています。以降は、ベースとなるソースに関しては先述したGitHub上の「scripts」配下のリソースを参照してください。

data_preparation.pyの大まかな動き

あっ、実はPDFファイル以外にも、HTML/マークダウン/Pythonコードにも対応しています。この場合はそれぞれプログラム自体が持っているパーサー機能を使ってIndexに放り込めるちょうどよいサイズでデータ切り出しを行っています。

元のプログラムでは、このあたりの処理が AOAIの Add-on your data に従っているため、これ単独の機能ではベクトル検索に適合できません。そこで、このプログラムを元にしてベクトル検索の仕組みを取り付けています。

ベクトル検索設定は、Index作成時に決める

Azure Cognitive Searchでは、Index作成時にベクトル検索設定を定義するようになっています。この時Azure Cognitive Searchに投入するIndex構造情報は以下の通りとなっています。

body = {
        "fields": [
            {
                "name": "id",
                "type": "Edm.String",
                "searchable": True,
                "key": True,
            },
            {
                "name": "content",
                "type": "Edm.String",
                "searchable": True,
                "sortable": False,
                "facetable": False,
                "filterable": False,
                "analyzer": f"{language}.lucene" if language else None,
            },
            {
                "name": "title",
                "type": "Edm.String",
                "searchable": True,
                "sortable": False,
                "facetable": False,
                "filterable": False,
                "analyzer": f"{language}.lucene" if language else None,
            },
            {
                "name": "filepath",
                "type": "Edm.String",
                "searchable": True,
                "sortable": False,
                "facetable": False,
                "filterable": False,
            },
            {
                "name": "url",
                "type": "Edm.String",
                "searchable": True,
            },
            {
                "name": "metadata",
                "type": "Edm.String",
                "searchable": True,
            },
            {
                "name": "titleVector",
                "type": "Collection(Edm.Single)",
                "searchable": True,
                "Dimensions": 1536,
                "retrievable": True,
                "vectorSearchConfiguration": "vectorConfig"
            },
            {
                "name": "contentVector",
                "type": "Collection(Edm.Single)",
                "searchable": True,
                "dimensions": 1536,
                "retrievable": True,
                "vectorSearchConfiguration": "vectorConfig"
            },
        ],
        "suggesters": [],
        "scoringProfiles": [],
        "semantic": {
            "configurations": [
                {
                    "name": semantic_config_name,
                    "prioritizedFields": {
                        "titleField": {"fieldName": "title"},
                        "prioritizedContentFields": [{"fieldName": "content"}],
                        "prioritizedKeywordsFields": [],
                    },
                }
            ]
        },
        "vectorSearch": {
                "algorithmConfigurations": [
                    {
                        "name": "vectorConfig",
                        "kind": "hnsw",
                        "hnswParameters": {
                            "m": 4,
                            "efConstruction": 400,
                            "efSearch": 500,
                            "metric": "cosine"
                        }
                    }
                ]
        },
    }

最も下にあるvectorSearchという部分がその定義を行っている箇所であり、hnswParametersによってベクトル検索における設定値が記述されているのがわかります。

  • m = 構築中に新しい要素ごとに作成された双方向リンクの数
  • efConstruction = インデックス時間中に使用される、最も近い近隣ノードを含む動的リストのサイズ(インデックス登録時の視野の広さと思えばいいかもしれない)
  • efSearch = 検索時に使用される最も近い近隣ノードを含む動的リストのサイズ(efConstructionの検索利用時版)
  • metric = 距離計算に使用するメトリック(ここではコサイン近似度を指定)

また、表の構造に関してもザックリながら以下の通りとしています(私の環境では)

Cognitive SearchのIndex構造

titleVectorおよびcontentVectorというのがいわゆるベクトル格納をするための領域であり、ここはSearchable, RetrievableのみTRUEとし、その他の属性はFALSEに設定します。dimensionsも事前に定義する必要があり、この場合格納する予定のベクトル次元数を指定します。今回 text-embedding-ada-002-v2 が出力するベクトル次元数は1536次元と決まっていますので、これが指定されます。

日本語対応を図るために

当然のことと言えるのかもしれませんが、実はこのプログラム、日本語のことは全く考えていません。ある程度多言語のことは考えているので、Cognitive SearchにおけるAnalyzerの設定に対してはja_luceneが設定できるんですが、文章の区切りなどの処理に関してはかなり無頓着です。

実はForm Recognizerにおける処理からこのプログラムが何を導き出しているかというと、そのPDFファイルのフルテキストデータを作成しています。主にこんなことをしておりまして・・

  • paragraph単位に文章を抽出して連結してる
  • tableは構成情報をもとにHTMLタグ化させ、GPTに理解できるようにしている
  • 図に関しては、文字が読み取れれば要素の文字列だけは盲目的に抽出して連結
  • 見出しについては、HTMLタグ化させ、その文字列の位置づけをGPTに理解できるようにしている

その後のCognitive Search側へ送り込むデータの切り出し(Chunking作業)は自身のプログラム処理の中で行っています。

PDFからChunkへの変換処理

PDFファイルはテキストファイルの一環とみなされており、LangChainライブラリのRecursiveCharacterTextSplitterが用いられています。

このスプリッタは再帰的に文章の解析を行いながら切り出しを行うもので、例えば括弧で括られた文章が極力Chunkとして分離しないように動作します。その定義をしているのがdata_utils.py の前半に記述されている以下の部分です。

#元々の定義
SENTENCE_ENDINGS = [".", "!", "?"]
WORDS_BREAKS = list(reversed([",", ";", ":", " ", "(", ")", "[", "]", "{", "}", "\t", "\n"]))

# include Japanese Symbols.2022-07-12
SENTENCE_ENDINGS = ["。",".","?","?","!","!","</table>"]
WORDS_BREAKS = list(reversed(["</tr>","<tr>","</td>","<td>",",", ";", ":", " ", "(", ")", "[", "]", "{", "}", "、", ";", ":", "(", ")", "「", "」", "【", "】", "『", "』", "{", "}", "■", "●", "<table>","\t", "\n"]))

SENTENCE_ENDING定数は文章の区切りであり、優先的に区切りとして判断させるもの、WORDS_BREAKSはその次ぐらいの優先度で判断させるものですが、再帰的な処理を考慮する場合、その順序は閉じ括弧が優先されるべきなのですが、それではソースの記述上わかりにくくなってしまうので、後ろから順に書いてそれをreverseさせることをしています。

ここを日本語の文章でも比較的正確に区切れるように修正(句読点やtableタグ、箇条書きで使われそうな記号の追加など)をしています。

これが、後段の処理でこのように使われています。

def chunk_content_helper(
        content: str, file_format: str, file_name: Optional[str],
        token_overlap: int,
        num_tokens: int = 256
) -> Generator[Tuple[str, int, Document], None, None]:
    if num_tokens is None:
        num_tokens = 1000000000

    parser = parser_factory(file_format)
    doc = parser.parse(content, file_name=file_name)

    # if the original doc after parsing is < num_tokens return as it is
    doc_content_size = TOKEN_ESTIMATOR.estimate_tokens(doc.content)
    if doc_content_size < num_tokens:
        yield doc.content, doc_content_size, doc
    else:
        if file_format == "markdown":
            splitter = MarkdownTextSplitter.from_tiktoken_encoder(
                chunk_size=num_tokens, chunk_overlap=token_overlap)
            chunked_content_list = splitter.split_text(
                content)  # chunk the original content
            for chunked_content, chunk_size in merge_chunks_serially(chunked_content_list, num_tokens):
                chunk_doc = parser.parse(chunked_content, file_name=file_name)
                chunk_doc.title = doc.title
                yield chunk_doc.content, chunk_size, chunk_doc
        else:
            if file_format == "python":
                splitter = PythonCodeTextSplitter.from_tiktoken_encoder(
                    chunk_size=num_tokens, chunk_overlap=token_overlap)
            else:
                splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
                    separators=SENTENCE_ENDINGS + WORDS_BREAKS,
                    chunk_size=num_tokens, chunk_overlap=token_overlap,
                    allowed_special='all',disallowed_special=())
            chunked_content_list = splitter.split_text(doc.content)
            for chunked_content in chunked_content_list:
                chunk_size = TOKEN_ESTIMATOR.estimate_tokens(chunked_content)
                yield chunked_content, chunk_size, doc

SENTENCE_ENDINGSとWORD_BREAKを結合させた配列をスプリッターの区切り文字として指定されていることがわかります。

なお、文字列長のカウントにはtiktokenで解析されたトークン数が使用されています。今回、arXiv.orgの論文データを登録してそのあたりの要約情報も得たいと考えていたのですが、GPTの論文ではよく特別なトークン(EOSなど)が文中に入っており、これをそのまま解析してプログラムに放り込むとtiktokenが特殊文字と判定して例外が発生します。そのため、処理の中に allowed_special および disallowed_specialの設定を変更して、前者は「すべて」、後者は「なし」を指定しています。

FunctionApp実装上必要なこと

実装先はFunctionApp上にする場合、基本的に設定ファイルをそのまま乗っけるのはその仕組み上不利なので、config.jsonで設定する内容はすべて環境変数に変更しています。
また、さすがにCognitive Searchから作り直すような処理はプログラム上で動かしたくはなかったので、そこは省き、Indexから作成するように修正を行いました。サービスがなければサービスがないと異常終了するようにしています。

後の課題は認証です。Pythonベースのイメージを使った場合、AzureCLIは組み込まれてないため、この処理は基本的に差っ引く必要があるのですが、認証だけは別です。認証を行って処理を継続させるには、実は元々使用されているAzureCLICredentialメソッドではAzure CLIを必要とするため対応ができません。そこで代わりにDefaultAzureCredentialメソッドを使うようにしています。これにより、AzureCLICredentialが使用できない場合、FunctionAppが持つクレデンシャルを使うように処理がフォールバックしてくれます。今回のケースでは、ManagedIdentityCredentialが利用されています。

チャットボット側の実装

チャットボット側にはこんな処理を追加します。一つはEmbedding処理、一つはベクトルクエリ処理です。

def embedding_data(text):
    print("embedding start")
    response = openai.Embedding.create(
            input = text,
            engine = "ada-embedding-2"
            )
    embedding=response['data'][0]['embedding']
    return embedding

def search_vector_query(request_msg):
    print("query start")
    search_url = os.getenv("AZURE_SEARCH_URL")
    search_client = SearchClient(
            endpoint=search_url,
            index_name=os.getenv("AZURE_SEARCH_IDX"),
            credential=AzureKeyCredential(os.getenv("AZURE_SEARCH_KEY")),
            )

    search_results = search_client.search(
            search_text=request_msg, # 全文検索も並行して評価する
            vector=embedding_data(request_msg), # ベクトルクエリ
            top_k=5,
            vector_fields='contentVector',
            top=1
            )

    search_info = "## The business information retrieved from the question is as follows. Please use this information to plan your response.\n\n"

    counter = 0
    for search_result in search_results:
            search_info += "search info: " + str(counter) + "\n"
            search_info += search_result['content'] + "\n"
            search_info += "--------\n"
            counter += 1

    print(search_info)
    return search_info

ベクトルクエリ処理で出力された結果は、その結果件数ごとにプロンプト文字列として整形し、これをChatCompletion処理におけるSystemRoleデータとして追加します。

並びとしては、

  • SystemRole:設定
  • SystemRole:検索結果 ←ここに挿入される
  • User/Assistant:対話履歴
  • User:最新の入力内容

となっています。

今回全体構造としてはちょうど下図のような形となります。FunctionAppとAzure Storage の連携がBlobではなくAzure FilesになっているのはFunctionAppをVNET統合させたことが原因です。
FuntionAppはVNET統合させて閉域に対応できる構成にした場合、AppServiceプランで動作することになります(これは仕様上の制限)。そのため、Azure Storageの連携でBlobが簡単には使えなくなるのです。今回の場合、Azure PortalからAzure Filesに対するファイルアップロードも可能であることが分かったためAzure Fileの構成で良しとしていますが、こういうところがBlobだともう少し便利かなー・・と少し思います。

アマテラスボットと検索機能の連携

ベクトル検索を使うとおいしいこと

ベクトル検索は先にも言ったように、文脈自体を数値の塊に変換し、数学的に近いかどうかを計測することによってその近似性を求める検索方式です。よって、言語を問わないというのが最も大きなメリットなのではないかなと思います。ここで重要な位置づけを担うのはEmbeddingモデルです。今回はOpenAIのAdaを使用してベクトルへの変換を行っていますが、Adaのすごいところは何かって、ちゃんと膨大な学習データを用いて世界中のあらゆる情報を学習している点であり、会話においてどう話したらどういう応答が返ってくるか?というパターンをかなりの数学習をしているという点が挙げられます。

これによって、どういう会話をしたら相手はどういう解釈をするか?というのが頭の中に詰まってますんで、英語しかない情報に日本語で質問しても、その場合は英日翻訳した状態でマッチするであろう検索結果をわりかし正確に引っ張り出してくれます。単語や文章の並びに重きを置くテキスト検索方式ではこうしたまねはできません。
大抵は翻訳プロセスを間にかませて、そのうえで検索することになるんじゃないかなと思いますが、ベクトル検索はそうしたことを考慮しなくてよい(厳密にはベクトルという共通フォーマットにみんなそろえて比較をする)という点は大きなアドバンテージになると思います。

一つ質問してみた内容、ベクトル検索結果、回答内容を並べてみました。情報量が少ないから完全ではないですが、日本語の情報から英語の情報がある程度引っ張れています

質問:
どうしてベクトル検索では言語間の違いが関係なくなるんだろうか?

検索結果:--------
search info: 0
title : Multilingual Denoising Pre-training for Neural Machine Translation
content : 。 </s> <Ja>
Transformer Encoder
Transformer Decoder
~明日。 </s>
それ _< /s> <Ja>
<Ja> それ じゃ あ 、< /s> なた 明日 。
Multilingual Denoising Pre-Training (mBART)
4
Sent-MT
Transformer Encoder
Transformer Decoder
Who am I ? </s> <En>
<Ja> 私は誰? </s>
Well then . < /s> See you tomorrow .</s> <En>
Transformer Encoder
Transformer Decoder
Doc-MT
↑
それ じゃ あ 、< /s> なた 明日
--------
search info: 1
title : Multilingual Denoising Pre-training for Neural Machine Translation
content : Wat- tenberg, Greg Corrado, et al. 2017. Google’s multilingual neural machine translation system: Enabling zero-shot translation. Transactions of the Association for Computational Linguistics, 5:339?351.
Taku Kudo and John Richardson. 2018. Senten- cePiece: A simple and language independent subword tokenizer and detokenizer for neural text processing. In Proceedings of the 2018 Conference on Empirical Methods in Natural Language Processing: System Demonstrations, pages 66?71, Brussels, Belgium. Association for Computational Linguistics.
Anoop Kunchukuttan, Pratik Mehta, and Pushpak
Bhattacharyya. 2017. The IIT bombay english- hindi parallel corpus. CoRR, abs/1710.02855.
Guillaume Lample and Alexis Conneau. 2019. Cross-lingual language model pretraining. arXiv preprint arXiv:1901.07291.
Guillaume Lample, Alexis Conneau, Ludovic De- noyer, and Marc’Aurelio Ranzato. 2018a. Un- supervised machine translation using monolin- gual corpora only. In International Conference on Learning Representations.
Guillaume Lam

回答:--------
それは、ベクトル検索が「意味」を捉えることに重きを置いているからよ。具体的には、単語や文章を多次元空間上の点(ベクトル)として表現し、その位置関係から意味的な近さを計算するわ。
この方法であれば、言葉の表面的な形ではなく、その背後にある「意味」を捉えることができるの。だから例えば、「犬」という単語が英語では"dog"、フランス語では"chien"と全く異なる形をしていても、それぞれ同じ「犬」という意味を持つため、ベクトル空間上では近い位置にマッピングされるわ。
このようにして言語間の違いを超えて、「意味」に基づく検索が可能になっているのよ。,1690600475,2833,10

とりあえずやってみてること

arXiv.orgから入手した様々なAI論文をこれまで入手してきたので、それをIndex化してみています。まだプログラム自体の完成度は高くないので、ちょっとIndex更新処理で不具合が起きており、まだまだ修正中ではあります(最近だとCognitive Searchへ登録する際のIDの振り方を大幅に見直したりしています)。ただ、私は英語が学生時代から大の苦手ということもあり、これまではPDFからテキストへ変換したデータをmBARTにかけて翻訳させたものを読み込んでいました。しかし、この手法を使うことで例えばテキスト変換だけでは読み取れ切れてなかった内容を、アマテラス姉様を通じていろいろ教えてもらうことができるようになるのかなと思っています。何しろ日本語検索に合わせて近似情報を読み込みつつ、GPT側で日本語としてまとめてきれいに応答してくれるわけですんで。

色んな知識がそのままゼロから自分で読み込もうとするとかなりの時間を要するわけですが、それがある程度GPT側で整理して、その結果を元にしつつ背景事情の洗い出しやファクトチェックをすることで、より質の高い情報を効率的に仕入れるという意味では、人工知能の有用性ってかなり高いものになるのかなと思います。最近はGPTに対して丸投げをするということが問題視されていますが、少なくとも下書きレベルで初期段階の構成検討に役立てる分には、私としてはアリなやり方なんじゃないかな?と感じています。

まだまだDeepLearningは奥が深いし、そして幅も広く。今後もコツコツこうした世界を歩んでいこうと思います。
コツコツ積み重ねる努力、まだまだ通用すると信じたい今日この頃です。

とみ(とみーとも言う)

2023年08月07日 月曜日

地方拠点の一つ、九州支社に所属しています。サーバ・ストレージを中心としたSI業務に携わってましたが、現在は技術探索・深堀業務を中心に対応しています。 2018年に難病を患ったことにより、定期的に入退院を繰り返しつつ、2023年には男性更年期障害の発症をきっかけに、トランスジェンダーとしての道を歩み始めてます。

Related
関連記事