嘘ばっかり答えるチャットボットをわざわざベクトル検索を使って実装する #役に立たないLLM

新しいLLMが出る度に話題になるのが、正確な知識を持っているかということです。LLMが嘘をまるで本当かのように話すことはハルシネーション(幻覚)と呼ばれ、課題となっています。

そのハルシネーションに対処する技術として、ベクトル検索を合わせたRAG(Retrieval-Augmented Generation)があります。ざっくり言うと、正確な内容を保持したデータベースを用意し、ユーザーから寄せられた質問に関連する内容をデータベースから引っ張ってきて、その内容をプロンプトに入れることで、LLMが持っていない知識についても正確な回答をさせようというものです。例えば、LLMが知っているはずがない、社内ドキュメントを検索する手法として使われ始めています。

そこで、ふと、思ったんです。データベースに嘘を用意しておいて、それを使ってRAGすれば、嘘を答えるBotが作れるんじゃないかって。

というわけでやってみました。

RAGで嘘をつくには、嘘のデータベースが必要です。今回は、アンサイクロペディアのデータを使いました。嘘や皮肉ばかりの、Wikipediaのパロディーサイトです。

https://ja.uncyclopedia.info/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8

データのライセンスはCC BY-NC-SA 3.0 DEED(表示 - 非営利 - 継承 3.0 非移植)、つまり非商用用途かつ、同じライセンスが継承されるのならば再配布も含めて利用可能です。内容をXML形式でダウンロードすることも出来ます。ダウンロード出来るサイトはいくつかありますが、例えば以下。

https://download.uncyc.org/

ja-wiki.xml.bz2 が日本語のデータです。私がダウンロードしたときはbz2圧縮が壊れていたのですが、復元可能な部分だけを利用しました。

壊れたXMLをパースするのは面倒でしたが、根性でパースします。パース過程は記事にしてもしょうがないので、以下に結果物を置いておきます。ライセンスは同じくCC BY NC SA 3.0とします。

アンサイクロペディアの記事内容を、記事毎に、OpenAIのtext-embedding-ada-002でベクトル化します。text-embedding-ada-002が受け入れられるトークン数は8000くらいなので、ベクトル化する時は、文字数は4096文字以下にカットしました。

ちなみに、text-embedding-ada-002は有料です。OpenAIのサービスの中では安価な部類ですが、使いすぎないようには注意してください。

以下が、ベクトル化したときのコードです。(上記のZIPデータは、すでにtext-embedding-ada-002によるベクトルの値も入っています)

必要なパッケージのインストール

pip install openai

Pythonのコード

import json
import openai

# 環境変数からAPIキーを取得
openai.api_key = "OpenAIのAPIキーを入れてください"

def vectorize_text(input_json, output_json):
    with open(input_json, 'r', encoding='utf-8') as f:
        data = json.load(f)

    total_files = len(data)
    print(f"Total files to process: {total_files}")
    
    for index, item in enumerate(data, start=1):
        text = item['text']
        
        # 4096文字を超えていたらカット
        if len(text) > 4096:
            text = text[:4096]
        
        # 進捗状況を表示
        print(f"Processing file {index} of {total_files}...")
        
        response = openai.Embedding.create(input=text, engine="text-embedding-ada-002")
        item['vector'] = response['data'][0]['embedding']
        
        # 進捗状況を更新
        progress = (index / total_files) * 100
        print(f"Completed: {progress:.2f}%")

    # ベクトルが追加されたJSONデータをファイルに保存
    with open(output_json, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=4)

# 入力JSONファイルと出力JSONファイルのパス
input_json_path = 'Uncyclopedia-ja.json'  # 入力JSONファイルのパス
output_json_path = 'Uncyclopedia-ja-with-vectors-CCBYNCSA30.json'  # 出力JSONファイルのパス

# ベクトル化関数の実行
vectorize_text(input_json_path, output_json_path)

このデータを、ベクトル検索できるように、pineconeにアップロードします。pineconeはベクトル検索に特化したデータベースサービスで、初歩的な機能は無料で使えるので、こういう実験にピッタリです。

pineconeにアップロードするコードは以下です。

必要なパッケージのインストール

pip install pinecone-client

Pythonのコード

import json
import openai
import pinecone

pinecone_api_key = "pineconeのAPIキー"
pinecone_environment = "pineconeのenviroment(地域)"

# indexを作成する
pinecone.create_index(
    name="uncyclopedia", # uncyclopediaという名前のプロジェクトを作る 
    dimension=1536,  # text-embedding-ada-002でベクトル化するときの次元数
    metric="cosine",
    metadata_config={
        "indexed": [
            "id",
            "title",
            "text",
        ]
   }
)
index_name = 'uncyclopedia' # uncyclopediaという名前のプロジェクトを作った場合
pinecone.init(api_key=pinecone_api_key, environment=pinecone_environment)
index = pinecone.Index(index_name)

# Jsonを読み込む
with open('Uncyclopedia-ja-with-vectors-CCBYNCSA30.json', 'r', encoding='utf-8') as f:
    data = json.load(f)

for item in data:
    index.upsert(
        vectors=[
            {
                "id": str(item['id']),
                "values": item['vector'],
                "metadata": {
                    "title": item['title'],
                    "text": item['text'][:10000] # 10000文字以上は切り捨て 
                }
            }
        ]
    )

pineconeはアップロード出来るmetadataのテキストデータの量に上限があるので、10000文字以上は切り捨てています。

これで準備は完了です。早速、嘘を回答させてみましょう。今回は長文のプロンプトが使えることで話題の、CyberAgentのCALM2-7b-chatを使ってみました。

必要なパッケージのインストール

pip install transformers accelerate

Windows環境で動かすには、pytorchやbitsandbytesをインストールする際、以下の様にインストールするようにしてください。

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install bitsandbytes --prefer-binary --extra-index-url=https://jllllll.github.io/bitsandbytes-windows-webui

Linux環境やGoogle Colabでは、普通に以下で大丈夫のはずです。

pip install torch bitsandbytes

Pythonのコード

import openai
import pinecone
import transformers
from transformers import AutoModelForCausalLM, AutoTokenizer, TextStreamer

assert transformers.__version__ >= "4.34.1"

openai.api_key = "OpenAIのAPIキー"
pinecone_api_key = "pineconeのAPIキー"
pinecone_environment = "pineconeのenviroment"
index_name = 'uncyclopedia'
pinecone.init(api_key=pinecone_api_key, environment=pinecone_environment)
index = pinecone.Index(index_name)

search_query = "阪神が優勝した理由を教えてください" # ここを質問に変更します 

response = openai.Embedding.create(input=search_query, engine="text-embedding-ada-002")
query_vector = response['data'][0]['embedding']

answer = index.query(vector=query_vector, top_k=1, include_distance=True)
answer_id = answer['matches'][0]['id']
res = index.fetch([str(answer_id)])

model = AutoModelForCausalLM.from_pretrained("cyberagent/calm2-7b-chat", device_map="auto", torch_dtype="auto", load_in_4bit=True)
tokenizer = AutoTokenizer.from_pretrained("cyberagent/calm2-7b-chat")
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)

prompt = f"""USER: 貴方は常に嘘を答えるジョークBotです。
ユーザーの質問に対して嘘を答えます。
嘘の情報として、以下の内容を参考にしてください。
========
{res['vectors'][str(answer_id)]['metadata']['text']}
========
さて、「{search_query}」という質問に対して、上記の情報を元に、嘘の答えを考えてみましょう。
ASSISTANT: """

token_ids = tokenizer.encode(prompt, return_tensors="pt")
output_ids = model.generate(
    input_ids=token_ids.to(model.device),
    max_new_tokens=300,
    do_sample=True,
    temperature=0.8,
    streamer=streamer,
)

すると、以下の様な答えが返ってきました。

阪神が優勝した理由ですか?それは、「六甲颪」と呼ばれる、チームカラーの「六甲おろし」の風」が、選手たちの体力を大いに鼓舞したからです。また、多くの阪神ファンたちが、地元である阪神エリアの川や海へと飛び込むことで、チームに対する愛情を体全体で表現したことで、チーム自体の士気を大いに高めたことも、優勝への大きな一歩となりました。

上記コードの返答

がはは! 思いっきり嘘ついてる! 楽しい! 文字が一部化けているのはご愛敬だと思っています(いずれ直したい)

ちなみに、基本的なRAGの実装サンプルとしても意外と(?)楽しいと思いますので、あなたならではの嘘Botを作ってみるのも良さそうです。


この記事が気に入ったらサポートをしてみませんか?