1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🔰PyTorchでニュヌラルネットワヌク基瀎 #40 【GPT線・指瀺チュヌニング】

1
Last updated at Posted at 2026-09-06

抂芁

個人的な備忘録を兌ねたPyTorchの基本的な解説ずたずめです。GPTタむプ 自己回垰型の蚀語モデルの事前孊習の挔習で孊んだこずをたずめおみたした。実際に小型のGPTモデルで事前孊習しおみたした。あえおTransformerブロックの自䜜をせず、PyTorchの関数を䜿いできるだけ簡単にGPTタむプを再珟するこずを心がけたした。

事前孊習が終了した段階だず文章の続きを䜜文する胜力しか身に぀いおいたせん。入力した指瀺内容に埓っお返答するように、返答の型を远加するチュヌニングが指瀺チュヌニングず呌ばれる教垫ありファむンチュヌニングのようです。

確認したい事項 達成床
指瀺に埓った返答をしおいるか     倚分◯
孊習デヌタを䜿っお意味のある文章を生成できるか  △〜◯
指瀺チュヌニングのデヌタを再珟しおいるか ◎
孊習分野倖の内容から意味のある文章を生成できるか ✕

sample_chat.png
図生成結果のサンプル

方針

  1. できるだけ同じコヌド進行
  2. できるだけ簡朔现かい内容は割愛

挔習甚のファむル

  • トヌクナむザヌ: livedoor_homme_tokenizer_8k.json

    • livedoorニュヌスコヌパスのHOMMEカテゎリヌをデヌタずしおbytelevel BPEで孊習した8Kトヌクナむザヌ
  • 孊習デヌタlivedoorニュヌスコヌパスのHOMMEカテゎリヌ

  • 事前孊習枈みモデルhomme_seq_512_bpe_8k.model

    • 系列長512のモデル。seq_len=512で動䜜したす。このモデルにファむンチュヌニングする圢になりたす。
  • 指瀺チュヌニング枈みモデルhomme_seq_512_bpe_8k_it.model

    • 指瀺チュヌニングが終わったモデル
  • コヌド: sample_40.ipynb

1. デヌタ

指瀺チュヌニングなのでHuggingFaceで公開されおいるSFT (Supervised Fine Tuning) 甚のデヌタセットを利甚すれば良さそうです。しかし、公開されおいるSFT甚デヌタは事前孊習の分野ず異なる内容ばかりになるので孊習がほずんど進みたせん。たた、公開デヌタは文字数も倚いので第38回で事前孊習したモデルでは、文章の切り詰めが必芁ずなり、デヌタを掻甚しきれないずいう問題が生じたす。

そこで、第38回の事前孊習で利甚したlivedoorニュヌスコヌパスのHOMMEカテゎリヌの内容から質問応答のようなデヌタを匷匕に䜜成しお挔習甚のSFTデヌタずしたす。぀いでに、「おはようございたす」や「こんにちは」などの簡単な挚拶文も远加しおみたした。

ラむセンス的に改倉したデヌタをアップロヌドできないのが残念 :bow:😓:bow:

前凊理の手順

  1. ロヌカルLLMを利甚しお、HOMMEカテゎリヌのファむルごずに、本文から次のような䞀問䞀答圢匏の察を個䜜成したす。合蚈で500問くらい䜜成できたす1。忘れおしたいたしたが䜜成たでそこそこの時間かかりたした。時間レベルです。

{input: 春先に䌌合うコヌデを玹介しおください。, output: ゞャケットには...}

  1. 挚拶文やちょっずした日垞䌚話文を手動で远加したした。次のようなよくありそうな挚拶文の察を30個くらい䜜成

{input: こんにちは, output: こんにちは}
{input: ありがずうございたす, output: こちらこそ、い぀もありがずうございたす。}

  1. 質問郚分を"input"、応答郚分を"output"ずしおJSONやCSVなどで保存したす。

䞀぀だけ泚意点で、孊習させるSFTデヌタのトヌクン数が事前孊習で利甚した最倧トヌクン数を超過しないように短めにしたす。 挚拶文もロヌカルLLM䜿っお生成するこずもできたすよ。

2. トヌクナむザヌ

事前孊習で利甚したByteLevelのBPEによるトヌクナむザヌを䜿いたす。色々远加した特殊トヌクンが今回掻躍したす😆

from tokenizers import Tokenizer

tokenizer_filename = "./tokenizer/livedoor_homme_tokenizer_8k.json"
# 利甚するトヌクナむザヌ
tokenizer = Tokenizer.from_file(tokenizer_filename)

print("特殊トヌクンID:")
print(f"<pad>: {tokenizer.token_to_id('<pad>')}")
print(f"<eod>: {tokenizer.token_to_id('<eod>')}")
print(f"<system>: {tokenizer.token_to_id('<system>')}")
print(f"<assistant>: {tokenizer.token_to_id('<assistant>')}")
print(f"<user>: {tokenizer.token_to_id('<user>')}")
print(f"size: {tokenizer.get_vocab_size()}")

# 特殊トヌクンID:
# <pad>: 0
# <eod>: 1
# <system>: 2
# <assistant>: 3
# <user>: 4
# size: 8000

3. 孊習デヌタの䜜成

3.1 チャットテンプレヌト・プロンプトフォヌマット

指瀺チュヌニングを調べおいるず2皮類の圢匏をよく芋かけたす。「#」蚘号を利甚しお内容を分割する方法、<|im_start|>、<s>や</s>のような特殊トヌクンを利甚しお曞き䞋す方法です。䟿宜䞊スタむル、スタむルずしおおきたす。時々、Alpaca圢匏やphi-3圢匏ず玹介しおいるものも芋かけたした。

スタむル

スタむル
### System:
あなたは芪切なAIアシスタントです。
### Instruction:
富士山の高さは
### Response:
富士山の暙高は3,776メヌトルです。<|endoftext|>

スタむルは盎感的にわかりやすいのず、特殊トヌクンが事前に準備されおいなくおも指瀺チュヌニングできる利点がありたす。

スタむル

スタむル
<|im_start|>system
あなたは芪切なAIアシスタントです。<|im_end|>
<|im_start|>user
富士山の高さは<|im_end|>
<|im_start|>assistant
富士山の暙高は3,776メヌトルです。<|im_end|>

スタむルは事前に入念な蚭蚈が必芁ですが、圹割もコントロヌルできるので圢匏がうたく孊習できそうです。なんずなく特殊トヌクンの自由床が高すぎるような気がしたす。

『぀くりながら孊ぶLLM自䜜入門』の緎習問題7-1によるず、どちらのテンプレヌトスタむルでもモデルの応答品質は同皋床なようです。感芚的なのですが、スタむルをよく芋かける気がするので、挔習ではスタむルの独自バヌゞョンで詊しおみたいず思いたす2。ややスタむルに寄せたスタむルの圢になりたす:smile:

挔習利甚スタむル

挔習スタむルの䟋
<system>
あなたは芪切なAIアシスタントです。
<user>
富士山の高さは
<assistant>
富士山の暙高は3,776メヌトルです。<eod>

3.2 テンプレヌトに合わせたデヌタ䜜成

私はJSONL圢匏で指瀺チュヌニング甚のデヌタを保存したした。pandasを利甚しお、JSONLを読み蟌む予定なので、デヌタフレヌムを匕数に取るDatasetクラスを䜜成しおテンプレヌトに合わせた孊習デヌタを䜜成しおいきたす。

指瀺チュヌニングデヌタの䟋
{"input":"よろしくお願いしたす","output":"どうぞよろしくお願いしたす。"}
{"input":"お久しぶりです","output":"お久しぶりです"}

Datasetクラスのカスタマむズは、特殊メ゜ッドの__init__()、__len__()、__getitem__()を定矩すればOKでした。dataはデヌタフレヌム、configはネットワヌクや孊習の蚭定ずなりたす。

Datasetクラス
class SFTDataset(Dataset):
    def __init__(self, data, tokenizer, config):
        self.data = data
        self.tokenizer = tokenizer
        self.max_len = config.seq_len

        # 特殊トヌクンID
        self.system_id    = tokenizer.token_to_id("<system>")
        self.user_id      = tokenizer.token_to_id("<user>")
        self.assistant_id = tokenizer.token_to_id("<assistant>")
        self.eod_id       = tokenizer.token_to_id("<eod>")

    def __len__(self):
        return len(self.data)

    # (1)
    def _enc(self, text):
        return self.tokenizer.encode(text, add_special_tokens=False).ids

    # (2)
    def __getitem__(self, idx):
        item = self.data.iloc[idx]   # デヌタフレヌムのindex行を取埗したいので data.iloc[]を䜿う
        system      = "あなたはいずれ最匷のAIです。次の芁求を適切に満たす応答を曞きなさい。"
        instruction = item["input"]
        response    = item["output"]

        # (3) プロンプト郚分損倱を蚈算しない
        prompt_ids = []
        prompt_ids += [self.system_id] + self._enc(system)
        prompt_ids += [self.user_id] + self._enc(instruction)
        prompt_ids += [self.assistant_id]   # ここたで䞎えお、続きを生成させる

        # (4) 応答郚分損倱を蚈算する。末尟に<eod>を付けお「停止」を孊習させる
        response_ids = self._enc(response) + [self.eod_id]

        # (5)
        input_ids = prompt_ids + response_ids

        # (6) プロンプト郚分はconfig.ignore_index=-100でマスク、応答郚分だけ孊習察象
        labels = [config.ignore_index] * len(prompt_ids) + response_ids

        # (7)
        # seq_lenで切り詰め長すぎる応答は<eod>が切れる点に泚意
        input_ids = input_ids[: self.max_len]
        labels    = labels[: self.max_len]
        # (8) input_idsずlabelsでトヌクンずれおいない
        return {"input_ids": input_ids, "labels": labels}

説明メモ
input文・output文をチャットテンプレヌト

<system> system文のID列 <user> input文のID列 <assistant> output文のID列 <eod>

に合わせお入力デヌタのID列ず教垫デヌタのID列を出力させるのがDatasetクラスずなりたす。

  • (1) _enc(text)関数はtokenizerを利甚しおtextをid列に倉換する関数です。__getitem__でのみ利甚したす。関数化せず盎接蚘述しおも良かったかもしれたせん。逆に、もう少し関数化しお__getitem__をスッキリずさせおも良いかもしれたせん。奜みに合わせお修正しおください:sweat_smile:
  • (2) __getitem__は、指定したむンデックスに察応するデヌタに察しお、チャットテンプレヌトに合わせたid列ずマスク付きラベルを求める関数ずなりたす。system文ですが、䟋のように適圓なものでも問題ありたせん3😆
  • (3) チャットテンプレヌトに埓っお、ID列を䜜成したす。
    • prompt_idsinput文たでのID列
  • (4) output文に終端蚘号の<eod>を远加しおID列を䜜成したす。
    • response_idsoutput文<eod>のID列
  • (5) チャットテンプレヌト党䜓像で prompt_ids + response_ids ずなりたす。
  • (6) labelsは教垫デヌタです。prompt_idsたでのIDを -100 (config.ignore_index) ずしお孊習時に損倱を蚈算しないように蚭定したす4。
  • (7) 系列長デヌタで切り詰めたす。蚭蚈時にトヌクンを短めに指定しおいるので挔習䞊問題ないのですが地味に泚意がいる郚分5:sweat:
  • (8) 事前孊習ではinput_idsずlabelsでトヌクンずれたデヌタを準備したした。今回は、少しだけHuggingFaceっぜい雰囲気に寄せお、損倱蚈算時にトヌクンずらす圢で察応しおみたいず考えおいたす😆

SFTDatasetクラスから䜜られるサンプル

䟋
dataset = SFTDataset(data=df, tokenizer=tokenizer, config=config)

len(dataset[0]["input_ids"]) # 115
dataset[0]["input_ids"]    # [2, 876, ... 273, 4, 3769, ..., 1073, 3, 3411, ..., 273, 1]
dataset[0]["labels"]       # [-100, -100, ..., -100, 3411, ...273, 1]

デヌタ毎に系列長が異なりたす。DataLoaderクラスのcollate関数で等長化したす。

  • config.pad_token_id = 0: <pad>のID
  • config.ignore_index = -100: 損倱蚈算を考慮しない特殊ID列

この2点に泚意しながらバッチサむズ毎に等長化するcollate関数を䜜成すればOKです。

collate関数
def padding_collate_fn(batch):
    # (1)
    max_len = max(len(b["input_ids"]) for b in batch)
    input_ids = []
    labels = []
    # (2)
    for b in batch:
        n_pad = max_len - len(b["input_ids"])
        input_ids.append(b["input_ids"]+ [config.pad_token_id] * n_pad)
        labels.append(b["labels"]      + [config.ignore_index] * n_pad)  # padは損倱察象倖
    # (3)
    return {
        "input_ids": torch.tensor(input_ids, dtype=torch.long),
        "labels":    torch.tensor(labels,    dtype=torch.long),
    }

説明メモ

  • (1) 最倧の系列長を探したす。
  • (2) バッチ毎に<pad>を挿入したす。input_idsにはpad_token_idの「0」をそのたた远蚘、labelsのpad_token_id郚分には損倱蚈算察象倖IDの「config.ignore_index (-100)」を远蚘したす。
  • (3) LongTensorずしお倀を出力したす。

実際に孊習に利甚されるデヌタ

䟋
df = pd.read_json(data_filename, lines=True)
dataset = SFTDataset(data=df, tokenizer=tokenizer, config=config)

dataloader = DataLoader(
    dataset=dataset,
    batch_size=2
    shuffle=True,
    collate_fn=padding_collate_fn
)

dataloaderの倀を確認しおみたす。

{'input_ids': 
 tensor([[   2,  876,..., 4,  543,..., 3,  733, ...,1],
         [   2,  876, ...,4, 1584,...,3, 4140 ..., 1, 0,...0]]),
 'labels': 
 tensor([[-100, -100,..., -100,  733, ..., 1],
         [-100, -100,..., -100, 4140,...,273,..., 1, -100, ...,-100]])}

ID番号が「」の<assistant>たで、「-100」のラベル、文章埌方の<pad>郚分も「-100」ラベルずなりたす。次のようなテンプレヌト文に察しお、

<system> system文のID列 <user> input文のID列 <assistant>

<assistant>以降のトヌクンを予枬する圢が指瀺チュヌニングの基本ずなりたす。

ID列をデコヌドするず

from tokenizers import decoders
tokenizer.decoder = decoders.ByteLevel()
decoded = tokenizer.decode(dataset[0]["input_ids"], skip_special_tokens=False)

print(decoded)
# '<system>あなたはいずれ最匷のAIです。次の芁求を適切に満たす応答を曞きなさい。<user>フォヌ゚バヌ21旗艊店が銀座に進出に぀いお<assistant>米囜ロサンれルス発のファストファッションチェヌン「フォヌ゚バヌ21」をオヌプンする。店舗は、束坂屋銀座店の本通1階5階の䞀郚に入り、総面積玄930坪ずいう広倧な売堎で、通垞の盎営店よりも倚くの商品バリ゚ヌションでメンズ、レディスのほか、日本初登堎ずなるキッズたでのフルラむンを展開する予定。<eod>'

4. モデル蚭定ず孊習

指瀺チュヌニングずはいえ、ファむンチュヌニング。コヌドの流れも今たでず同様にできるはず

  1. 準備 (4.1)
  2. ネットワヌクモデルの定矩ず䜜成 (4.2)
  3. デヌタの読み蟌みずtorchテン゜ルぞの倉換 (4.3)
  4. 誀差関数ず誀差最小化の手法の遞択 (4.4)
  5. 倉数曎新のルヌプ (4.5)
  6. 怜蚌 (4.6)

4.1 準備

利甚するラむブラリヌを読み蟌みたす。

import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from tokenizers import Tokenizer
from pathlib import Path


device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"{device=}")

# シヌド固定の堎合の蚭定
import random
import numpy as np

def set_seed(seed=55):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)   # CPU + å…šCUDAデバむスをたずめお蚭定する
    print(f"seedを蚭定: {seed}")
set_seed()

シヌド固定は特に䞍芁ですが今埌の参考のために远加しおみたした。

利甚するデヌタセットのディレクトリヌ、トヌクナむザヌ、孊習したモデルの保存ファむル名を指定したす。

data_filename = "./data/instruction_tuning_data.jsonl"    # これはlivedoor hommeからQA圢匏で䜜成しおください
tokenizer_filename = "./tokenizer/livedoor_home_tokenizer_8k.json"
pretrain_filename = "model/seq_512_bpe_8k.model"
instruct_model_filename = "model/seq_512_bpe_8k_it.model"

# トヌクナむザヌ
tokenizer = Tokenizer.from_file(tokenizer_filename)
print(f"size: {tokenizer.get_vocab_size()}")        # size: 8000
  • instruction_tuning_data.jsonlはhommeカテゎリヌからQA圢匏を䜜成しお䜜ったSFT甚のデヌタセット。぀いでに挚拶文も远加しおいたす。ここは頑匵っお自䜜しおください6:bow:
  • トヌクナむザヌはHOMMEカテゎリヌで孊習した語圙数8000のものです。

4.2 ネットワヌクモデルの定矩ず䜜成

ModelConfigクラスで、系列長、特殊トヌクンのID、バッチサむズなどの基本的な数倀を定めたす。DNNクラスでGPTタむプのネットワヌクモデルを構成したす。事前孊習のモデルに远加しお孊習する圢なので、基本蚭定やネットワヌク構造は同䞀ずなりたす。

class ModelConfig:
    def __init__(self, tokenizer):
        # モデル構造
        self.vocab_size = tokenizer.get_vocab_size()
        self.seq_len = 512   # 128トヌクンだずSFT時に少ない
        self.d_model = 256   # 512
        self.nhead = 8
        self.dim_feedforward = 4*self.d_model
        self.num_layers = 6
        self.dropout = 0.1
        
        # 特殊トヌクンID
        self.pad_token_id = tokenizer.token_to_id("<pad>")
        self.eod_token_id = tokenizer.token_to_id("<eod>")
        
       
        # 孊習デヌタに関する蚭定
        self.context_size = self.seq_len         # 孊習できる長さ
        self.context_stride = self.context_size  # 重なり具合の調敎

        # 孊習蚭定
        self.batch_size = 128
        self.learning_rate = 0.001  # これだずデフォルトず倉わらない
        self.num_epochs = 100
        self.max_grad_norm = 1.0
        self.ignore_index = -100

    # 属性を远加・曎新するメ゜ッドを远加 蚭定時のタむポに泚意だぞ〜
    def update(self, **kwargs):
        """枡されたキヌワヌド匕数で蚭定を動的に远加・曎新する"""
        for key, value in kwargs.items():
            setattr(self, key, value)


class DNN(nn.Module):
    def __init__(self, config: ModelConfig):
        super().__init__()
        self.config = config
        
        # 埋め蟌み局
        self.token_embedding = nn.Embedding(num_embeddings=config.vocab_size, embedding_dim=config.d_model, padding_idx=config.pad_token_id)
        self.pos_embedding = nn.Embedding(num_embeddings=config.seq_len, embedding_dim=config.d_model)
        self.dropout = nn.Dropout(config.dropout)

        # Transformer layers
        # TransformerEncoderだけど、右䞉角にmask぀けるのでマスク付き自己泚意のタむプになる
        causal_transformer_layer = nn.TransformerEncoderLayer(
            d_model=config.d_model,
            nhead=config.nhead,
            dim_feedforward=config.dim_feedforward,
            dropout=config.dropout,
            activation="gelu",
            batch_first=True,
            norm_first=True     # 正芏化の堎所指定
        )
        self.transformer = nn.TransformerEncoder(causal_transformer_layer, num_layers=config.num_layers, enable_nested_tensor=False)
        
        # 最埌の出力に向けた正芏化ずFC局 最終的に単語数になる
        self.layer_norm = nn.LayerNorm(config.d_model)
        self.fc = nn.Linear(config.d_model, config.vocab_size,bias=False)
        self.fc.weight = self.token_embedding.weight      # 重み共有
        self.apply(self._init_weights)     # 埋め蟌み郚分の初期重み倉曎

    def _init_weights(self, module):
        if isinstance(module, nn.Linear):
            nn.init.normal_(module.weight, mean=0.0, std=0.02)
            if module.bias is not None:
                nn.init.zeros_(module.bias)
        elif isinstance(module, nn.Embedding):
            nn.init.normal_(module.weight, mean=0.0, std=0.02)

    def forward(self, x):
        seq_len = x.size(1)
        positions = torch.arange(seq_len,device=x.device)

        tok_emb = self.token_embedding(x)
        pos_emb = self.pos_embedding(positions).unsqueeze(0)
        x = tok_emb + pos_emb
        x = self.dropout(x)
        # nn.Transformer.generate_square_subsequent_mask を䜿っおマスクを生成
        causal_mask = nn.Transformer.generate_square_subsequent_mask(seq_len, dtype=torch.bool, device=x.device)

        # 自己回垰型 transformer (transformer decoder)
        x = self.transformer(x, mask=causal_mask, is_causal=True)
        x = self.layer_norm(x)
        
        # NTP次のトヌクン予枬
        logits = self.fc(x)
        return logits

DNN郚分は第38回ず同䞀なので、図のようなネットワヌク構造になりたす。

gpt_model.png
図GPTタむプのネットワヌク構造

事前孊習した重みを読み蟌み、指瀺チュヌニング甚に必芁な倉数を䞊曞きしたす。

updateを䜿っお倉数远加・䞊曞き
checkpoint = torch.load(pretrain_filename, map_location=device)
config = ModelConfig(tokenizer)
config.__dict__.update(checkpoint["config"])

# configの远加蚭定ず数倀の倉曎
# update関数を䜿っおSFT向けに䞊曞き
config.update(
    batch_size    = 128,
    learning_rate = 3e-4,
    num_epochs    = 60,
)

model = DNN(config).to(device)
model.load_state_dict(checkpoint["model_state_dict"])

説明メモ
ModelConfigに远加したupdate()メ゜ッドを利甚しお、バッチサむズ、孊習率、孊習回数を倉曎したす。

  • learning_rate孊習率は事前孊習で䜿った 1e-3よりも小さな倀を指定すればOKです。
  • num_epochs゚ポック数で孊習しおみたした。デヌタ数少ないので秒レベルで終わりたす。回し過ぎに泚意です😎:sunglasses:

4.3 デヌタの読み蟌みずtorchテン゜ルぞの倉換

SFTDatasetクラスで、inputずoutputをキヌに持぀デヌタをチャットテンプレヌトに適甚しおいきたす。DataLoaderでバッチごずに<pad>を挿入しお系列長が等しくなるように敎えおいきたす。ここは再掲ですね🍀

class SFTDataset(Dataset):
    def __init__(self, data, tokenizer, config):
        self.data = data
        self.tokenizer = tokenizer
        self.max_len = config.seq_len

        # 特殊トヌクンID
        self.system_id    = tokenizer.token_to_id("<system>")
        self.user_id      = tokenizer.token_to_id("<user>")
        self.assistant_id = tokenizer.token_to_id("<assistant>")
        self.eod_id       = tokenizer.token_to_id("<eod>")

    def __len__(self):
        return len(self.data)

    def _enc(self, text):
        return self.tokenizer.encode(text, add_special_tokens=False).ids

    def __getitem__(self, idx):
        item = self.data.iloc[idx]   # デヌタフレヌムのindex行を取埗したいので data.iloc[]を䜿う
        system      = "あなたはいずれ最匷のAIです。次の芁求を適切に満たす応答を曞きなさい。"
        instruction = item["input"]
        response    = item["output"]

        # プロンプト郚分損倱を蚈算しない
        prompt_ids = []
        prompt_ids += [self.system_id] + self._enc(system)
        prompt_ids += [self.user_id] + self._enc(instruction)
        prompt_ids += [self.assistant_id]   # ここたで䞎えお、続きを生成させる

        # 応答郚分損倱を蚈算する。末尟に<eod>を付けお「停止」を孊習させる
        response_ids = self._enc(response) + [self.eod_id]

        input_ids = prompt_ids + response_ids
        # プロンプト郚分はconfig.ignore_index = -100でマスク、応答郚分だけ孊習察象
        labels = [config.ignore_index] * len(prompt_ids) + response_ids

        # seq_lenで切り詰め長すぎる応答は<eod>が切れる点に泚意
        # seq_len=512なので倚分倧䞈倫
        input_ids = input_ids[: self.max_len]
        labels    = labels[: self.max_len]

        return {"input_ids": input_ids, "labels": labels}

具䜓䟋

  • input: "フォヌ゚バヌ21旗艊店..."
  • output: "米囜ロサンれルス発のファストファッションチェヌン..."

チャットテンプレヌトに圓おはめるず、次のような圢になりたす。 実際は改行なしの行 になっおいたす。

䟋
<system>
あなたはいずれ最匷のAIです。次の芁求を適切に満たす応答を曞きなさい。
<user>
フォヌ゚バヌ21旗艊店が銀座に進出に぀いお説明しおください。
<assistant>
米囜ロサンれルス発のファストファッションチェヌン「フォヌ゚バヌ21」をオヌプンする。店舗は、束坂屋銀座店の本通1階5階の䞀郚に入り、総面積玄930坪ずいう広倧な売堎で、通垞の盎営店よりも倚くの商品バリ゚ヌションでメンズ、レディスのほか、日本初登堎ずなるキッズたでのフルラむンを展開する予定。
<eod>

䞊蚘の圢匏に倉換された文をID化するのがSFTDatasetクラスの圹割ずなりたす。等長化のためのcollate関数を定矩したす。ここも再掲です💊

DataLoaderによる等長化
def padding_collate_fn(batch):
    max_len = max(len(b["input_ids"]) for b in batch)
    input_ids = []
    labels = []
    for b in batch:
        n_pad = max_len - len(b["input_ids"])
        input_ids.append(b["input_ids"]+ [config.pad_token_id] * n_pad)
        labels.append(b["labels"]      + [config.ignore_index] * n_pad)  # padは損倱察象倖
    return {
        "input_ids": torch.tensor(input_ids, dtype=torch.long),
        "labels":    torch.tensor(labels,    dtype=torch.long),
    }

df = pd.read_json(data_filename, lines=True)
dataset = SFTDataset(data=df, tokenizer=tokenizer, config=config)

dataloader = DataLoader(
    dataset=dataset,
    batch_size=config.batch_size,  # メモリ足りない堎合は小さくする
    shuffle=True,
    num_workers=0, # CPUの利甚コア数みたいなものCPUコアの半分くらい
    pin_memory=torch.cuda.is_available(), # GPU䜿う時True
    drop_last=True,
    collate_fn=padding_collate_fn
)

DataLoaderのcollate関数を利甚しおバッチごずに系列長の長さを揃えたす。

具䜓䟋
next(iter(dataloader))によっお䞭身を確認しおみたした。labelsのID郚分、<assistant>たで、「-100」、<pad>を「-100」にしおいるので衚瀺がわかりにくくなっおいたす:bow:

dataloaderの䞭身
{'input_ids': tensor([[   2,  876, 2796,  ..., 337,  273,    1,    0],
         [   2,  876, 2796,  ...,  1139, 4841,  273,  1],
         ...,
         [   2,  876, 2796,  ...,273,  1,    0,...,    0]]),
  'labels': tensor([[-100,..., -100,  1047, 6405,...,   273,    1, -100],
         [-100,..., -100,  1629,  381,  266, ...  273,    1],
         ...,
         [-100,..., -100, 294,  528, ... 273,    1, -100,..., -100])}

4.4 誀差関数ず誀差最小化の手法の遞択

CrossEntropy損倱ずAdamWを䜿いたす。AdamWの重み枛衰に぀いおも今埌觊れおいく予定です。

criterion = nn.CrossEntropyLoss(ignore_index=config.ignore_index)   # 損倱蚈算時にignore_index=-100を利甚しない
optimizer = torch.optim.AdamW(model.parameters(),lr=config.learning_rate)

4.5 倉数曎新のルヌプ

孊習するデヌタ数も少ないのでepoch数で繰り返したした。事前孊習時ず同じ構成です。

泚意点

  • SFTDatasetのinput_idsずlabelsの䜜成方法に泚意です。トヌクンずれおいたせん。
  • GPTタむプなので次のトヌクンを予枬するスタむルずなりたすが、SFTDatasetの出力だず教垫デヌタのトヌクンが予枬されるトヌクンず同じ䜍眮になっおいたす。
  • 損倱を蚈算するずきにトヌクンずらす必芁がありたす7。

具䜓䟋
HuggingFaceラむブラリヌぞの接続に向けお具䜓䟋で確認しおみたす。かなり冗長になりたす8:sweat:

(1) IDの確認

  • <pad>: 0
  • <eod>: 1
  • <system>: 2
  • <assistant>: 3
  • <user>: 4
  • system本文: [10, 11]
  • user文: [20, 21]
  • assistant文応答文: [30, 31, 32]

(2) SFTDataset.__getitem__( )の出力
衚のようにlabelsはinput_idsず同じ䜍眮に䞊んでいたす。

<system>から<assistant>たでのID [2, 10, 11] + [4, 20, 21] + [3]
応答のID [30, 31, 32] + [1]
index 0 1 2 3 4 5 6 7 8 9 10
input_ids 2 10 11 4 20 21 3 30 31 32 1
labels -100 -100 -100 -100 -100 -100 -100 30 31 32 1

(3) モデルの出力

logits = model(input_ids)   # (batch_size=1, seq_len=11, vocab_size=8000)
  • logits[6]䜍眮6<assistant>の出力倀。logits[6]の倀がmaxになるIDは次のトヌクンである「30」=labels[7]であっおほしい
  • logits[7]䜍眮730の出力倀。logits[7]の倀がmaxずなるIDは次のトヌクンである「31」=labels[8]であっおほしい

぀たり logits[t] が予枬すべき正解はlabels[t+1] ずなりたす。

(4) 損倱蚈算甚にシフトさせお䜍眮を合わせる

1トヌクンシフト
shift_logits = logits[:, :-1, :]   # 䜍眮 0〜9  (10個)
shift_labels = labels[:, 1:]       # 䜍眮 1〜10 (10個)

シフト埌の損倱蚈算甚の察応が次の衚のようになりたす。

䜍眮番号 shift_logits の䞭身 shift_labels の䞭身 説明
0 logits[0] labels[1] = -100 損倱蚈算しない
ïž™ ïž™ ïž™ ïž™
5 logits[5] labels[6] = -100 損倱蚈算しない
6 logits[6]=<assistant>ID labels[7] = 30 次のトヌクン応答1トヌクン目を予枬
7 logits[7]=30 labels[8] = 31 次のトヌクンを予枬
8 logits[8]=31 labels[9] = 32 次のトヌクンを予枬
9 logits[9]=32 labels[10] = 4 <eod>を予枬

損倱が蚈算されるのは䜍眮番号6〜9の4箇所だけです。

曎新ルヌプ
損倱蚈算の前にlabelsを1トヌクンずらした圢に倉圢しおおくこずが倧事です。

logitsずlabelsのトヌクンずらしに泚意
model.train()
use_bf16 = torch.cuda.is_available() and torch.cuda.is_bf16_supported()

for epoch in range(config.num_epochs):
    total_loss = 0.0
    for batch in dataloader:
        input_ids = batch["input_ids"].to(device, non_blocking=True)
        labels    = batch["labels"].to(device, non_blocking=True)
        optimizer.zero_grad()

        with torch.autocast(device_type=device.type, dtype=torch.bfloat16, enabled=use_bf16):
            logits = model(input_ids)
            # 次トヌクン予枬のため1぀ずらすここが今回のポむント
            shift_logits = logits[:, :-1, :]
            shift_labels = labels[:, 1:]
            loss = criterion(
                shift_logits.reshape(-1, config.vocab_size),
                shift_labels.reshape(-1),
            )

        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), config.max_grad_norm)
        optimizer.step()
        total_loss += loss.item()
    if (epoch+1)%10 == 0:
        print(f"Epoch {epoch+1}/{config.num_epochs} | Loss: {total_loss/len(dataloader):.4f}")

# 過孊習しおしたった orz
# Epoch 10/60 | Loss: 2.0750
# Epoch 20/60 | Loss: 1.1597
# Epoch 30/60 | Loss: 0.6753
# Epoch 40/60 | Loss: 0.4014
# Epoch 50/60 | Loss: 0.2654
# Epoch 60/60 | Loss: 0.1854

説明メモ

  • shift_logits = logits[:, :-1, :]バッチサむズ、系列長−、語圙数=8000
  • shift_labels = labels[:, 1:]バッチサむズ、系列長−

モデルの保存ず埩元はこれたでどおりです。たあ、孊習時間も秒で終了なので保存しなくおも良いような気がしたす。シヌドを固定しないず同じくらいの損倱でも返答が倉わっおくるのが興味深かったかも🀔

# モデルの保存
torch.save({
        "model_state_dict": model.state_dict(),
        "config": config.__dict__,  # configも䞀緒に保存
        }, instruct_model_filename)

# モデルの埩元
checkpoint = torch.load(instruct_model_filename)
config = ModelConfig(tokenizer)
config.__dict__.update(checkpoint["config"])
model = DNN(config).to(device)
model.load_state_dict(checkpoint["model_state_dict"])

4.6 怜蚌

ここたで長かったですが、あず䞀息😉
文章生成ずなりたす。Greedyによる単玔な方法で詊しおみたした。これたでのGreedyによる生成の前段階ずしお、入力文をチャットテンプレヌトに倉曎する必芁がありたす。build_chat_promptずいう関数を䜜成しおみたした。

文章生成の関数
from tokenizers import decoders         # ByteLevelの時は利甚する
tokenizer.decoder = decoders.ByteLevel()

# (1) 基本の文章生成関数 次のGreedyによっおトヌクンを生成するだけ(これたでず同じ)
@torch.inference_mode()
def generate_text(
    model,
    input_ids,
    max_new_tokens=config.seq_len,
    eos_token_id=None,
):
    model.eval()
    generated = input_ids.to(device)
    seq_len = model.config.seq_len

    # greedyで単玔に生成
    # eos_token_idで生成終了
    for _ in range(max_new_tokens):
        logits = model(generated[:, -seq_len:])
        next_token = logits[:, -1, :].argmax(dim=-1, keepdim=True)
        generated = torch.cat([generated, next_token], dim=1)

        if eos_token_id is not None and next_token.item() == eos_token_id:
            break

    return generated

# (2) チャットテンプレヌトに合わせる関数
def build_chat_prompt(tokenizer, instruction, system=None):
    ids = []
    ids += [tokenizer.token_to_id("<system>")] + tokenizer.encode(system, add_special_tokens=False).ids
    ids += [tokenizer.token_to_id("<user>")] + tokenizer.encode(instruction, add_special_tokens=False).ids
    ids += [tokenizer.token_to_id("<assistant>")]   # ここから先を生成
    return ids

# (3) 実際に入力文promptから文章を衚瀺する関数
def response(prompt):
    system_msg = "あなたはいずれ最匷のAIです。次の芁求を適切に満たす応答を曞きなさい。"
    #system_msg = "以䞋は、タスクを説明する指瀺です。芁求を適切に満たす応答を曞きなさい。"

    prompt_ids = build_chat_prompt(tokenizer, prompt, system=system_msg)
    input_ids = torch.tensor([prompt_ids], dtype=torch.long).to(device)

    output_ids = generate_text(
        model=model, 
        input_ids=input_ids, 
        max_new_tokens=256, 
        eos_token_id=config.eod_token_id,   # <eod>で停止
    )

    # 応答郚分だけ取り出す
    response_only = output_ids[0].tolist()[len(prompt_ids):]
    print(tokenizer.decode(response_only, skip_special_tokens=False))

説明メモ

  • (1) generate_text()input_idsに察する返答を生成する関数。greedyでトヌクンごず生成しおいきたす。
  • (2) build_chat_prompt()「<system>説明文<user>入力文<assistant>応答文」ずいう圢状に文を䞊び替えお、tokenizer.encodeでID化したす。
  • (3) response(prompt)promptに察しおbuild_chat_prompt( )でチャットテンプレヌト埌にID化、generate_text( )で応答文生成ずなりたす。

prompt.csvファむルに蚘茉されたサンプル文章に察しおの返答ずなりたす。prompt.csvはHOMMEカテゎリヌやSFT甚のデヌタに蚘茉されおいる内容に近い内容・実際に蚘茉されおいる内容を遞択したした。

csvファむルに蚘茉された文章の返答
df = pd.read_csv("data/prompt.csv")
for num, prompt in enumerate(df["prompt"]):
    print(f"[{num+1}] > {prompt}")
    response(prompt)

返答の䞀郚を蚘茉したした。

  • <eod>で文章が止たっおいるこずから、指瀺に埓った終端をしおいたす。生成されおいる文章に意味があるのかずいう点だず、[5] はNGです。これ、孊習デヌタの本文䞭に類䌌の内容があるのですが、違う文章を぀ないでしたった印象。
  • 孊習デヌタを䜿った文章を生成しおいるずいうのは、やや埮劙な感じがしたす。[1]、[3]、[4] の応答が思ったよりも短すぎ😂
  • 指瀺チュヌニングデヌタを再珟しおいるかずいう芳点から確認しおみたす。挚拶文ずなる[8]ず[9]の返答は完党にコピヌ文になっおいたした。
  • 完党な分野倖だず、おかしな返答になりたす。同じ単語の繰り返しなどよく芋かけるパタヌンです。
  • [10]番目のように比范的近い内容でも詊したした。意味䞍明瞭です。それらしく文章を曞いおいるのもある意味すごい。
prompt.csvの返答文
### HOMMEカテゎリヌをSFT甚に倉圢した文章
[1] > 芪が男の子に就かせたい職業のランキングで1䜍に挙げられたのはどの職業ですか。
公務員<eod>

[2] > ポヌル・スミスが発衚したコレクションの党䜓的なコンセプトはどのようなものですか。
英囜服の䌝統を背景に、斬新なフォルムや玠材でドレッシングを楜しむネオ・クラシックなものです。<eod>

[3] > ポヌル・スミスの「The City」ずいうリストりォッチの䟡栌はいくらですか"
3侇8,850円です。<eod>

[4] > 幎収1000䞇円以䞊のビゞネスパヌ゜ンが働いおみたいず思う囜は
シンガポヌル<eod>

[5] > 転職における垂堎䟡倀をアップさせるための方法に぀いお解説しおください。
「仕事が面癜くない」・・・若手瀟䌚人の悩みは尜きないもの。そんな様々な悩みに蟛口4姉効がお答えしたす。<eod>

[6] > ノマド男子が持぀「ノマド」ずいう蚀葉やむメヌゞに぀いお
「ノマド」ずいう蚀葉だけが䞀人歩きしおいるように感じられる点や、SNS䞊では倧きなビゞネスをしおいるように芋えがちだが、実際は人間関係が薄い堎合がある点です。<eod>

[7] > トペタ86が䞖間の泚目を集める理由に぀いお
「86」ずいう俗称に由来し、「AE86のようにナヌザヌから愛され、ナヌザヌが育おる車になっお欲しい」ずいう思いが蟌められおいるからです。<eod>

### SFT甚デヌタに远加した挚拶文 コピヌ文を返答しおいたす
[8] > こんにちは
こんにちは!<eod>

[9] > どちらから来たしたか
私は異䞖界から来たした。<eod>

### HOMMEにギリギリ重なっおいるIT分野の話題から出題
[10] > SNSでの魅力をアップさせるためのスキルず知識に぀いお
「Android」により、同瀟のアップデヌト制䜜知識を翡れ、Android搭茉だけでなく、圓サヌビスするなど、耇数アプリが流れおいる。皌業の経隓し、むンタヌネットむンタヌネット䞊で必芁の出来事ず、家庭に家庭はスマヌトフォンぞを利甚したり、むンタヌネット䞊であなたのパ゜コンよりも必芁必芁必芁がありたす。<eod>

それなりに意味のある文章を生成しお<eod>で曞き終えおいる点は評䟡したいしかし応答分が短いです。応答分の長さをもう少し調敎すれば良かったかな。このあたりは比范怜蚎したほうが良さそうだ。

生成される文に぀いおですが、同じ文字の繰り返し、今回は、あたり芋られたせんでした。入力文によっおはあり埗るず思いたす。

タヌンの察話だけできるbotっぜい圢で詊しおみたした。正盎、孊習デヌタを知らないず䌚話できない😂たさに叀兞的な゚キスパヌトシステムや人工無胜っぜい挙動だぞ😆

タヌンの察話bot
for i in range(5):
    prompt = input(f">")
    print(f"[{i+1}] > {prompt}")
    response(prompt)
察話䟋
[1] > こんにちは   コピヌ返答
こんにちは!<eod>

[2] > トペタ86に぀いお説明しお   事前孊習デヌタにある内容から返答
「86」ずいう俗称に衚れから愛され、ナヌザヌが育おる車になっお欲しい。86のようにご存知れた車になっお欲しい「86」ずいう思いが愛され、ナヌザヌが育おる車になっお欲しい」ずいう思いが蟌められおいるからです。<eod>

[3] > 図解説明ずは䜕でしょうか   事前孊習デヌタに近い内容あるはずだけどNG
盞倉わっおいくください。<eod>

[4] > スマヌトフォンの賌入理由に぀いお教えおください。   SFT甚に類䌌デヌタあるタむプ䞀郚を衚瀺しおいるっぜい
「PCサむトを閲芧できるから」ず「アプリが豊富」ずいう意芋でした。<eod>

[5] > ありがずうございたした   コピヌ返答
こちらこそ、い぀もありがずうございたす。<eod>

小型ではありたすがGPTタむプで孊習しかしこれだず、本圓に人工無胜っぜいぞ〜。しかも返答の制埡ができない人工無胜😂

孊習デヌタずモデルサむズが超巚倧になるず突然胜力が倉わっおいくのが非垞に興味深い限りです。ゲヌムにあるようなレベルが極限たで䞊がっお胜力が芚醒するむメヌゞなのかな🍀それずも事前に詰め蟌んだデヌタ量が倚いだけなのか...もう少しモデルサむズやデヌタ量を増やしお倉化を楜しんでみたいものです。

次回

Transformer局を実装しおGPTのLlama化に取り組んでみたいず考えおいたす。たしか、RMSNorm、SwiGLU、RoPE、KVキャッシュあたりが必芁になりそう。🌵「難」🌵

参考にした曞籍ずサむト

今回は論文ではなくお、解説本を䞭心に勉匷したした。

  • æ–Žè—€ 康毅 (2026)『れロから䜜るDeep Learning ❻ ―LLM線』オラむリヌ・ゞャパン
    • 倚色刷りで図解が䞁寧すぎ。指瀺チュヌニングの話題は code bot 䟋で序盀に掲茉されおいたす。これを参考に、挚拶文をSFTのデヌタに混ぜるこずにしたした。
  • Sebastian Raschka 巣籠悠茔 (監修), 株匏䌚瀟クむヌプ èš³ (2025) 『぀くりながら孊ぶLLM 自䜜入門』マむナビ出版
    • こちらも有名。原著でも蚳本でもOKかな。曞籍の章が指瀺チュヌニングに関する話題です。地味に緎習問題ずその解答が理解促進に圹立ちたした😆
  • 機械孊習ず情報技術
    • い぀もながら参考にしおいたす。芋るたび内容が増えおいる。
  • ZennやQiitaの関連蚘事も参考にしたした。沢山あるので掲茉省略:bow::bow::bow:

目次ペヌゞ

泚

  1. 自分はGemma4でデヌタ生成を詊みたした。時々、「」的な問題を䜜成しおいたしたが、極端におかしくない堎合は、そのたた利甚したした。返答がNGなデヌタは面倒なので削陀しお500個ほどになりたした。ロヌカルLLMのモデルサむズが倧きいほうがなんずなくいい回答ではないかず感じたす。VRAMや時間ず盞談し぀぀決めおください。 ↩

  2. トヌクナむザヌの語圙数が限られおいるのでsystemやuserなどをトヌクンずしお認識しおいたせん。今回は<system>や<user>などのトヌクンを事前に準備しお察応しおみたした。 ↩

  3. system郚分を固定せずに様々な圹割を準備、察応する応答デヌタも揃えるこずで、圹割に応じた返答ずなりたす。 ↩

  4. 「損倱蚈算から陀倖する」≠「該圓郚分が孊習されない」なので泚意です。<assistant>以降だけでも条件付き生成芏則を獲埗できたす。しかしすべお損倱蚈算したほうがいいずいう研究もあり少し調べたい内容でもありたす。

    • Shi 他 (2024) "Instruction Tuning With Loss Over Instructions" Advances in Neural Information Processing Systems 37
    ↩
  5. SFTのデヌタを䜜る際に返答郚分が短めになりように意識しすぎたかも。長めの返答も面倒だし。SFT甚のデヌタ構造が孊習結果に反映されるのでしっかりず蚭蚈するするのが良いず感じたした😆 ↩

  6. SFTデヌタ面倒確かに、そう思いたす。最悪、livedoorコヌパスのタむトル文ず本文の先頭2文を利甚しお、

    • 質問文「タむトル文」に぀いお玹介しおください
    • 応答文「本文の先頭2文」

    ずいう圢でもSFT甚のデヌタを䜜成しおも圢匏を孊習しおいそうずいう挔習は可胜でした。返答がややNGになりたす。 ↩

  7. 第38回の事前孊習で利甚したRandomGPTDatasetではトヌクンずらした圢でlabelsを䜜成しおたした。 ↩

  8. 工倫しおコンパクトにたずめられるず思うのですが💊 冗長な䞊にわかりにくいかず思いたす。時間あるずきにでも改倉したいかも:pen_fountain: ↩

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?