相づちに特化した軽量なLLMを作ってみる #役に立たないLLM

会話するAIキャラクターを作ろうとすると、返答を生成する待ち時間が気になります。気になるはずです。GPT-4はサーバーが重いときはおよそ会話として成立しないほど待つこともあります。

そこで、軽量なローカルLLMにとりあえず相づちだけ打たせて、その間に、性能の良いLLMにちゃんとした返答を生成させれば良いのでは、なんてことを考えてみました。

データセットとして、以下のRosebleuデータセットを使わせていただきます。

https://open_contents_datasets.gitlab.io/project_home/

このゲームシナリオのデータから、

  • 発話のデータが10文字以下のものを相づちと想定する

  • 10文字以下の発話の直前の発話を問いかけとする

  • 相づちはあまり意味があってもよくないので、名詞と思われるものを含むものは除外する。Mecabは名詞の中に固有名詞も含むので、人名やゲーム固有の名称もある程度除外出来る

  • Mecabでは名詞と判断できなかったものを目で探して、それも除外する処理を入れる

  • 性別や特定の性格が想起される単語も除外する。僕、オレ、貴様、お前、マスターなど

といった条件で、問いかけ+相づちの形のデータセットを作成します。

以下、https://gitlab.com/open_contents_datasets/Rosebleu をダウンロードした時、そのフォルダ以下にある全てのJSONLを一気に処理するスクリプトの例です。ゲームが成人向け作品なので、一括処理をすると成人向けシーンのシナリオも含みます。それが不適切な場合は、当該のファイルを削除してから使うといいと思います。

python script.py "Rosebleuのフォルダ"

のように使います。

import json
import MeCab
import os
import argparse

def process_jsonl_file(file_path, filtered_pairs):
    with open(file_path, "r", encoding="utf-8") as f:
        lines = f.readlines()

    for i in range(len(lines) - 1):
        prev_line = json.loads(lines[i])
        current_line = json.loads(lines[i + 1])

        if len(current_line["utterance"]) <= 10:
            mecab_result = mecab.parse(current_line["utterance"])
            contains_noun = False
            for row in mecab_result.split("\n"):
                elements = row.split("\t")
                if len(elements) >= 2:
                    pos = elements[1].split(",")[0]
                    if pos == "名詞":
                        contains_noun = True
                        break
            if not contains_noun:
                filtered_pairs.append({
                    "input": prev_line["utterance"],
                    "output": current_line["utterance"]
                })

def filter_names(filtered_pairs):
    names = ["大くん", "大地", "大ちゃん", "お前", "貴様", "俺", "僕", "兄", "ボク",
             "必撃", "ぼく", "おれ", "かずくん", "彼女", "新くん", "オレ", "アラタ",
             "マスター", "灯"]

    return [pair for pair in filtered_pairs if all(name not in pair["output"] for name in names)]

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Process JSONL files.')
    parser.add_argument('folder_path', type=str,
                        help='Path to the folder containing JSONL files')

    args = parser.parse_args()
    folder_path = args.folder_path
    mecab = MeCab.Tagger()

    all_filtered_pairs = []

    for root, _, files in os.walk(folder_path):
        for file in files:
            if file.endswith(".jsonl"):
                file_path = os.path.join(root, file)
                process_jsonl_file(file_path, all_filtered_pairs)

    # 名前フィルタリング
    all_filtered_pairs = filter_names(all_filtered_pairs)

    # 一つのJSONLファイルに出力
    with open("all_filtered_pairs_no_names.jsonl", "w", encoding="utf-8") as f:
        for pair in all_filtered_pairs:
            f.write(json.dumps(pair, ensure_ascii=False) + "\n")

うまく行けば、以下の様なJSONL形式のデータセットがずらっと得られるはずです。

{"input": "おかしいなあ。呼び出しくらうようなこと、少なくとも見つからないようにはしてたはずなんだけど", "output": "やってはいるんだな"}
{"input": "で、なんで二人ともついてきてるの?", "output": "面白そう"}
{"input": "お兄ちゃんは何も悪くありません! きっと何かの間違いです!", "output": "ぶっ!"}
{"input": "わ、私、頑張って五歳くらい若返りますっ。お兄ちゃんが望むならっ", "output": "いや、どうやって"}
...

このデータを使って、ファインチューニングを行います。今回は、軽量ながらinstructionチューニングがされているLINEの1.7Bを使わせていただきました。

ファインチューニングの方法は他に色々書かれているので、この記事ではいったん省略します。今回はQLoRAでやりました。このモデルはGPT2なんですが、GPT2のQLoRAはあんまり情報が転がってないので、後で書くかも?

ということで、ファインチューニングしてみた結果が以下です。

なんだか相づちっぽい!!!!

このアプローチはもうちょっと詰めていくと、行けそうな気がします。

1.7Bサイズは4bit量子化したりすれば、Raspberry Pi 4など低処理能力の環境でも動かせる可能性があるので、ロボットに組み込むなどのアプローチも行けるかもしれません。とりあえずローカルで相づちだけ打たせておいて、高度な返答を高性能なLLMに任せる、といった具合です。

宣伝

こうした「別に何の役にも立たないけどちょっと面白いLLM活用術」的な同人誌を作成中です。11月11日から始まる技術書典15に出すつもりで頑張って書いておりますのでよろしくお願いします

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