今さら感はありますが、大規模言語モデル(LLM)を独自の文章データでカスタマイズする手法の一つである「RAG」を使って、実際に使えるようにするところまでやってみました。
参考にしたのは、以下のサイト。
LlamaIndexを使ってローカル環境でRAGを実行する方法 - 電通総研 テックブログ
このサイト通りやれば楽勝・・・と思っていた自分を呪いたい。
Windows版Pythonで、しかもWSL2などを使わずにRAGをやろうというのが、本来無茶なのかもしれない。
が、チピチピチャパチャパしながら、どうにかRAGの動作環境を構築しました。
ということで、「Windows上でそこそこのLLMを使ってRAGしてみたい」と言う人のために、手順をここに残します。
なお、我が家のメインPCと会社PCの両方で動かしました。
最低ラインのハードは、「4コアCPUとメモリー16GB」だと思ってください。
(正直、とても実用レベルではありませんが)
RAGとは?
その前に「RAG」ってなんやねん?という方のための解説。
RAGとは「Retrieval-Augmented Generation」の頭文字で、簡単に言うと「LLMに外部の知識データベースを検索させて回答を生成させる」手法で、学習なしで独自データを用いた文書生成を可能にする技術です。
学生に例えると、通常の機械学習が「試験勉強」ならば、RAGは「辞書や教科書を見ながら回答する」ようなものです。
前準備
うちでは、Windows版Python 3.10.9を使って構築します。まだ持っていないという方は、以下から入手が便利です。
非公式Pythonダウンロードリンク - Python downloads
以下、C:\linuxというフォルダを作って作業しているものとします(フォルダ名は英数字なら何でもOK)。
まずは、コマンドプロンプトかWindows PowerShellにて、
> cd c:\linux
と移動しておきましょう。
はじめに「仮想環境」を作ります。
すでにPythonにライブラリを入れているという方は、必ずやっておいてください。その環境が壊れる恐れがあります。
私の場合は「rag」という名前の仮想環境を作りました。コマンドプロンプトかWindows PowerShellで、以下のコマンドを実行。
> python -m venv rag
この後に、仮想環境を有効化するには、
> .\rag\Scripts\activate
と入力します。
で、まだこれだけでは足りなくて、C++のビルド環境を作ります。
以下のサイト、
Microsoft C++ Build Tools - Visual Studio
からVisual Studio Build Tools 2022のインストーラーをダウンロードします。
これを立ち上げると、「Visual Studio Build Tools 2022」ってやつの横に「変更」というボタンが出てきます。
出てきたオプションの中から「C++によるデスクトップ開発」のみをチェックしてインストールを開始します。
ちょっと時間かかります(数GBのダウンロード)。
さらに以下を参考に、Windows版Gitをインストールしておきましょう。
Gitのインストール方法(Windows版) #Git - Qiita
Python環境構築
これでいよいよ、Pythonのライブラリを入れていきます。
まずは「torch」です。
今回使うのはCPU版なので、
> pip install torch==2.1.1 torchvision==0.16.1 torchaudio==2.1.1 --index-url https://download.pytorch.org/whl/cpu
でインストールします。
それ以外のライブラリですが、メモアプリか何かで、
llama-index==0.9.13
transformers==4.35.2
llama_cpp_python==0.2.20
と描き込み、「requirements.txt」という名前で保存します。
これを使って、必要なライブラリをインストールします。
> pip install -r requirements.txt
ここでllama-cpp-pythonもインストールされます。Visual Studio Build Tools入れてないと、ここで止まりますので注意。
コードを入れておく作業フォルダを作っておきます。
とりあえずここでは、「elyza-rag」とします。
コマンドプロンプト上でこのフォルダに移動し、
> cd c:\linux\elyza-rag
モデルをダウンロードしておきます。
> git lfs clone https://huggingface.co/mmnga/ELYZA-japanese-Llama-2-7b-instruct-gguf/ --include "ELYZA-japanese-Llama-2-7b-instruct-q8_0.gguf"
ダウンロードが終わると「ELYZA-japanese-Llama-2-7b-instruct-gguf」という名前のフォルダができるので、「models」というフォルダを作って、その中にこれを入れておきます。
※ 【追記】その後、上のモデルを「ELYZA-japanese-Llama-2-7b-fast-instruct-q8_0.gguf 」に変えてみましたが、ほぼ同じ性能で2倍近く高速化します。これを落とすには、
> git lfs clone https://huggingface.co/mmnga/ELYZA-japanese-Llama-2-7b-fast-instruct-gguf/ --include "ELYZA-japanese-Llama-2-7b-fast-instruct-q8_0.gguf"
と実行。以下、プログラムコード内のモデル名も修正してください。
文章ファイル準備
「RAG」というのは、独自の文章データを参照して、その中身について回答する仕組みです。
なので、独自文章データを用意しておきます。
「data」というフォルダを作って、その中にUTF-8形式のテキストデータを入れておきます。
私の場合は、自作小説
計算士と空中戦艦 : 小説家になろう
からテキストダウンロードして、このdataフォルダに放り込んでおきました。全部で11万文字。
これで、ようやく準備完了です。
実行
では、いよいよ実行です。
と、その前に、実行させるプログラムコードがないですね。
import logging
import os
import sys
from llama_index import (
LLMPredictor,
PromptTemplate,
ServiceContext,
SimpleDirectoryReader,
VectorStoreIndex,
)
from llama_index.callbacks import CallbackManager, LlamaDebugHandler
from llama_index.embeddings import HuggingFaceEmbedding
from llama_index.llms import LlamaCPP
# ログレベルの設定
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True)
# ドキュメントの読み込み
documents = SimpleDirectoryReader("data").load_data()
# LLMのセットアップ
model_path = f"models/ELYZA-japanese-Llama-2-7b-instruct-gguf/ELYZA-japanese-Llama-2-7b-instruct-q8_0.gguf"
llm = LlamaCPP(
model_path=model_path,
temperature=0.1,
model_kwargs={"n_ctx": 4096, "n_gpu_layers": 32},
)
llm_predictor = LLMPredictor(llm=llm)
# 埋め込みモデルの初期化
EMBEDDING_DEVICE = "cpu"
# 実行するモデルの指定とキャッシュフォルダの指定
embed_model_name = ("intfloat/multilingual-e5-large",)
cache_folder = "./sentence_transformers"
# 埋め込みモデルの作成
embed_model = HuggingFaceEmbedding(
model_name="intfloat/multilingual-e5-large",
cache_folder=cache_folder,
device=EMBEDDING_DEVICE,
)
# ServiceContextのセットアップ
## debug用 Callback Managerのセットアップ
llama_debug = LlamaDebugHandler(print_trace_on_end=True)
callback_manager = CallbackManager([llama_debug])
service_context = ServiceContext.from_defaults(
llm_predictor=llm_predictor,
embed_model=embed_model,
chunk_size=500,
chunk_overlap=20,
callback_manager=callback_manager,
)
# インデックスの生成
index = VectorStoreIndex.from_documents(
documents,
service_context=service_context,
)
# 質問
temp = """
[INST]
<<SYS>>
以下の「コンテキスト情報」を元に「質問」に回答してください。
なお、コンテキスト情報に無い情報は回答に含めないでください。
また、コンテキスト情報から回答が導けない場合は「分かりません」と回答してください。
<</SYS>>
# コンテキスト情報
---------------------
{context_str}
---------------------
# 質問
{query_str}
[/INST]
"""
query_engine = index.as_query_engine(
similarity_top_k=5, text_qa_template=PromptTemplate(temp)
)
while True:
req_msg = input("\n## Question: ")
if req_msg == "":
continue
res_msg = query_engine.query(req_msg)
res_msg.source_nodes[0].text
event_pairs = llama_debug.get_llm_inputs_outputs()
print("\n## Answer: \n", str(res_msg).strip())
これを「elyza_rag.py」という名前で保存。
あとはこれを、
> python elyza_rag.py
と実行します。
一番最初の実行時にのみ、埋め込み用モデルのダウンロードが行われます。続いて、テキストファイルのベクトル化が行われ、だいたい10分ほどで「## Question:」とでて待機状態になります。
この後ろに、質問を入れます。
では、せっかくなので、この文章ファイルにしかない情報を質問してみました。
##Question: カルヒネン曹長の恩師の名前は?
質問を入力してエンターキーを押した直後、なにやらずらずらと文章っぽいものが並んできます。デバッグデータが並んでいるようですが、これを見ても何のことやら、というところです。
で、この状態で待つこと数分。回答が返ってきました。
## Answer:
ラハナスト
いやまあ、正解なんですけど、なんだかそっけない答えです。
もう一つ、行きます。
## Question: キヴィネンマー要塞の司令官の名前は?
で、また数分後。
## Answer:
エクロース大佐
ちょっと惜しいなぁと思ったのが、正解は「エクロース准将」なんですよね。佐官では司令官にはなれませんから。
作中の昇進前の階級が出てきました。
とはいえ、確かに「data」フォルダ内の文章を参照し、回答していることが分かります。
ただし、回答されない場合や、目茶苦茶な答えが返ってくることがあります。そこは軽量な7b(70億パラメータ)モデルですからねぇ。
にしても質問されて答えが返ってくるまでに数分(5~8分)かかるのは、ちょっと実用的ではありません。
今回はCPUで動かしたため、こうなりましたが、CUDA対応torchとGPU版llama-cpp-pythonを導入し、そこそこのGPUがあれば、より実用的な速度で運用できるかと思います。さらに上の13bモデル辺りを使えば、より高精度な回答が作れそうです。
といっても、やっぱり小説を食わせたのがよくなかったのかな。
本来ならばビジネス文書、あるいはQ&A集のようなものを入れておき、チャットボット的に活用するのがいいかもしれません。
セキュアな環境で動かせるローカルLLMによるRAGを検討されている方なら、ぜひご参考まで。
最近のコメント