工作と競馬2

電子工作、プログラミング、木工といった工作の記録記事、競馬に関する考察記事を掲載するブログ

ローカルLLMでRAG

概要

ローカルPC上で、LLMを動かし、かつ、RAGを実装するための自分用メモ。




背景と目的

ローカルPC上で、LLMを動かす環境がかなり整ってきた。また、RAGの実装方法についても、Web上にたくさんある。そこで、クラウドなどに置きづらい情報に対してLLMで質問応答できる仕組みを自分の手元に試しに構築してみる。



詳細

0. 環境


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

リモート水位センサで使用している筐体はポリカーボネート製のボックスです。

確かに、以下の記事

dekuo-03.hatenablog.jp

に記載のある内容である。本ブログ固有の情報に基づいて、それらしい答えを出してくれている。 とはいうものの、質問によっては的外れの回答を返してしまう。原因としては、LLMのモデルの精度やRAGのベクトルの作り方だろうと思う。今回は、チャンク分割の方法も1つしか試していないし、情報として渡したチャンクが1つだけ。類似度の高いものを複数渡すという手もある。そもそもチャンク分割以外のやり方もある。この辺りはいろいろ試してもう少し使えるものにしていきたい。

なお、Ollama上でLLMが起動している状態(タスクマネージャでGPUのRAMが使用されているような状態)で、RAG用ベクトルが作成済みのものを読み込む場合に、実行時間は2,3秒であった。


5. 参考


まとめと今後の課題

ローカルのLLMで、RAGを実装することができた。実用的な精度を出すためにはいくつも課題があるので、今後もいろいろトライしていきたいと思う。