概要
ローカルPC上で、LLMを動かし、かつ、RAGを実装するための自分用メモ。
背景と目的
ローカルPC上で、LLMを動かす環境がかなり整ってきた。また、RAGの実装方法についても、Web上にたくさんある。そこで、クラウドなどに置きづらい情報に対してLLMで質問応答できる仕組みを自分の手元に試しに構築してみる。
詳細
0. 環境
- OS: Windows 11 および WSL Linux Ubuntu 22.04
- CPU: Intel Core i5-14400F
- RAM: 16GB
- GPU: NVIDIA GeForce RTX 4060(RAM 8GB)
- Python 3.10
1. ローカルLLM実行環境(Ollama)
ローカルLLMを実行するにあたり、様々な環境が存在する。いくつか試したものだと、
- LM Studio
- Ollama
などがあるが、このうち自分の環境ではOllamaがよかったのでOllamaを使う。
Ollamaは、WebサイトからWindows用をダウンロードした。
2. LLMモデル
LLMのモデルは、今回の環境で無理なく動くものとして
- llama 3.2
を選択した。コマンドラインで、以下を入力。
ollama run llama3.2
初回は、モデルデータのダウンロードがあるため起動まで時間がかかる。起動後、以下のプロンプトが表示されればOK。
>>> Send a message (/? for help)
3. RAGの実装
3.1 embedding modelのインストール
まず、テキストのベクトル化(エンベッディング)をする必要があるので、ベクトル化するためのembedding modelをインストールする。Ollamaのembedding modelの説明ページを見ると3つが用意されている。ここでは、最もパラメータサイズが多い
- mxbai-embed-large
を選択した。
ollama pull mxbai-embed-large
3.2 実装
Pythonのライブラリをインストール。
pip install ollama scikit-learn
スクリプト冒頭でインポートする。
import ollama from sklearn.metrics.pairwise import cosine_similarity
3.2.1 全体構成
ソースコードは、参考サイト1やその他を参考に、以下の流れとなる。
RAG用データベースのもとになる文書を読み取る ↓ 文書をチャンク分割する ↓ チャンク分割した文書の埋め込みベクトルを取得 ↓ 質問文を取得 ↓ 質問の埋め込みベクトルを取得 ↓ 質問とのコサイン類似度が最も高い文書ベクトルを抽出 ↓ LLMに質問を投げかけ回答文を生成
3.2.2 ベクトル化
以下の関数を定義する。
def vectorize_text(text): response = ollama.embeddings( model="mxbai-embed-large", prompt=text, ) return response["embedding"]
3.2.3 文書をチャンク分割する
以下の関数を定義する。 chunk_sizeやoverlapは、分割するサイズと前後のチャンクとの重複の量である。前後のチャンクとある程度重複させることで、1つ前のチャンクの末尾で切れてしまった部分を次のチャンクでは1つにつながった文としてベクトル化できる。
def split_text(source_text, chunk_size=400, overlap=50): chunks = [] start = 0 while start + chunk_size <= len(source_text): chunks.append(source_text[start : start + chunk_size]) start += chunk_size - overlap if start < len(source_text): chunks.append(source_text[-chunk_size:]) return chunks
3.2.3 質問とのコサイン類似度が最も高い文書ベクトルを抽出
以下の関数を定義する。
def find_most_similar(question, vectors, documents): # コサイン類似度が最も高い回答を取得 max_similarity = 0 most_similar_index = 0 for index, vector in enumerate(vectors): similarity = cosine_similarity([question], [vector])[0][0] # print(f"コサイン類似度: {similarity.round(4)}:{documents[index]}") # 取り出したコサイン類似度が最大のものを保存 if similarity > max_similarity: max_similarity = similarity most_similar_index = index return most_similar_index, max_similarity
3.2.4 全体
上記の関数を用いて、以下のように全体の流れを実装。なお、埋め込みベクトルを作成する際には時間がかかるので、一度作成したベクトルを何らか保存しておいて、それをロードするほうが、何度も質問を変えて試す場合には素早く実行できてよい。
if __name__ == "__main__": # RAG用データベースのもとになる文書 with open("source.txt", "r", encoding="utf-8") as f: source_text = f.read() source_text = source_text.replace("\n", "") # 文書をチャンク分割 documents = split_text(source_text) # 文書の埋め込みベクトルを取得 vectors = [vectorize_text(doc) for doc in documents] # 質問文 question = "{質問}" # 質問の埋め込みベクトルを取得 question_vector = vectorize_text(question) # 質問とのコサイン類似度が最も高い文書ベクトルを抽出 most_similar_index, max_similarity = find_most_similar(question_vector, vectors, documents) print(f"\n質問: {question}\n回答: {documents[most_similar_index]}\nコサイン類似度: {max_similarity}\n") # LLMに質問 ask_question(question, documents[most_similar_index])
4. 動作確認
動作確認として、以下の条件で動かしてみた。
- RAGデータベースの基: 本ブログの全記事をテキスト出力した文書
- 質問: 「リモート水位センサで使用している筐体を教えてください。」
回答は以下になった。
質問: リモート水位センサで使用している筐体を教えてください。 回答: 理</li></ul><p>設置環境がリモート水位センサと同様なので、要求されるものも同じである。</p><p><br></p><h2>2. 材料</h2><p>筐体には、リモート水位センサで実績のある<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DD%A5%EA%A5%AB%A1%BC%A5%DC%A5%CD%A1%BC%A5%C8">ポリカーボネート</a>製のボックス(ホームセンタービバホームで扱っている)のサイズ違いを使用。ボックス自体が透明なので、ボックス内にカメラレンズを構えても、ボックスを通して外が撮影可能だ。なので穴あけも必要ない。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak コサイン類似度: 0.907289728364207 リモート水位センサで使用している筐体はポリカーボネート製のボックスです。
確かに、以下の記事
に記載のある内容である。本ブログ固有の情報に基づいて、それらしい答えを出してくれている。 とはいうものの、質問によっては的外れの回答を返してしまう。原因としては、LLMのモデルの精度やRAGのベクトルの作り方だろうと思う。今回は、チャンク分割の方法も1つしか試していないし、情報として渡したチャンクが1つだけ。類似度の高いものを複数渡すという手もある。そもそもチャンク分割以外のやり方もある。この辺りはいろいろ試してもう少し使えるものにしていきたい。
なお、Ollama上でLLMが起動している状態(タスクマネージャでGPUのRAMが使用されているような状態)で、RAG用ベクトルが作成済みのものを読み込む場合に、実行時間は2,3秒であった。
5. 参考
まとめと今後の課題
ローカルのLLMで、RAGを実装することができた。実用的な精度を出すためにはいくつも課題があるので、今後もいろいろトライしていきたいと思う。