LlamaIndexのインデックスを更新し、更新前後で知識がアップデートされているか確認してみた

あなたのデータでカスタマイズできるLlamaIndex 実はインデックス更新もデキル!カワイイ!!
2023.03.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんちには。

データアナリティクス事業本部 インテグレーション部 機械学習チームの中村です。

今回はFine-tuningなしにGPTをカスタマイズして使用可能な、LlamaIndex(旧称GPTIndex)のindex更新についてご紹介します。

LlamaIndexとは

LlamaIndexは大規模言語モデル(LLM)と外部データ(あなた自身のデータ)を接続するためのインターフェースを提供するプロジェクトとなっています。

LLMのカスタマイズするためのパラダイムには主に2種類があります。

  • LLMをFine-tuningする
  • 入力プロンプトにコンテキストを埋め込む

LlamdaIndexは後者を、より性能よく、効率よく、安価に行うために様々なデータ取り込みやインデックス化を実施することが可能です。

これらのことは、LangChainというライブラリでも実行できますが、LlamdaIndexはより高レベルのAPIを備えています。

実際、LlambdaIndexはその中でLangChainを使用しており、併用することも可能です。

LlamdaIndexの公式ドキュメントは以下を参照ください。

LangChainの公式ドキュメントは以下を参照ください。

今回はLlambdaIndexでindexの更新について検証してみました。

使ってみた

使用環境

Google Colaboratoryを使います。ハードウェアアクセラレータは無し、ラインタイム仕様も標準です。

主なバージョン情報は以下です。

!python --version
Python 3.9.16
!pip freeze | grep -e "openai" -e "llama" -e "langchain"
langchain==0.0.108
llama-index==0.4.26
openai==0.27.2

セットアップ

モジュールをインストールします。

!pip install llama-index
!pip install python-dotenv
!pip install html2text

このやり方にこだわる必要はないのですが、OPEN_AI_KEYを.envに書き込みます。

!echo 'OPENAI_API_KEY="あなたのOPENAIのAPIキー"' >.env

importは以下の通りです。

from dotenv import load_dotenv
load_dotenv()

from langchain import OpenAI
from llama_index import GPTSimpleVectorIndex, SimpleWebPageReader, LLMPredictor, OpenAIEmbedding

ドキュメントの作成

他記事と同様、自身のブログ記事の情報を使ってみます。

SimpleWebPageReaderにURLのリストを与えることで、読み込むことが可能です。

documents = SimpleWebPageReader(html_to_text=True).load_data([
    "https://dev.classmethod.jp/articles/reintro-managed-ml-transcribe/"
])

読み込まれたものは、Documentクラスのリストとなっています。

type(documents[0])
llama_index.readers.schema.base.Document

Documentクラスには、本文を表すtextの他に、doc_idなどが含まれています。

おそらくPydanticで作られたモデルですので、以下のようにto_dictで辞書型に展開が可能です。

documents[0].to_dict().keys()
dict_keys(['text', 'doc_id', 'embedding', 'doc_hash', 'extra_info'])

doc_idは以下のように自動的に割り当てられています。

(これらはユーザ側で指定して割り当てることも可能です)

[doc.doc_id for doc in documents]
['d756607b-5d73-49e8-bff7-d6ca3a87a0dd']

indexの作成

indexの作成に、今回はGPTSimpleVectorIndexを使ってみます。

index作成のみの場合、LLMPredictorは与えても与えなくても実行はされません。

GPTSimpleVectorIndexは埋め込みベクトルを作成しますので、埋め込みベクトル用のモデルを定義します。

デフォルトと同じですが、今回は今後のことを考え、text-embedding-ada-002を明示的に与えます。

embed_model = OpenAIEmbedding(model="text-embedding-ada-002")

vector_index = GPTSimpleVectorIndex(
    documents=documents
    , embed_model=embed_model
)

text-embedding-ada-002は現行のモデルの中でベストな選択肢であり、より良く、より安く、よりシンプルに埋め込みベクトルの作成が可能です。

その他、使用可能な埋め込みベクトルのモデル一覧は以下を参照ください。

これでindexが作成されます。

裏側では埋め込みベクトルを作成するためのリクエストが投げられているようです。

作成されたindexはjsonファイルとして保存し、後で読み込むことが可能です。

vector_index.save_to_disk(save_path="vector_index.json")

読み込みは以下のようにします。

vector_index = GPTSimpleVectorIndex.load_from_disk(
    save_path="vector_index.json"
)

indexの構造

少しindexの中身を見てみましょう。

save_to_dictでpythonの辞書型として閲覧することが可能です。

vector_index.save_to_dict().keys()
dict_keys(['index_struct_id', 'docstore', 'vector_store'])

index_struct_idには、index自体のIDが格納されています。

vector_index.save_to_dict()['index_struct_id']
207b17b0-068a-48fe-ba48-61a137c8ae76

docstoreにはテキストデータ、vector_storeには埋め込みベクトルのデータが主に含まれているようです。

docstoreの構造

index構造の中身は以下のようにアクセスできました。

vector_index\
    .save_to_dict()['docstore']['docs']['207b17b0-068a-48fe-ba48-61a137c8ae76'].keys()
dict_keys(['text', 'doc_id', 'embedding', 'doc_hash', 'extra_info', 'nodes_dict', 'id_map', 'embeddings_dict', '__type__'])

index構造にはノードが格納されており、今回のように1つのURLを読み込ませた場合でも複数のノードが格納されています。

ノード構造はnodes_dictで確認できます。

for k, v in vector_index.save_to_dict()['docstore']['docs']\
    ['207b17b0-068a-48fe-ba48-61a137c8ae76']['nodes_dict'].items():
    print(f"{k=}, {v['ref_doc_id']=}, {len(v['text'])=}, {v['text'][:10]=}")
k=3891845498087588076, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=5425, v['text'][:10]='[![Develop'
k=4427056839256078074, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=4619, v['text'][:10]='1時間の通話では、6'
k=5950673443756062847, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=5611, v['text'][:10]='   def cre'
k=5646322192762935901, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=4184, v['text'][:10]='SDK for Py'
k=1251796069225676980, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=5475, v['text'][:10]='Transcribe'
k=7808665043682296837, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=5926, v['text'][:10]=' OpenAIからC'

このように元ドキュメントがref_doc_idに格納され、それを分割したデータがノードとなっていることがわかります。

各ノードの文字数は、ある一定のトークン数になるよう分割された結果ではないかと考えられます。

vector_storeの構造

vector_storeの中にある埋め込みベクトルは、embedding_dictに格納されており、以下のようにアクセスできます。

for k,v in vector_index.save_to_dict()['vector_store']\
    ['simple_vector_store_data_dict']['embedding_dict'].items():
    print(k, len(v))
e505043d-2a7a-45c2-afd5-4012c1f58ae8 1536
14e9edb2-b4ad-42d8-b2e8-a297e79aa1c8 1536
132400d6-3168-4dfd-a28e-4eb6315f49ed 1536
1c264712-e9b7-4398-9c0c-df79411ab648 1536
13554ab6-7d60-4920-9025-4135c77d8936 1536
e0dd1b83-4bcc-457a-8e38-da26e1a90721 1536

1536次元の埋め込みベクトルが分割毎に格納されていることが分かります。

各埋め込みベクトルの文書本体への参照は以下から確認できます。

vector_index.save_to_dict()['vector_store']['simple_vector_store_data_dict']\
    ['text_id_to_doc_id']
{'e505043d-2a7a-45c2-afd5-4012c1f58ae8': 'd756607b-5d73-49e8-bff7-d6ca3a87a0dd',
 '14e9edb2-b4ad-42d8-b2e8-a297e79aa1c8': 'd756607b-5d73-49e8-bff7-d6ca3a87a0dd',
 '132400d6-3168-4dfd-a28e-4eb6315f49ed': 'd756607b-5d73-49e8-bff7-d6ca3a87a0dd',
 '1c264712-e9b7-4398-9c0c-df79411ab648': 'd756607b-5d73-49e8-bff7-d6ca3a87a0dd',
 '13554ab6-7d60-4920-9025-4135c77d8936': 'd756607b-5d73-49e8-bff7-d6ca3a87a0dd',
 'e0dd1b83-4bcc-457a-8e38-da26e1a90721': 'd756607b-5d73-49e8-bff7-d6ca3a87a0dd'}

各ノードへの参照は、docstore内の以下を確認すれば分かりそうです。

vector_index.save_to_dict()['docstore']['docs']\
    ['207b17b0-068a-48fe-ba48-61a137c8ae76']['id_map']
{'e505043d-2a7a-45c2-afd5-4012c1f58ae8': 3891845498087588076,
 '14e9edb2-b4ad-42d8-b2e8-a297e79aa1c8': 4427056839256078074,
 '132400d6-3168-4dfd-a28e-4eb6315f49ed': 5950673443756062847,
 '1c264712-e9b7-4398-9c0c-df79411ab648': 5646322192762935901,
 '13554ab6-7d60-4920-9025-4135c77d8936': 1251796069225676980,
 'e0dd1b83-4bcc-457a-8e38-da26e1a90721': 7808665043682296837}

クエリの実行

クエリをする場合は、LLMPredictorが必要になります。

先ほど指定しなかったため今はデフォルトが割り当てられています。

後から変更する方法がわかりませんでしたので、gpt-3.5-turboを指定して再度インスタンス化します。

llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="gpt-3.5-turbo"))

vector_index = GPTSimpleVectorIndex.load_from_disk(
    save_path="vector_index.json"
    , llm_predictor=llm_predictor
)

ChatGPTと同じモデルを使用するにはこのように、gpt-3.5-turboを指定する必要があります。

デフォルト(未指定)で実行すると、text-davinci-003のモデルが使用されるのでご注意ください。

それではindexにクエリを投げてみます。

Amazon Transcribeについての記事しか格納していませんので、Amazon Transcribeについて聞いてみましょう。

answer = vector_index.query(
    "Amazon Transcribeの料金について箇条書きに要約し、日本語で回答してください。"
)
print(answer)

Amazon Transcribeの料金についての要点は以下の通りです。

  • 課金単位は、音声ファイルの長さ(秒)とトランスクリプトの文字数(1文字あたり0.0004ドル)に基づく。
  • 月間無料枠があり、60分までの音声ファイルと100万文字までのトランスクリプトが無料で利用できる。
  • 月間無料枠を超えた場合、音声ファイルの長さに応じた料金がかかる。
  • リアルタイムストリーミングの場合は、1分あたり0.0045ドルの料金がかかる。
  • PII Redaction機能を使用する場合は、追加料金がかかる。
  • Amazon Transcribe Medicalは、別途料金がかかる。

以上のように、Amazon Transcribeの料金は利用状況に応じて変動する点に注意が必要です。また、PII Redaction機能を使用する場合は、追加料金がかかることも覚えておく必要があります。

きちんと答えてくれました。(あくまで回答例ですので、正確性については1次情報を参照ください)

次に、indexとして与えていないAmazon Translateについて聞いてみましょう。

answer = vector_index.query(
    "Amazon Translateの特徴やメリットについて箇条書きに要約し、日本語で回答してください。"
)
print(answer)

この記事ではAmazon Transcribeについて説明されていますが、Amazon Translateに関する情報は提供されていません。したがって、質問に回答することはできません。ただし、Amazon Transcribeは音声を入力し、書き起こし(Transcribe)するフルマネージドな機械学習サービスであり、使用する前にPricingを確認することが重要です。基本的には、1か月あたりの文字起こしされた音声データの秒数に基づいた従量課金となります。また、PII Redaction機能を追加することにより費用の追加が発生します。

このように、Amazon Translateそのものについての回答は得られませんでした。

indexの更新

Amazon Translateに関する自身の記事を以下のコードでindexに追加してみます。

documents_insert = SimpleWebPageReader(html_to_text=True).load_data([
    "https://dev.classmethod.jp/articles/reintro-managed-ml-translate/"
])

for doc in documents_insert:
    vector_index.insert(doc)

insert後のdocstoreを念のため確認してみます。

for k, v in vector_index.save_to_dict()['docstore']['docs']\
    ['207b17b0-068a-48fe-ba48-61a137c8ae76']['nodes_dict'].items():
    print(f"{k=}, {v['ref_doc_id']=}, {len(v['text'])=}, {v['text'][:10]=}")
k=3891845498087588076, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=5425, v['text'][:10]='[![Develop'
k=4427056839256078074, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=4619, v['text'][:10]='1時間の通話では、6'
k=5950673443756062847, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=5611, v['text'][:10]='   def cre'
k=5646322192762935901, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=4184, v['text'][:10]='SDK for Py'
k=1251796069225676980, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=5475, v['text'][:10]='Transcribe'
k=7808665043682296837, v['ref_doc_id']='d756607b-5d73-49e8-bff7-d6ca3a87a0dd', len(v['text'])=5926, v['text'][:10]=' OpenAIからC'
k=552892730702840963, v['ref_doc_id']='36e443c4-9258-4999-bc87-f35e91565266', len(v['text'])=4962, v['text'][:10]='[![Develop'
k=8750176048377064155, v['ref_doc_id']='36e443c4-9258-4999-bc87-f35e91565266', len(v['text'])=3425, v['text'][:10]='機能概要\n\n### '
k=6357430065525334600, v['ref_doc_id']='36e443c4-9258-4999-bc87-f35e91565266', len(v['text'])=4057, v['text'][:10]='So I purpo'
k=6890481197321989435, v['ref_doc_id']='36e443c4-9258-4999-bc87-f35e91565266', len(v['text'])=3636, v['text'][:10]='カスタム用語の使用\n'
k=8792357583002318384, v['ref_doc_id']='36e443c4-9258-4999-bc87-f35e91565266', len(v['text'])=5034, v['text'][:10]='まとめ\n\nいかがでし'
k=4429791289228322753, v['ref_doc_id']='36e443c4-9258-4999-bc87-f35e91565266', len(v['text'])=5476, v['text'][:10]=' OpenAIのWh'
k=5769781433432053730, v['ref_doc_id']='36e443c4-9258-4999-bc87-f35e91565266', len(v['text'])=1384, v['text'][:10]=' * [Snowfl'

ref_doc_id36e443c4-9258-4999-bc87-f35e91565266になっている部分が新しい文書です。

これで新しい文書がindexに追加されていることが分かりました。

更新後のクエリ実行

再度Amazon Translateについてのクエリを実行してみましょう。

answer = vector_index.query(
    "Amazon Translateの特徴やメリットについて箇条書きに要約し、日本語で回答してください。"
)
print(answer)

Amazon Translateの特徴やメリットについての要約は以下の通りです。

  • 日本語に対応しているが、東京リージョンでは使えない機能があるため注意が必要。
  • 翻訳の精度が高く、多言語に対応している。
  • APIを利用することで、簡単に翻訳機能を実装できる。
  • カスタム辞書や翻訳メモリの利用が可能で、翻訳の品質を向上させることができる。
  • セキュリティが高く、データの暗号化やアクセス制御が行われている。

今度はきちんと更新された知識で回答してくれていることが分かります。

その他のindex更新操作

今回紹介したinsert以外にも、deleteやupdateなどの処理が可能です。

(updateを使うには、doc_idを手動で管理するなどのひと手間が必要です)

詳細は以下の公式情報も参照ください。

まとめ

いかがでしたでしょうか。

LlamaIndexのindex更新について今回は見ていきました。

知識の更新や削除などの操作が可能なことが確認できましたので、今後の活用がより具体的にイメージできるようになったかなと思います。

本記事が、今後LlamaIndexをお使いになられる方の参考になれば幸いです。