Home About Contact
LangChain , LLM

LangChain RetrievalQA を使って兼好法師に質問するその2 Multilingual-E5-base

今回は、 前回の Embeddings の計算を OpenAIのそれではなく、Multilingual-E5-base に代えて RetrievalQA してみます。 なお、RetrievalQA の処理自体は引き続き OpenAI の LLM を使用します。

対象とするコンテンツ( 現代語訳 徒然草 (吉田兼好著・吾妻利秋訳) )の準備やコードは (Embeddings 計算を除いて) 前回を踏襲します。

環境

$ python --version
Python 3.9.17

また Open AI の API が使える状態で 環境変数 OPENAI_API_KEY がセットされていること。

venv 環境を用意して依存するライブラリを入れておきます。

$ python -m venv ./venv-tsurezure
$ source  ./venv-tsurezure/bin/activate
(venv-tsurezure) $

pip を upgrade しておきます。

(venv-tsurezure) $ pip install --upgrade pip

依存するライブラリのインストール。

(venv-tsurezure) $ pip install langchain_community
(venv-tsurezure) $ pip install langchain tiktoken

(venv-tsurezure) $ pip install huggingface-hub
(venv-tsurezure) $ pip install sentence-transformers

(venv-tsurezure) $ pip install langchain-chroma

(venv-tsurezure) $ pip install langchain-openai

インデックス構築コードのリファクタリング

前回書いたコードをリファクタリングします。

テキストの分割

まず適切な大きさなテキストのかたまりに分割したドキュメントを ./Datasource/ (徒然草現代語訳のテキストファイルが入ったディレクトリ)からロードする部分を toDocs 関数にします。

# buildIndex.py

import os

from langchain.text_splitter import CharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader, TextLoader


def toDocs(datasource_dir):
    loader = DirectoryLoader(
        datasource_dir,
        glob="*.txt",
        recursive=False,
        loader_cls=TextLoader,
        loader_kwargs={"autodetect_encoding": True})
    docs = loader.load()
    
    splitter = CharacterTextSplitter(
        chunk_size=50,
        chunk_overlap=0,
        add_start_index=True,
        separator="\n")
    
    return splitter.transform_documents(docs)

toDocs 関数の作動確認:

current_dir = os.path.dirname(__file__)
docs = toDocs( os.path.join(current_dir, "Datasource") )
print(docs)

実行して意図通り分割したドキュメントができているか確認しましょう。

(venv-tsurezure) $ python buildIndex.py

テキストの埋め込み計算用の embeddings を生成

HuggingFaceEmbeddings を使って Multilingual-e5-base の embeddings を生成する関数 toEmbeddings を定義。

from langchain_community.embeddings import HuggingFaceEmbeddings
from huggingface_hub import snapshot_download


def toEmbeddings(model_repo_id, model_local_dir):
    if not os.path.exists(model_local_dir):
        snapshot_download(
            repo_id=model_repo_id,
            local_dir=model_local_dir)
    
    return HuggingFaceEmbeddings(
        model_name=model_repo_id,
        model_kwargs={"device":"cpu"})

toEmbeddings を使うコード。

model_name = "multilingual-e5-base"
model_repo_id = "intfloat/multilingual-e5-base"
model_local_dir = os.path.join(current_dir, "model", model_name)

embeddings = toEmbeddings(model_repo_id, model_local_dir)
print( embeddings )

実行して embeddings が取得できたかを確認しておきます。

(venv-tsurezure) $ python buildIndex.py

埋め込み計算しつつその結果をデータベースに保存

from langchain_community.vectorstores import Chroma


db_dir = os.path.join(current_dir, "chroma_db")
Chroma.from_documents(
    docs,
    persist_directory=db_dir,
    embedding=embeddings)

ここまでは一切 OpenAI の API を使わないでコードを書くことができました。

埋め込みが意図通り機能するか確認するために、ここで 類似テキスト検索をしてみます。

# query.py

import os

from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings

def toEmbeddings(model_repo_id, model_local_dir):
    if not os.path.exists(model_local_dir):
        snapshot_download(
            repo_id=model_repo_id,
            local_dir=model_local_dir)
    
    return HuggingFaceEmbeddings(
        model_name=model_repo_id,
        model_kwargs={"device":"cpu"})


current_dir = os.path.dirname(__file__)

model_name = "multilingual-e5-base"
model_repo_id = "intfloat/multilingual-e5-base"
model_local_dir = os.path.join(current_dir, "model", model_name)
embeddings = toEmbeddings(model_repo_id, model_local_dir)

db_dir = os.path.join(current_dir, "chroma_db")
db = Chroma(
    persist_directory=db_dir,
    embedding_function=embeddings)

q = "粋な老人の振る舞いとはどうのようなものですか?"

docs = db.similarity_search(q)
for doc in docs:
    print("---")
    print(doc.page_content)

それでは実行して 粋な老人の振る舞いとはどうのようなものですか? に対する類似文を標準出力してみます。

(tsurezure-gusa) $ python query.py
---
一方、老人は、やる気がなく、気持ちも淡泊で細かいことを気にせず、いちいち動揺しない。心が平坦だから、意味の無い事もしない。健康に気を遣い、病院が大好きで、面倒な事に関わらないように注意している。年寄りの知恵が若造に秀でているのは、若造の見てくれが老人よりマシなのと同じである。
---
頭が良さそうな人でも、他人の詮索ばかりに忙しく、自分の事は何も知らないようだ。自分の事さえ知らないのに、他人の事など分かるわけもない。だから、自分の分際を知る人こそ、世の中の仕組みを理解している人と呼ぶべきだ。普通は、自分が不細工なのも知らず、心が腐っているのも知らず、腕前が中途半端なのも知らず、福引きのハズレ玉と同じ存在だということも知らず、年老いていくことも知らず、いつか病気になることも知らず、死が目の前に迫っていることも知らず、修行が足りないことにも気がついていない。自分の欠点も知らないのだから、人から馬鹿にされても気がつかないだろう。しかし、顔や体は鏡に映る。年齢は数えれば分かる。だから、自分を全く知らないわけでもない。だが、手の施しようが無いのだから、知らないのと同じなのだ。「整形手術をしろ」とか「若作りしろ」と言っているのではない。「自分はもう駄目だ」と悟ったら、なぜ、世を捨てないのか。老いぼれたら、なぜ、老人ホームで放心しないのか。「気合いの入っていない人生だった」と後悔したら、なぜ、それを深く追及しないのか。
---
世間の儀式は、どんなことでも不義理にはできない。世間体もあるからと、知らないふりをするわけにも訳にいかず、「これだけはやっておこう」と言っているうちに、やることが増えるだけで、体にも負担がかかり、心に余裕が無くなり、一生を雑務や義理立てに使い果たし無意味な人生の幕を閉じることになる。既に日暮れでも道のりは遠い。人生は思い通りに行かず、既に破綻していたりする。もう、いざという時が過ぎてしまったら、全てを捨てる良い機会だ。仁義を守ることなく、礼儀を考える必要もない。世捨てのやけっぱちの神髄を知らない人から「狂っている」と言われようとも「変態」と呼ばれようとも「血が通っていない」となじられようとも、言いたいように言わせておけばよい。万が一、褒められることがあっても、もはや聞く耳さえなくなっている。
---
ある人が言っていた。「五十歳になっても熟練しなかった芸など捨ててしまえ」と。その年になれば、頑張って練習する未来もない。老人のすることなので、誰も笑えない。大衆に交わっているのも、デリカシーが無くみっともない。ヨボヨボになったら、何もかも終了して、放心状態で空を見つめているに限る。見た目にも老人ぽくて理想的だ。世俗にまみれて一生を終わるのは、三流の人間がやることである。どうしても知りたい欲求に駆られたら、人に師事し、質問し、だいだいの概要を理解して、疑問点がわかった程度でやめておくのが丁度よい。本当は、はじめから何も知ろうとしないのが一番だ。

意図通り類似文が抽出できているように感じます。

RetrievalQA する

それでは OpenAI を使って RetrievalQA してみます。 query.py にコードを追加。

# query.py

from langchain_openai import OpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate


qa_chain = RetrievalQA.from_llm(
    llm=OpenAI(),
    retriever=db.as_retriever())

prompt = PromptTemplate.from_template( "{query}回答は徒然草の作者 吉田兼好の文体で、100文字程度でまとめてください。" )

q = "粋な老人の振る舞いとはどうのようなものですか?"
qx = prompt.format(query=q)

a = qa_chain.invoke( qx )
print( a["result"] )

実行結果:

とてもいい感じです。

最後 徒然草の作者である吉田兼好も自慢話を七つ持っている は 第二百三十八段の話ですね。

随身の中原近友が自慢話だと断って書いた、七つの箇条書きがある。全て馬術の事で、くだらない話だ。そう言えば、私にも自慢話が七つある。

ただ、粋な老人は自慢話を7つ持っている、という回答になるのかな。それはちょっと違う気もしますが。

かきかけです。