小説の続きをAIに予想させる：LoRAファインチューニングで日本語LLMを微調整してみた

※注※タイトル、導入文章、コード、ほとんどはChatGPTに作ってもらっています！！！

「自分の好きな小説をAIに学習させて、その続きや二次創作を書かせたい！」
そんな妄想を叶えたくて、日本語対応のLLMをLoRAでファインチューニングしてみました。
ChatGPTに質問しながら実験した結果と、つまずいたポイントなどを共有します。

LoRA（Low-Rank Adaptation）という軽量なファインチューニング手法を使って、日本語LLaMAモデルを自分の小説テキストで学習させました。LoRAは大きなモデルを全部学習し直すのではなく、一部だけを更新する手法です。

HuggingfaceのTransformersライブラリを使えば、モデルの読み込みや学習が簡単に行えます。


## LoRAファインチューニング
train.py で指定したディレクトリ下のテキストを読み込み学習ファイルを作成
gene.py でtrain.pyで作成した学習ファイルを読み込み必要なプロンプトを設定して文章作成

1,028,096 バイト相当のテキストを学習させてみて
364文字のプロンプトを入力
新規に259文字が返ってきました。
一応それっぽい文章がかえってきましたが、、小説として楽しむには259文字は少なすぎるということです、
低スペックPCで自然な大量のテキスト作る方法はまだ聞いていません（あるかどうか知りませんが）

詰まった部分はモデルのダウンロードですね、事前にダウンロードせずにpythonを実行するとモデルのダウンロードから始めるのですが
それが失敗ばかりして実行できませんでした。

```モデルを事前にダウンロードするコマンド.txt
huggingface-cli download "elyza/ELYZA-japanese-Llama-2-7b-instruct"
```

```train.py
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer, BitsAndBytesConfig
from peft import get_peft_model, LoraConfig, TaskType
from datasets import Dataset
import torch, os

# モデル名（日本語LLaMAベース）
model_id = "elyza/ELYZA-japanese-Llama-2-7b-instruct"

# 量子化（QLoRA）
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4"
)

# トークナイザ・モデル
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=False)
model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb_config, device_map="auto")

# LoRA設定
peft_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    task_type=TaskType.CAUSAL_LM
)
model = get_peft_model(model, peft_config)

# データ読み込み
def load_texts(folder):
    return [{"text": open(os.path.join(folder, f), encoding="utf-8").read() * 3 } for f in sorted(os.listdir(folder)) if f.endswith(".txt")]

data = load_texts("main_data") + load_texts("bonus_data")
dataset = Dataset.from_list(data)

# トークナイズ
def tokenize(example):
    tokens = tokenizer(example["text"], truncation=True, padding="max_length", max_length=2048)
    tokens["labels"] = tokens["input_ids"].copy()
    return tokens

tokenized_dataset = dataset.map(tokenize)

# 学習設定
training_args = TrainingArguments(
    output_dir="./finetuned_model",
    per_device_train_batch_size=1,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    num_train_epochs=3,
    fp16=True,
    save_steps=200,
    save_total_limit=2,
    logging_steps=10,
    report_to="none"
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset
)

trainer.train()
```

```gene.py
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
from peft import PeftModel
import torch

base_model_id = "elyza/ELYZA-japanese-Llama-2-7b-instruct"
peft_model_path = "./finetuned_model/checkpoint-3"  # 最新チェックポイントに変更

tokenizer = AutoTokenizer.from_pretrained(base_model_id)
base_model = AutoModelForCausalLM.from_pretrained(base_model_id, device_map="auto", torch_dtype=torch.float16)
model = PeftModel.from_pretrained(base_model, peft_model_path)

pipe = pipeline("text-generation", model=model, tokenizer=tokenizer, device_map="auto")


prompt = """
(続きを予測したい文章)
"""
output = pipe(prompt, max_new_tokens=300, temperature=0.8, do_sample=True)[0]['generated_text']

print(output)
```