メインコンテンツへスキップ

自分の過去ツイートで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をファインチューニングしたところ、思った以上に「俺っぽい」ツイートを生成することができました。

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

    というわけで、rinna 3.6bを自分の過去ツイートでfinetuneしたオレッターです。かなり俺っぽい!! pic.twitter.com/3k7CpM5HmL

    — 松xR (@matsu_vr) June 8, 2023

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

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

    過去ツイートの取得

    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容量での学習を試してみたいと思っています。

     
     
    xR技術、VTuber、AI、ライブ配信、遠隔講義、メタバースなどが好きです

    あなたへのおすすめ