ローカルLLMでメモ検索試してみた
はじめに
巷ではどこもかしもこ生成AIの話題で盛り上がってます。私の部署でもLLMを利用していきたいという話が上がりこれから準備を進めていく流れになってます。
私自身ChatGPTやGithub Copilotを軽く触った程度しかなく踏み込んだ使い方は全然わかっていないので、今回はLLMを用いて社内文章検索(PC内にある自分のメモファイル検索)をやってみようと思います。
目指すシチュエーション
LLMを利用したシステムの題材でよく見かけるのが社内文章検索です。
今回はとりあえずお試しで自分の手元にあるテキスト形式のメモを対象にLLMに問い合わせるシステムを目指してみたいと思います。
RAGを利用して自分のメモの情報をもとにLLMに回答してもらいつつ、元となったデータのソースを表示できる形にしたいと思います。
※RAGについての説明は他のサイト等で良い説明があると思うのでここでは割愛します。
準備
まだ仕事上で使えるChatGPTなりの環境が手元にないので今回はローカルLLMで試してみることにしました。
使用するモデルは日本語に対応したELYZA-japanese-Llama-2-7b-fast-instruct-q4_0.ggufというものを利用します。
LlamaCppで利用するためにggufという形式に変化されたものをDLして利用します。
使用したライブラリ・モデル
LangChain: ドキュメントの分割やクエリ処理を容易にするPythonライブラリ。
Chroma: ベクトル検索エンジン。
HuggingFaceEmbeddings: ドキュメントを埋め込みベクトルに変換するためのライブラリ。
LLamaCpp: ローカルでLLMを動作させるためのライブラリ。CPU環境で動かしたかったため選択。
ELYZA-japanese-Llama-2-7b-fast-instruct: 高性能な日本語LLMモデル。
手順
1. 必要なライブラリのインストール
まず、必要なライブラリをインストールします。
pip install langchain chromadb langchain-community
2. ドキュメントの読み込みと分割
ローカルのメモファイル(今回はObsidian.txt)を読み込み、LangChainを用いて適切なサイズに分割します。
from langchain.text_splitter import CharacterTextSplitter
from langchain.document_loaders import TextLoader
DOC_ROOT = #フォルダを設定
document_path = DOC_ROOT + "Obsidian.txt"
doc = TextLoader(document_path, encoding='UTF-8').load()
text_splitter = CharacterTextSplitter(
separator='\n\n',
chunk_size=1000,
chunk_overlap=50
)
docs = text_splitter.split_documents(documents=doc)
3. ドキュメントの埋め込みと検索エンジンの構築
読み込んだドキュメントを埋め込みベクトルに変換し、Chromaを用いて検索エンジンを構築します。
from langchain.vectorstores import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
STORE_PATH = "./chroma_db"
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/distiluse-base-multilingual-cased-v2"
)
store = Chroma.from_documents(
documents=docs,
embedding=embeddings,
persist_directory=STORE_PATH,
collection_metadata={'hnsw:space': "cosine"}
)
retriever = store.as_retriever(search_kwargs={"k": 1})
4. LLMのロードと質問プロンプトテンプレートの設定
ELYZA-japanese-Llama-2-7b-fast-instructモデルをロードし、質問応答のチェーンを設定します。
from langchain_community.llms import LlamaCpp
from langchain_core.prompts import PromptTemplate
GGUF_MODEL_PATH = "./ELYZA-japanese-Llama-2-7b-fast-instruct-q4_0.gguf"
llm = LlamaCpp(
model_path=GGUF_MODEL_PATH, verbose=False, seed=0, n_gpu_layers=-1
)
req_temp = """
[INST] <<SYS>>あなたは誠実で優秀な日本人のアシスタントです。前提条件の情報だけで回答してください<</SYS>>
前提条件:{context}
質問: {question}
[/INST]"""
prompt = PromptTemplate.from_template(req_temp)
5. 質問応答チェーンの実行
質問とドキュメント検索を組み合わせて、質問応答チェーンを実行します。
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
chain_rag_from_docs = (
RunnablePassthrough.assign(content=(lambda x: format_docs(x["context"])))
| prompt
| llm
)
chain_with_rag = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}).assign(answer=chain_rag_from_docs)
answer = chain_with_rag.invoke("kj法について教えてください。")
6. 結果のフォーマット
最終的な結果を適切な形式で表示します。
resp_temp = PromptTemplate.from_template("""
question:{question}
answer:{answer}
source:{source}
""")
resp = resp_temp.invoke({
"question": answer['question'],
"answer": answer['answer'],
"source": answer['context'][0].metadata['source'],
})
print(resp.text)
7. 実行結果
実行結果の出力として、LLMの答えに加えてソース情報がだせていることが確認できました。
試してみて
今回はローカルLLMを用いて手元にあるメモを基にLLMに回答してもらうスクリプトを書いてみました。
LLMの回答がちょっとイマイチなのはメモ内の情報量が足りないのもあるかもしれません。。。
本来であれば複数のファイルを読み込ませてもっと実用的な感じにしたかったですがLLMの推論にすごい時間がかかってしまうことがわかったので簡単な例にとどまってしまいました。CPU環境で推論させるのは実用的にはちょっと辛そうです(llama-cpp-pythonでかつMACだと遅いみたいな記事も見かけたので私の環境問題である可能性もあります)
今回実際に手を動かしてみてRAGを利用してLLMに対して外部知識を与える流れは掴めたのでよかったです。パラメータの設定やRAGのやりかたなどまだ工夫する余地はありそうなのでもっと色々勉強していきたいと思います。
参考にさせてもらった記事
https://zenn.dev/tsutof/articles/abe58215c2c347
https://qiita.com/yujimats/items/448f105905fee1031fb6