嘘ばっかり答えるチャットボットをわざわざベクトル検索を使って実装する #役に立たない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形式でダウンロードすることも出来ます。ダウンロード出来るサイトはいくつかありますが、例えば以下。
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を作ってみるのも良さそうです。