自分の過去ツイートでrinna/japanese-gpt-neox-3.6bをfinetuningして「俺tter」を生成する

TL;DR

以下のWindows環境でPEFTを動かすというQiita記事を参考にしただけではあります。

Windows環境でLLMをPEFTでファインチューニングしようとしたとき、ほぼ必ずbitsandbytesというライブラリのエラーに悩まされると思うのですが、こちらの記事ではその対処法が書いてあります。助かりました。

そして、npakaさんの上記の記事を参考に、Google Colabではなくローカルで動かしたという感じです。 

キャラクター性が一貫したLLMを作るための最初のテストに最適

「一貫したキャラ性を持った回答をするAIを作りたい」
「でもライセンスの問題もなくキャラ性を保ったままそれなりの規模があるデータセットなんて無い」
「自分のツイートを使えばいいのでは💡」

そんなことを考えて、自分(@matsu_vr)の過去ツイートで、日本語LLMのrinna/japanese-gpt-neox-3.6bをファインチューニングしたところ、思った以上に「俺っぽい」ツイートを生成することができました。

私の過去ツイートをざっと眺めてみていただければ、かなり「それっぽい」ことが伝わるかもしれません。

キャラクター性をファインチューニングで付加しようとしたとき、自分のツイートというのは権利の問題もないので利用しやすいです。テスト用途としては、自分のツイートは最適かもしれません。このテストの成果を活かして、自分なりのキャラクターを作っていけばいいので。

過去ツイートの取得

Twitterの設定画面から、自分の過去ツイートのアーカイブを取得出来ます。ちなみに、クリックしてすぐに取得出来るわけではなく、1日くらい後に「準備が出来ました」というメールが来て、ダウンロード出来るようになります。待ちましょう。

ダウンロードしたファイルを解凍し、dataフォルダの中にあるtweets.jsファイルがあなたのツイートのテキストデータです。なぜかJavaScriptのファイルなので、最初のwindow.YTD.tweets.part0 =を削除し、ファイル名をtweets.jsonにします。

最初のwindow.YTD.tweets.part0 = を削除。大きいファイルなので、普通のテキストエディタでは編集出来ない可能性もあるので、大規模ファイルに対応したエディタを使いましょう

必要な情報だけを取得

こうして得られたJSONには、ファインチューニングするには余計なデータもたくさんあります。以下の様なスクリプトを書いて、必要な要素だけを取り出しましょう。

import json
import re

# JSONファイルを読み込む
with open('tweets.json', 'r', encoding='utf-8') as f:
    data = json.load(f)

# 出力を保存するリストを作成
output = []

# 各ツイートについて処理を行う
for tweet in data:
    full_text = tweet['tweet']['full_text']

    # 最初の「、」または「。」で区切る
    match = re.search(r'(.*?[、。])(.*)', full_text)

    # 「、」または「。」が見つかった場合
    if match:
        first_sentence = match.group(1)
        remaining_text = match.group(2)

        # 新しいフォーマットに整形
        formatted = {
            "input": first_sentence,
            "completion": remaining_text
        }

        # 結果をリストに追加
        output.append(formatted)

# 結果を新しいJSONファイルに書き込む
with open('formatted_tweets.json', 'w', encoding='utf-8') as f:
    json.dump(output, f, indent=4)

自分のツイートのテキストを最初に出てきた句読点で区切り、前半をinput(instruction)、後半をoutput(completion)として学習データとすることにします。

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

Google Colabには標準で入っているけど、ローカルには入ってないパッケージも入れる必要があります。Windows11+Cuda11.8の環境では以下の様になります。

pip install -Uqq git+https://github.com/huggingface/peft.git
pip install -Uqq transformers datasets accelerate
pip install sentencepiece
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install scipy

つづいて、最初に挙げたQiita記事を参考に、bitsandbytesの0.37.0をインストールします。こちらは、記事を参考にしてください。pipでbitsandbytes==0.37.0をインストールし、venvのファイルが入ったフォルダのLib\site-packages\bitsandbytes\cuda_setup\main.pyを記事のガイドの通りに書き換え、libbitsandbytes_cpu.dllとlibbitsandbytes_cuda116.dllをダウンロードしてきて、Lib\site-packages\bitsandbytes\に入れます。ちょっと手間ですが、書かれたとおりにやれば大丈夫です。

PEFTによる学習の実行

以下の様なスクリプトをfinetune.pyとして作成します。ほぼnpakaさんの記事のまんまです(本当に感謝)。データの取り回しの所だけ、自分のツイートのJSONを読みに行くように変更しています。人に向けたリプライや、RTのデータは除外するようにしています。

# 基本パラメータ

model_name = "rinna/japanese-gpt-neox-3.6b-instruction-ppo"
peft_name = "lorappo-rinna-3.6b"
output_dir = "lorappo-rinna-3.6b-results"

from transformers import AutoTokenizer

# トークナイザーの準備
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)

CUTOFF_LEN = 256  # コンテキスト長

# トークナイズ関数
def tokenize(prompt, tokenizer):
    result = tokenizer(
        prompt,
        truncation=True,
        max_length=CUTOFF_LEN,
        padding=False,
    )
    return {
        "input_ids": result["input_ids"],
        "attention_mask": result["attention_mask"],
    }



# データセットをJSONからロード
import json

with open("formatted_tweets.json", "r", encoding='utf-8') as f:
    loaded_data = json.load(f)

data_1 = [item for item in loaded_data if not item['input'].startswith('RT')]
data = [item for item in data_1 if not item['input'].startswith('@')]

print("データ数:", len(data))


# プロンプトテンプレートの準備
def generate_prompt(data_point):
    result = f"""### 指示:
{data_point["input"]}

### 回答:
{data_point["completion"]}
"""
    # 改行→<NL>
    result = result.replace('\n', '<NL>')
    return result


# データセットの準備
VAL_SET_SIZE = 1000

train_dataset = []
val_dataset = []

for i in range(len(data)):
    if i % 5 == 0:
        x = tokenize(generate_prompt(data[i]), tokenizer)
        val_dataset.append(x)
    else:
        x = tokenize(generate_prompt(data[i]), tokenizer)
        train_dataset.append(x)

from transformers import AutoModelForCausalLM

# モデルの準備
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    load_in_8bit=True,
    device_map="auto",
)

from peft import LoraConfig, get_peft_model, prepare_model_for_int8_training, TaskType

# LoRAのパラメータ
lora_config = LoraConfig(
    r= 8,
    lora_alpha=16,
    target_modules=["query_key_value"],
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM
)

# モデルの前処理
model = prepare_model_for_int8_training(model)

# LoRAモデルの準備
model = get_peft_model(model, lora_config)

# 学習可能パラメータの確認
model.print_trainable_parameters()

import transformers
eval_steps = 200
save_steps = 200
logging_steps = 20

# トレーナーの準備
trainer = transformers.Trainer(
    model=model,
    train_dataset=train_dataset,
    eval_dataset=val_dataset,
    args=transformers.TrainingArguments(
        num_train_epochs=3,
        learning_rate=3e-4,
        logging_steps=logging_steps,
        evaluation_strategy="steps",
        save_strategy="steps",
        eval_steps=eval_steps,
        save_steps=save_steps,
        output_dir=output_dir,
        save_total_limit=3,
        push_to_hub=False,
        auto_find_batch_size=True
    ),
    data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),
)

# 学習の実行
model.config.use_cache = False
trainer.train()
model.config.use_cache = True

# LoRAモデルの保存
trainer.model.save_pretrained(peft_name)

書いたら、python finetune.py を実行して、しばらく待ちます。いいGPUを積んでいてもかなり待つと思いますので、エラーが出てないことを確認したら寝て翌朝に確認してもいいでしょう。

生成のテスト

出来上がったらテストしてみましょう。以下の様なスクリプトをinference.py として作成します。こちらもnpakaさんの記事のほぼそのままです。

import torch
from peft import PeftModel, PeftConfig
from transformers import AutoModelForCausalLM, AutoTokenizer

model_name = "rinna/japanese-gpt-neox-3.6b-instruction-ppo"
peft_name = "lorappo-rinna-3.6b"
output_dir = "lorappo-rinna-3.6b-results"

# モデルの準備
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    load_in_8bit=True,
    device_map="auto",
)

# トークナイザーの準備
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)

# LoRAモデルの準備
model = PeftModel.from_pretrained(
    model,
    peft_name,
    # device_map="auto"
)

# 評価モード
model.eval()


# プロンプトテンプレートの準備
def generate_prompt(data_point):
    if data_point["input"]:
        result = f"""### 指示:
{data_point["instruction"]}

### 入力:
{data_point["input"]}

### 回答:
"""
    else:
        result = f"""### 指示:
{data_point["instruction"]}

### 回答:
"""

    # 改行→<NL>
    result = result.replace('\n', '<NL>')
    return result


# テキスト生成関数の定義
def generate(instruction, input=None, maxTokens=256) -> str:
    # 推論
    prompt = generate_prompt({'instruction': instruction, 'input': input})
    input_ids = tokenizer(prompt,
                          return_tensors="pt",
                          truncation=True,
                          add_special_tokens=False).input_ids.cuda()
    outputs = model.generate(
        input_ids=input_ids,
        max_new_tokens=maxTokens,
        do_sample=True,
        temperature=0.7,
        top_p=0.75,
        top_k=40,
        no_repeat_ngram_size=2,
    )
    outputs = outputs[0].tolist()
    # print(tokenizer.decode(outputs))

    # EOSトークンにヒットしたらデコード完了
    if tokenizer.eos_token_id in outputs:
        eos_index = outputs.index(tokenizer.eos_token_id)
        decoded = tokenizer.decode(outputs[:eos_index])

        # レスポンス内容のみ抽出
        sentinel = "### 回答:"
        sentinelLoc = decoded.find(sentinel)
        if sentinelLoc >= 0:
            result = decoded[sentinelLoc + len(sentinel):]
            return result.replace("<NL>", "\n")  # <NL>→改行
        else:
            return 'Warning: Expected prompt template to be emitted.  Ignoring output.'
    else:
        return 'Warning: no <eos> detected ignoring output'


# テキスト生成
print("自然言語処理ってさ、\n{0}".format(generate('自然言語処理ってさ、')))
print("台風近づいてるなぁ。\n{0}".format(generate('台風近づいてるなぁ。')))

python inference.py と実行すると、生成結果が得られるはずです。

自然言語処理ってさ、

「日本語を正しく理解する」ってところから始めるから、日本語の文章が正しくても、文脈や話し手の意図が伝わらないと意味が通じない。文脈が伝わるような文脈を生成できるような技術が生まれないかなー、とか思ってみたり

台風近づいてるなぁ。

仕事休みになるのかな...? https://t.co/GVPcnJhQs7</tmLt>

私のTwitterを見たことがない人にはわからないかもしれないですが、書いた本人から見るとかなり自分っぽいです。自分のツイートと混ぜられて出されたらどれがAIによるものかわからなくなりそうなくらいです。

ちなみに、付いてきたt.coのURLにアクセスしてみてもとくに何もありません。架空のユーザーにメンションしていたりとか、なかなか面白い結果が出てくると思います。

ちなみに、私の4800位の数のツイートを使って、VRAM使用量は13GBくらいでした。次はQLoRAを使って、より少ないVRAM容量での学習を試してみたいと思っています。

いいなと思ったら応援しよう!