流行りの LLM に関する記事第一弾です。業務で使ったりもしますが、個人的にも勉強して追いついていきたい分野なので色々学んだことをアウトプットして LLM の記事を連載していきたいと思います。
この記事の内容
この記事は Python のライブラリである llama_index
の本当に入門の部分だけをまとめます。具体的には、Retrieval Augmented Generation の考え方と具体的な方針を LlamaIndex のWebページ に則って説明したのち、いくつかの基本的な機能をコードと共に確認します。Embedding など、LLM 関連の基本的な知識を仮定します。
ぶっちゃけた話、この記事に書いてあることは全て LlamaIndex の公式 docs を読んだり、ちょっとググればすぐ出てくるようなことばかりです。ただ、初めて LlamaIndex に触れる方や、LLM のライブラリ操作などに慣れていらっしゃらない方は初めから公式 docs に飛び込んでも情報過多でどこから手をつけていいのかわかりにくいと思います。そのような方に対して最低限これだけは、という情報を示せるように記事を構成するつもりです。
本記事執筆時の Python と llama_index
のバージョンは以下のとおりです。
Python: 3.11.1
llama_index
: 0.10.36
本記事は RAG の5つのステップのうち indexing までをカバーします。Querying 以降は未執筆です(頑張ってすぐに記事化します)。
Retrieval Augmented Generation (RAG)
Retrieval Augmented Generation (RAG) とは、大量の自然言語データで学習済みの LLM に対して、追加の(少量の)データを追加で与えることで特定の用途向けに LLM をカスタマイズする手法です。例えば家電メーカーなどの場合、自社プロダクトの説明書やその他様々な資料を LLM に対して与えることで、プロダクトに関する質問に対し回答するチャットボットを作成することが可能になります。
RAG に関する研究は近年継続的に発表されており、アカデミアの世界でもその有効性が確認されているようです(理論ベースか実験ベースかはあまりよくわかりませんが)123。
LlamaIndex では RAG には5つの段階があるとしています。
- Loading: LLM に追加で与えるデータを読み込む
- Indexing: 読み込んだデータを構造化し、LLM から情報を検索しやすい形にする
- Storing: indexing を経て構造化されたデータを保存する
- Querying: LLM に対して質問などを投げ、回答を得る
- Evaluation: LLM の回答を評価する
以下、順に見ていきましょう。
RAG: 5つのステップ
参考ページはこちら。
llama_index
の内部の動きを見るために、ログの設定を行います。
import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
次に、LLM に与えるデータのソースを用意します。題材はなんでもいいんですが、最近 Whiplash の映画を見直したので J. K. Simmons の Wikipedia をとってくることにします。
import requests
import os
response = requests.get("https://en.wikipedia.org/w/api.php",
params={
"action": "query",
"format": "json",
"titles": "J._K._Simmons",
"prop": "extracts",
"explaintext": True
}).json()
page = next(iter(response["query"]["pages"].values()))
text = page["extract"]
filepath = os.path.join("input/simmons", "simmons.txt")
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w") as f:
f.write(text)
Loading
from llama_index.core import SimpleDirectoryReader
docs = SimpleDirectoryReader(input_files=["input/simmons/simmons.txt"]).load_data()
このようにして、simmons.txt
のデータを読み込むことができます。SimpleDirectoryReader
は最も基本的なローディングメソッドになっており、今回は直接ファイルのパスを指定することでファイルを読み込みましたが、ディレクトリのパスを与えることでファイルを一括で読み込んだり、特定のファイルは読み込みから除外する、特定の拡張子を持つファイルのみを読み込む、などのコントロールをすることができます。
Reader
は他にも様々なものが用意されています。このページには llama_index
に標準で備わった Reader
クラスが列挙されています。WikipediaReader
もあるので先ほどのソースはこちらの Reader
でも用意することができそうですね。
また LlamaHub では拡張 Reader
が用意されており、様々なソースから情報をとってこれるようです。
Transformation
Loading ステップの中には、厳密には load と transformation の二つの段階があります。Transformation とは、ロードされたドキュメントをいくつかの塊に分割する(node に parse する)、embedding を与える、などを意味します。Indexing のための前準備、くらいのイメージかと。
Transformation には低レベル API と 高レベル API を利用することができます。低レベル API では transformation の各ステップに対して細かい設定を与えることができます。一方で、高レベル API はtransformation から indexing までがパッケージ化されており、その中身を細かくいじることはできませんが、llama_index
に設定を任せることができます。
低レベル API の使用例として、ドキュメントのパースを見てみましょう。
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import TokenTextSplitter
pipeline = IngestionPipeline(transformations=[TokenTextSplitter()])
nodes = pipeline.run(documents=docs)
これによって与えられた documents を nodes に分割しています。
ちなみにここでは transformations
引数に与えることができる TextSplitter
も様々なものが用意されています。
-
TokenTextSplitter
: 決まった token 数ごとに split する -
SentenceSplitter
: 文の区切れ目を考慮しつつ split していく
次に紹介する高レベル API でのデフォルトは SentenceSplitter
になっていたと思います。
Indexing
Index とは(私のイメージでは) LLM が情報検索しやすいように構造化された文章オブジェクトの集合です。ここでいう index は日本語で「目次、目録」ぐらいのニュアンスだと思います。Index は代表的なものに以下の4つがあります(こちらで全て紹介されています)。
-
VectorStoreIndex
: 最も基本的な index で、transformation によって得られた node それぞれに対して embedding を与える -
SummaryIndex
: node の集合を、順序を持つものとしてそのまま保持する -
TreeIndex
: node から木を構成する(階層的に構成するとしか書いていないが、意味的な近さを元に構築していると考えられる) -
KeywordTableIndex
: 各 node からキーワードを抽出し、キーワードから node への写像を定義する
以下のように高レベル API を用いて index を作成することができます。例として VectorStoreIndex
を使用していますが、他の index も同様です。
from llama_index.core import VectorStoreIndex
from llama_index.llms.openai import OpenAI
from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.llm = OpenAI(temperature=0, model="gpt-4-0125-preview")
Settings.embed_model = OpenAIEmbedding(model="text-embedding-ada-002")
vector_index = VectorStoreIndex.from_documents(docs)
ちなみに低レベル API を利用して生成した nodes からは以下のように index を構成できます。
vector_index_from_nodes = VectorStoreIndex(nodes)
Querying、といきたいところですが、、、
さて、いよいよ最も重要な部分である querying をみていきたいのですが、ここまでで記事がそこそこ長くなってしまったのと、異なる index の種類間での比較がまだ十分にできていないのでこの記事はここまでにして、次回の記事で querying 周りを詳しく書いていければと思います。J. K. Simmons の出る幕なかった。。。
おわりに
ここまでお読みいただきありがとうございました!この記事の続きはもちろん、他の LLM 関連の話題なども記事にしていければと考えています。是非ともコメントなどお寄せいただけると幸いです。