自分の過去ツイートで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にします。
必要な情報だけを取得
こうして得られた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 と実行すると、生成結果が得られるはずです。
私のTwitterを見たことがない人にはわからないかもしれないですが、書いた本人から見るとかなり自分っぽいです。自分のツイートと混ぜられて出されたらどれがAIによるものかわからなくなりそうなくらいです。
ちなみに、付いてきたt.coのURLにアクセスしてみてもとくに何もありません。架空のユーザーにメンションしていたりとか、なかなか面白い結果が出てくると思います。
ちなみに、私の4800位の数のツイートを使って、VRAM使用量は13GBくらいでした。次はQLoRAを使って、より少ないVRAM容量での学習を試してみたいと思っています。