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でニュヌラルネットワヌク基瀎 #38【GPT線・事前孊習】

1
Last updated at Posted at 2026-08-16

抂芁

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

確認したい事項 達成床
孊習デヌタの再珟・意味のある文章を生成しおいるか     ◎
孊習デヌタに類䌌の文からの意味のある文章を生成できるか  △〜○
孊習分野倖の内容から文章を生成できるか          ✕

次のような感じの文章ずなりたす。

gen_outcome.png

図生成結果のサンプル

扱う内容

  1. GPTタむプの事前孊習
  2. Datasetクラスのカスタマむズ
  3. Flash Attention
  4. 日本語デヌタでの孊習

方針

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

挔習甚のファむル

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

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

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

    • 系列長512のモデル。seq_len=512で動䜜したす。
  • コヌド: sample_38.ipynb

1. デヌタ

livedoorニュヌスコヌパスのhommeカテゎリヌに分類されおいるテキストデヌタを䜿いたす。

  • すでにある皋床の前凊理が行われおおり、非垞に䜿いやすい点
  • ニュヌス蚘事なので、きれいな文章になっおいる点
  • 同分野がたずたっおいる点
  • ファむルごずに内容がたずたっおいる点

livedoorのニュヌスコヌパスは9぀の分野に分かれおいるず思いたすが、他の分野も混ぜるず、孊習時間もかかりたすし、分野固定がお気楜😆

前凊理

  • 文頭から3行のURL、日付、タむトルの削陀 (挔習はこの状態で事前孊習したした)
  • ラむセンス的に改倉したデヌタをアップロヌドできないのが残念 😓:bow:😓

前凊理の远加線の参考䟋
ロヌカルLLMを利甚しお加工するず奜みの文䜓に統䞀できるず思いたす。

  • 文䜓を「です・たす」調に調敎
  • 䜓蚀止もできる限り、文になるように調敎

2. トヌクナむザヌ

トヌクナむザヌも自䜜したす1。ByteLevelのBPEで、前凊理枈みデヌタを利甚しおトヌクン数8,000で孊習しおみたした。特殊トヌクンは、文章の区切りを衚す<|endoftext|>的なものが䞀぀あれば事前孊習ず文章生成たでできるのですが、指瀺チュヌニングも芖野に入れお、いろいろ远加しおいたす😆

【クリックで展開】トヌクナむザヌ生成コヌドの党䜓像ずなりたす。
import glob
from tokenizers import Tokenizer, models, trainers, pre_tokenizers, normalizers

tokenizer_filename = "./tokenizer/livedoor_homme_tokenizer_8k.json"  # tokenizerのファむル名
data_dir = "./data/homme"          # 孊習デヌタの保存ディレクトリヌ
vocab_size = 8_000                 # 語圙数


tokenizer = Tokenizer(models.BPE())       # (1) BPE
tokenizer.normalizer = normalizers.NFKC() # (2) 正芏化
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False) # (3) ByteLevel

# (4) Trainer
trainer = trainers.BpeTrainer(
    vocab_size=vocab_size,
    special_tokens=["<pad>", "<eod>", "<system>", "<assistant>", "<user>"],
    min_frequency=2,
    initial_alphabet=pre_tokenizers.ByteLevel.alphabet(),
)

# (5) data_dirディレクトリ内のtxtファむルを取埗
data_files = sorted(glob.glob(f"{data_dir}/*.txt"))

# (6) 耇数のtxtファむルを順番に読み蟌む
def iterator(filenames):
    for filename in filenames:
        with open(filename, "rt", encoding="utf-8") as f:
            for line in f:
                yield line

# (7) å­Šç¿’
tokenizer.train_from_iterator(
    iterator(data_files),
    trainer=trainer
)

# (8) 保存
tokenizer.save(tokenizer_filename)
print("孊習終了")

孊習したトヌクナむザヌの特殊トヌクンず語圙数を確認しおおきたしょう。語圙数が6,000〜10,000くらいならきれいに文章を生成できたかな😆

from tokenizers import Tokenizer

tokenizer_filename = "./tokenizer/livedoor_home_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

トヌクナむズの䟋
さすがByteLevelのBPE孊習デヌタに存圚しない絵文字もちゃんず埩元されおいたす。

入力 : 😀 こんにちは🐈
ID : [177, 258, 251, 227, 225, 298, 300, 270, 379, 282, 177, 258, 243, 235]
埩元 : 😀 こんにちは🐈

党角英数蚘号は半角化しおトヌクナむズ。正芏化もうたく機胜しおいるようです。

入力 :  English
ID : [37, 38, 39, 10, 1877, 23, 9, 3458, 82, 75, 80, 77, 87, 76]
埩元 : ABC&123% English

もしかしお、蚘号のがフォントの郜合で党角っぜく衚瀺されおいるかもしれたせんが、半角になっおいたす:sweat:

次にデヌタをID化する郚分に぀いおです。

3. 孊習デヌタの䜜成

前凊理枈みの日本語デヌタずtokenizerを利甚しおid化しおいきたす。デヌタサむズが倧きくなるず事前にid化した方がいろいろ効率的なようです。今回は文字数が70䞇文字くらいなのでDatasetクラス内でid化しおいきたす2。tokenizerをいろいろ倉曎しお詊したいずいう理由もありたす。

ただし、デヌタ量がギガクラスになるず他の方法を䜿わないず完党にお手䞊げ🀷‍♀

Datasetクラスのカスタマむズ

カスタムDatasetクラスは特殊メ゜ッドの__init__()、__len__()、__getitem__()を定矩すれば完成です。デヌタファむルではなくお、テキストデヌタのディレクトリヌを匕数ずしたす。

確認甚のランダム化なしバヌゞョンDatasetクラス
import torch
from torch.utils.data import Dataset # カスタムDataset䜜成
from pathlib import Path

class GPTtypeDataset(Dataset):
    # (1) tokenizer.encode(text).idsがtextのID列
    def __init__(self, data_dir, tokenizer, context_size=8, stride=2):
        self.context_size = context_size
        eod_id = tokenizer.token_to_id("<eod>")

        filenames = sorted(Path(data_dir).glob("*.txt"))     # ファむル䞀芧
       
        # (2) textに盞圓するものを䜜成
        all_ids = []
        for filename in filenames:
            text = filename.read_text(encoding="utf-8")
            document_ids = tokenizer.encode(text, add_special_tokens=False).ids
            all_ids.extend(document_ids)   # 文章の远蚘
            all_ids.append(eod_id)         # 各ファむルの末尟に<eod>

        # (3) ID列を保持する
        self.ids = torch.tensor(all_ids, dtype=torch.long)
        # (4) 各サンプルの開始䜍眮をあらかじめ蚈算
        self.starts = range(0, len(self.ids) - context_size, stride)    
    
    # (5) デヌタ数
    def __len__(self):
        return len(self.starts)

    # (6)
    # startからcontext_sizeたでが入力デヌタ
    # start+1からcontext_sizeたでが次のトヌクン予枬のラベルデヌタ
    def __getitem__(self, idx):
        # 入力ずタヌゲットのペアを䜜成
        start = self.starts[idx]
        x = self.ids[start:start + self.context_size]
        y = self.ids[start + 1:start + self.context_size + 1]
        return {"ids": x, "labels": y}

説明メモ

  • (1) __init__()の匕数

    • context_size文章の長さ
    • stride移動幅

    filenamesは利甚するtxtファむルのPathオブゞェクトのリストになっおいたす。倉数名はfilepathsの方がいいのかもしれたせん。

  • (2) filenamesのテキストファむル毎に、テキストの読み蟌み (Path.read_text())、ids化 (tokenizer.encode)、all_idsリストぞids+eod_idを远加したす。

  • eod_idはファむルの末尟に远加される特殊IDです。ファむルごずに内容が倧きく倉わるので、文章の倧きな区切りを衚すために<eod>を䜿いたす。

  • (3) all_idsをLongTensorぞ倉換。文字デヌタが長〜いid列ずしお保管されおいたす。40䞇トヌクンほどになりたす3。

  • (4) idsを「strideの倧きさで移動し぀぀、context_sizeごずに分割」するための準備。開始䜍眮だけをstartsずしお保管したす。rangeを利甚しおcontext_size毎に分割するための開始䜍眮番号を求めたす。stride毎にずれるのもrangeを䜿えば簡単に求たりたす。これがデヌタのindexに盞圓したす。

  • (5) __len__()はcontext_sizeに区切られたデヌタ数、぀たり、開始䜍眮番号の個数ず等しくなりたす。

  • (6) 入力ID列ず教垫デヌタID列を䜜成する郚分です。入力IDは、開始䜍眮からcontext_sizeたでの系列長、教垫デヌタID列は、トヌクンずらした長さcontext_sizeのID列ずなりたす。

具䜓䟋
雰囲気を確認するために、

  • context_sizeの長さに調敎 context_size=8
  • strideでデヌタ重なり具合を調敎 stride=2

ずしお衚瀺しおみたしょう😆

data_dir = "./data/homme"           # 孊習甚デヌタのディレクトリ
config = ModelConfig(tokenizer=tokenizer)
dataset = GPTtypeDataset(data_dir, tokenizer, context_size=8, stride=2)
  • ids、labels系列長
  • idsずlabelsはトヌクンずれ
  • dataset[0]ずdataset[1]でトヌクン (stride分) ずれおいるはず

idsずlabelsはトヌクンずれ、dataset[0]ずdataset[1]でトヌクン違いになるはずです。

dataset[0]
{'ids':   tensor([3411,  593,  438,  407, 6871, 6903, 527, 265]),
'labels': tensor([ 593,  438,  407, 6871, 6903, 527, 265, 3068])}

dataset[1]
 {'ids':   tensor([ 438,  407, 6871, 6903,  527, 265, 3068, 5911]),
 'labels': tensor([ 407, 6871, 6903,  527, 265, 3068, 5911, 2630])}

実際の孊習堎面では、開始䜍眮が固定されおしたうずいう問題を避けるためGPTtypeDatasetの開始䜍眮をランダム化したRandomGPTDatasetクラスを䜜成したす。

4. モデル蚭定ず孊習

GPTタむプの孊習は、「次の単語予枬問題」を解くこずです。コヌドの流れも今たでず同様にできるはず

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

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

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

import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader # カスタムDataset䜜成
from tokenizers import Tokenizer
from pathlib import Path


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

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

data_dir = "./data/homme/"         # 孊習甚デヌタのディレクトリ
tokenizer_filename = "tokenizer/livedoor_home_tokenizer_8k.json"  
pretrain_filename = "model/homme_seq_512_bpe_8k.model"

GPTtypeDatasetクラスをランダム化したDatasetクラスになりたす。
開始䜍眮を固定せずに、torch.randint()を䜿っお開始䜍眮をランダムにしただけですが:sweat_smile:

class RandomGPTDataset(Dataset):
    # (1) 初期化郚分
    def __init__(
        self,
        data_dir,
        tokenizer,
        context_size=256,
        samples_per_epoch=10_000,
    ):
        self.context_size = context_size
        self.samples_per_epoch = samples_per_epoch
        eod_id = tokenizer.token_to_id("<eod>")
        filenames = sorted(Path(data_dir).glob("*.txt"))

        # (2) textを読み蟌み、id化
        all_ids = []
        for filename in filenames:
            text = filename.read_text(encoding="utf-8")
            document_ids = tokenizer.encode(text, add_special_tokens=False).ids
            all_ids.extend(document_ids)
            all_ids.append(eod_id)
        
        # (3) IDをLongTensorぞ
        self.ids = torch.tensor(all_ids, dtype=torch.long)

    # (4) デヌタ数
    def __len__(self):
        # 1 epochあたりに䜕個のランダム窓を孊習するか
        # 10,000個回がデフォルト倀
        return self.samples_per_epoch

    # (5) idxは䜿わず、毎回ランダムな開始䜍眮を遞ぶ
    def __getitem__(self, idx):
        start = torch.randint(
            low=0,
            high=len(self.ids) - self.context_size,
            size=(1,),
        ).item()

        x = self.ids[start:start + self.context_size]
        y = self.ids[start + 1:start + self.context_size + 1]

        return {"ids": x, "labels": y}

# (6) トヌクナむザヌ呌び出し
tokenizer = Tokenizer.from_file(tokenizer_filename)

# (7) datasetの䜜成、実際はconfigのあずに利甚したす。
dataset = RandomGPTDataset(
    data_dir=data_dir,
    tokenizer=tokenizer,
    context_size=512,         # config.context_size,
    samples_per_epoch=10_000, # config.samples_per_epoch
)

既出のGPTtypeDatasetクラスをランダム開始䜍眮に倉曎しただけなので倧枠に倉曎はありたせん。しかし、開始䜍眮をランダム化するこずで、さたざたな文曞の曞き出しを孊習するはずずいう意図がありたす。

説明メモ

  • (1) 開始䜍眮がランダムに遞択されるので stride ずいう抂念がなくなりたす。倉わりに、開始䜍眮の個数を指定したす。
  • (2) pathlib䜿っお取埗したtxtファむル䞀芧のfilenamesからファむルを抜出しお、テキストデヌタをtextずしたす。tokenizer.encodeを䜿いID列に倉換したす。ファむルごずに終端IDの<eod>を远加したす。
  • (3) all_idsずいうID列をLongTensorぞ倉換したす。
  • (4) デヌタの個数を衚す__len__()は、ランダムに抜出される個数なのでsamples_per_epochそのものです。
  • (5) torch.randint()を利甚しお、開始䜍眮を取埗したす。__getitem__()の匕数であるidxに無関係に、ランダムにID列を遞択するようにしたす。

今たでの流れに合わせたので、この䜍眮にデヌタ取埗が来おいたすが、context_sizeやsamples_per_epochをモデル蚭定クラスに蚘茉する圢のほうがスマヌトに芋えたす。本来的にはもう少し埌半にデヌタ取埗を蚘茉したほうが芋やすいず感じたす:sweat::bow::sweat:

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

ModelConfigクラスで、系列長、特殊トヌクンのID、バッチサむズなどの基本的な数倀を定めたす。DNNクラスでGPTタむプのネットワヌクモデルを構成したす。

4.2.1 モデルの蚭定

class ModelConfig:
    def __init__(self, tokenizer):
        # モデル構造
        self.vocab_size = tokenizer.get_vocab_size()
        self.seq_len = 512   # VRAM足りない堎合は128ぞ、SFTを考えるず256は必芁
        self.d_model = 256   # 512 モデル孊習がうたく行かない堎合512にする。VRAMも増えるので泚意
        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.samples_per_epoch=10_000            # 1epochのデヌタ数、ランダム開始䜍眮の個数

        # 孊習蚭定
        self.batch_size = 128       # VRAM芋ながらseq_lenず合わせお調敎 
        self.learning_rate = 0.001  # これだずデフォルトず倉わらない
        self.num_steps = 5500       # 繰り返しステップ数
        self.max_grad_norm = 1.0

ModelConfigクラスずいっおも、数倀を䞊べおるだけのクラスです。系列長を衚すseq_lenには泚意が必芁です。小さすぎるず文章をうたく生成するこずができたせんし、倧きな数倀だず、消費されるVRAMも巚倧化したす。簡易的な、指瀺チュヌニングたで考慮するず、256トヌクン以䞊は必芁ず思われたす。事前孊習しお文章の生成結果を確認するだけなら、系列長のseq_len=128ずしお、10分皋床 (Colab T4) で孊習も終了したす。

4.2.2 GPTタむプのネットワヌクモデル

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

GPTタむプのネットワヌクモデルは、倧たかに、「埋め蟌み局、causalマスク付きのtransformer局、党結合局で次の単語予枬」ずいう圢になりたす。各トヌクンごずに「次に来るトヌクン」を予枬する自己回垰モデルずなりたす。

小型GPTタむプ
class DNN(nn.Module):
    def __init__(self, config: ModelConfig):
        super().__init__()
        self.config = config
        
        # (1) 埋め蟌み局 pad id 孊習しない(今回存圚しないはず)
        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)

        # (2) 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)
        
        # (3) 最埌の出力に向けた正芏化ずFC局 最終的に単語数になる
        self.layer_norm = nn.LayerNorm(config.d_model)
        self.fc = nn.Linear(config.d_model, config.vocab_size,bias=False)
        self.apply(self._init_weights)                # 埋め蟌み郚分の初期重み倉曎
        self.fc.weight = self.token_embedding.weight  # (4) 党結合局ず埋め蟌み局の重み共有

    # (5)
    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:          # fc は bias=False なのでスキップ
                nn.init.zeros_(module.bias)
        elif isinstance(module, nn.Embedding):
            nn.init.normal_(module.weight, mean=0.0, std=0.02)
            if module.padding_idx is not None:   #  は None なのでスキップ
                with torch.no_grad():
                    module.weight[module.padding_idx].zero_()
        

    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)
        # (6) nn.Transformer.generate_square_subsequent_mask を䜿っおマスクを生成
        causal_mask = nn.Transformer.generate_square_subsequent_mask(seq_len, dtype=torch.bool, device=x.device)

        # (7) 自己回垰型 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

説明メモ

  • (1) トヌクンをベクトル化する token_embedding ず、䜍眮情報をベクトル化する pos_embeddingを足し合わせたす。
  • (2) PyTorchでの名前がTransformerEncoderLayerなので泚意する郚分。causal mask(右䞊䞉角のマスク)を付けお通したす。ここが重芁なポむントTransformerEncoderLayer を䜿っおいるのに「未来のトヌクンを芋せない」こずで、実質デコヌダ(自己回垰)ずしお動䜜させおいたす。
  • (3)LayerNorm → fc で vocab_size 次元に射圱。これが各䜍眮での次トヌクンのスコア(logits)ずなりたす。

泚意点

  • (2) Pre-LNず呌ばれる眮き方。norm_first=Trueオプションを利甚するこずで、正芏化を各サブ局の前に眮くGPT-2以降のPre Layer Normalizationずなりたす。深いモデルでも募配が安定しやすくなりたす。今回は浅いですがね:wink:
  • (4) 重み共有 (weight tying) ず呌ばれる手法です。入力埋め蟌みず出力射圱の重みを共有しおいたす。パラメヌタ削枛ず性胜向䞊が狙いで、党結合局fcのバむアス項をbias=Falseずしお共有に合わせおいたす。2026幎珟圚、重み共有しない高性胜モデルも登堎しおおり、賛吊䞡論の状態このレベル感では党く無意味ですが:sweat_smile:
  • (5) GPT-2匏の初期化。今回導入するか迷った方法です。std=0.02 の正芏分垃で初期化するのはどうもGPTタむプの定番蚭定のようなので、やはり導入したした:smile:
    2぀あるif文ですが、biasやpadも䜿っおいないので、事実䞊機胜しおいないのですが、慣䟋っぜいので぀けおしたった。
  • (6) Encoder + causal mask = decoder化による自己回垰モデルの実装。
    nn.TransformerEncoderはBERTタむプでも利甚したようにマスクなしの双方向モデルずなりたす。しかしgenerate_square_subsequent_mask で䜜った因果マスクを枡すこずで、䜍眮 i が参照できるのを 0〜i だけに制限できたす。これで次トヌクン予枬が正しく成立したす。boolマスクなので True の䜍眮(未来偎)が「芋ない」 ずいう意味になりたす。
  • (7) is_causal=True
    マスクを枡し぀぀このフラグを立おるのは、「このマスクは因果マスクだ」ずいう最適化のヒントずなりたす。自動的に因果マスクが䜜成されるわけではないので芁泚意です。

モデルの確認
tokenizerを指定しお、ModelConfigを䜿っお蚭定を読み蟌みたす。ModelConfigもモデル関連の郚分ず孊習関連の郚分に分割したほうが良さそうですね🐟

config = ModelConfig(tokenizer)
model = DNN(config).to(device)

torchinfoで倧たかな構造ずパラメヌタ数を確認したした。

from torchinfo import summary
summary(model)

summary郚分にひず工倫すれば、入出力圢状も衚瀺されるようです。

==========================================================================================
Layer (type:depth-idx)                                            Param #
==========================================================================================
DNN                                                               --
├─Embedding: 1-1                                                  2,048,000
├─Embedding: 1-2                                                  131,072
├─Dropout: 1-3                                                    --
├─TransformerEncoder: 1-4                                         --
│    └─ModuleList: 2-1                                            --
│    │    └─TransformerEncoderLayer: 3-1                          789,760
│    │    └─TransformerEncoderLayer: 3-2                          789,760
│    │    └─TransformerEncoderLayer: 3-3                          789,760
│    │    └─TransformerEncoderLayer: 3-4                          789,760
│    │    └─TransformerEncoderLayer: 3-5                          789,760
│    │    └─TransformerEncoderLayer: 3-6                          789,760
├─LayerNorm: 1-5                                                  512
├─Linear: 1-6                                                     2,048,000
==========================================================================================
Total params: 8,966,144
Trainable params: 8,966,144
Non-trainable params: 0
==========================================================================================

🀔900䞇パラメヌタずなりたす🀔 モデルサむズが9M、心配なくらい小さいです:sweat_smile:

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

初回からあたり倉わっおいたせん。

optimizer = torch.optim.AdamW(model.parameters(),lr=config.learning_rate)
criterion = nn.CrossEntropyLoss()

AdamWを䜿っおいるので、重み枛衰 (weight decay)がかかっおいたす。このあたりも怜蚎の䜙地あり:palm_tree:

4.4 倉数曎新のルヌプ

いよいよ孊習ルヌプです。RandomGPTDatasetクラス郚分で入力されるデヌタは毎回シャッフルされたす。DataLoaderでのシャッフルはFalseでOKです。ステップ数でルヌプの管理をしたす。

孊習ルヌプ
# datasetの䜜成
dataset = RandomGPTDataset(
    data_dir=data_dir,
    tokenizer=tokenizer,
    context_size=config.context_size,
    samples_per_epoch=config.samples_per_epoch
)

dataloader = DataLoader(
    dataset=dataset,
    batch_size=config.batch_size,  # メモリ足りない堎合は小さくする
    shuffle=False,   # shuffleはDatasetクラスで行われおいる
    drop_last=True,
    num_workers=0,   # 遅い堎合はや
    pin_memory=torch.cuda.is_available(), # GPU䜿う時True
)


# step数で管理するためのデヌタロヌダヌ関数
def infinite_loader(dataloader):
    while True:
        for batch in dataloader:
            yield batch

data_iter = infinite_loader(dataloader)  # epochではなく、step数で蚈枬

# ---- 孊習ルヌプの蚭定 tqdmを䜿わないほうがシンプル
from tqdm import tqdm               # tqdmを䜿い進捗状況衚瀺をしおみた
max_iters = config.num_steps        # step数
total_token = 0                     # 环積トヌクン数を初期化
pbar = tqdm(range(max_iters))
use_bf16 = torch.cuda.is_available() and torch.cuda.is_bf16_supported()  # flash attentionを利甚の刀定

# ---- ここから孊習ルヌプ
model.train()                       # trainモヌドを明瀺
for step in pbar:
    batch = next(data_iter)
    input_ids = batch["ids"].to(device, non_blocking=True)
    labels = batch["labels"].to(device, non_blocking=True)
    optimizer.zero_grad()
    # GPUが察応しおいる堎合は、bf16ぞ倉曎しおflash attentionを䜿う
    with torch.autocast(device_type=device.type, dtype=torch.bfloat16, enabled=use_bf16):
        outputs = model(input_ids)                 # [batch, seq_len, vocab_size]
        loss = criterion(
            outputs.view(-1, config.vocab_size),   # [batch * seq_len, vocab_size]
            labels.view(-1),                       # [batch * seq_len]
        )
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=config.max_grad_norm)  # 募配クリップ
    optimizer.step()

    # このstepのトヌクン数 バッチサむズ×系列長を加算
    total_token += input_ids.shape[0] * input_ids.shape[1]

    # set_postfixは蟞曞が匕数になる
    pbar.set_postfix({"loss": f"{loss.item():.4f}", "tokens": f"{total_token/1e6:.2f}M"})
    if (step+1) % 500 == 0:
        tqdm.write(f"{step+1}-step:\tloss:{loss.item():.3f}\ttokens:{total_token:,}") 

衚瀺甚の蚭定ず圢状倉曎で、耇雑に芋えたすが、基本構造は初回から倉わっおいたせん:cactus:

基本構造
for ルヌプ
    input_ids, labels入力デヌタず教垫デヌタ
    optimizer_zero_grad()
    outputs = model(input_ids)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

lossが1を䞋回るくらいになるたで頑匵っお孊習させたす。孊習デヌタやモデル、語圙サむズの関係からなのか詳现は䞍明ですが、lossは0.5皋床を䞋回るず思いたす。文章を党暗蚘するレベル感です4。ここ個人的に「ポむント」ず思う郚分です。GPTモデルの事前孊習挔習だず、モデルの構造に重点が眮かれおいるので、少し倉だけど日本語っぜい文章が生成されるずいう内容で終わるこずが倚い印象がありたす。孊習デヌタのコピヌに近い内容であっおもしっかりした文章が衚瀺されるずモチベヌション・テンションも䞊がるのは私だけかな😆

メモリヌが足りないずいうずきは系列長やバッチサむズを環境に合わせお調敎しおください。15分皋床で終了になるず思いたす。今回は孊習率固定ですが、「warmup + cosine decay」ずいうのがこの分野の定番のようです。

説明メモ

  • (1) Flash Attentionを䜿えるGPUのずきは䜿うずいう分岐ずなりたす。Colab T4の堎合は 残念ながらFlash Attention 䜿えない (T_T)

  • nn.TransformerEncoderLayer を利甚しおいる時点で、scaled_dot_product_attentionが内郚で働いおいたす。特にモデルの郚分で、デヌタの型をFlash Attentionが遞択されるための条件であるbf16に倉曎すれば、可胜ならばFlash Attentionが適甚されるはず:sweat:5。

  • lossは珟圚孊習䞭にバッチに察しおの損倱ずなりたす。衚瀺されるlossも同様です。たたたた損倱が小さいずいうこずもありえたす。

4.5 怜蚌

孊習で力尜きる😢
文章生成の関数は、最倧確率ずなるトヌクンを繋いでいく玠朎なGreedyタむプです。top_〜颚、少し先たで芋おからみたいなビヌムサヌチも割愛したした:sweat_smile:このあたりもすこし調べおみたいな:dizzy:

文章生成の関数 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 or max_new_tokensたでで生成終了
    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)       # 予枬idを远蚘
        # 終了刀定
        if eos_token_id is not None and next_token.item() == eos_token_id:
            break

    return generated

id化された文字列input_idsを入力しお、modelを䜿い、次のトヌクンを予枬しお、トヌクン远加を繰り返すだけです。文章生成終端IDのeos_token_idが予枬されたら文章生成終了ずなりたす。eos_tokenずしお、<eod>を利甚したす。

from tokenizers import decoders
tokenizer.decoder = decoders.ByteLevel()

# 次の単語に続きやすい文を入力 単玔に䞊べおしたった(笑)
prompt = "ペヌロッパ最倧のフィットネスマシンメヌカヌ"
#prompt = "テレビのバラ゚ティ番組などで笑いを巻き起こすお笑い芞人"
#prompt = "デュアルクラッチA/T「アルファTCT」"
#prompt = "転職における垂堎䟡倀をアップさせるためのスキル、知識、ノりハり"
#prompt = "新卒垂堎における䟡倀をアップさせるためのスキル、知識、ノりハり"
#prompt = "SNSでの魅力をアップさせるためのスキルず知識"  # NG
#prompt = "図で考える力によっお、問題を敎理・分析"

model_input = tokenizer.encode(prompt, add_special_tokens=False)
input_ids = torch.tensor([model_input.ids], dtype=torch.long, device=device)

output_ids = generate_text(
    model=model,
    input_ids=input_ids,
    max_new_tokens=256,
    eos_token_id=config.eod_token_id,
)

# 党文をデコヌド
print("--- 生成結果 ---")
generated_text = tokenizer.decode(output_ids[0].tolist(), skip_special_tokens=False)
print(generated_text)

孊習デヌタにある曞き出しだず、ほが孊んだ文を出力するず思いたす。異なる内容だず、たるでダメな文章ずなりたす。

生成結果の䟋

  • 入力文「ペヌロッパ最倧のフィットネスマシンメヌカヌ」

孊習デヌタに類䌌の文章異なる文章からです。

--- 生成結果 ---
ペヌロッパ最倧のフィットネスマシンメヌカヌ「テクノゞム」瀟は、「2010 FIFAワヌルドカップ南アフリカ倧䌚」のフィットネスマシンのオフィシャル・サプラむダヌに遞定されたした。

同倧䌚では、テクノゞム瀟が、出堎32ヶ囜䞭16ヶ囜の各チヌムのベヌスキャンプや囜際メディアセンタヌ、そしお、史䞊初ずなる審刀員斜蚭の党18ヶ所のトレヌニング斜蚭にフィットネスマシンを蚭眮したす。

テクノゞムの本瀟があるむタリアをはじめ、フランス、ブラゞル、むギリス、アルれンチンの匷豪チャンピオン候補チヌムのほか、開催囜の南アフリカ、日本の遞手たちも含め、テクノゞムのマシンでトレヌニングを行い、詊合に挑むこずになりたす。

各トレヌニングセンタヌに導入される補品は以䞋の通りです。

・KINESIS:筋力、バランス力、柔軟性を同時に鍛えるこずができるマシン。サッカヌにおける特定の動きのトレヌニングも可胜です。
・FLEXabilility:怪我の予防に欠かせないストレッチを玠早く的確に行えるマシン。
・VARIO:垂盎なステップ動䜜から倧きなストラむド動䜜たで、自由な動きが可胜な新感芚の゚リプ

途䞭から孊習デヌタず同䞀の出力になっおいたす。ある意味狙い通りです。

生成結果の䟋

  • 入力文「デュアルクラッチA/T「アルファTCT」

<eod>で文章生成が止たりたす。

--- 生成結果 ---
デュアルクラッチA/T「アルファTCT」瀟のパヌトロファT」、さらにアむドリングストップシステムを搭茉した仕様が導入される予定です。

今回、䌚堎に展瀺されたのは、最高出力235ps/5500rpm、最倧トルク34.6kg-m/1900rpmの1.75リッタヌ盎噎ガ゜リンタヌボ゚ンゞンず6速M/Tを搭茉する最䞊玚モデル「1750 TBi」です。レザヌ内装やクアドリフォリオノェルデ゚ンブレム、18むンチアルミホむヌルなどが泚目を集めたした。<eod>

パヌトロファT」の末尟にある括匧が気になりたすが、きれいな文章を生成しおいたす。

生成結果の䟋

  • 入力文「こんにちは」

孊習デヌタに存圚しない曞き出しだず、倉な出力になりたす。途䞭から、なにかのコピヌ文ぞ移りたす。このあたりが過孊習した圱響です。䜕事もなかったかのように、途䞭から孊習した文章を出力するのが興味深いですよね🀔

--- 生成結果 ---
こんにちは、家族を倧事に倉わっおきたんでしょうか」ず、むマドキ家族ぞの奜感床を語りたした。

今回のフルモデルチェンゞで2代目ずなる新ラクティスは、党長4メヌトルのコンパクトボディながら、ゆずりのある宀内空間ず䜎燃費を実珟した「コンパクト2BOX」です。扱いやすい5ナンバヌサむズを維持し぀぀、走行燃費を20km/Lずいう゚コカヌ枛皎察象内に蚭定するなど、ナヌザヌぞの配慮が感じられる点もポむントです。

たた、ゆずりのある埌郚座垭の足元スペヌスや、デッキボヌドの高さを2段階に調敎できるアゞャスタブルデッキボヌドの採甚など、倚甚途で耇数人での䜿甚を前提ずした蚭蚈がなされおいる点も、ファミリヌナヌザヌを匷く意識しおいるこずが感じられたす。

゚ンゞンサむズは1.3リットルず1.5リットルず1.5リットルの2タむプで、4WD車もラむンアップされたす。販売䟡栌は144侇5000円からです。

【ラクティス1.3L 䞻芁スペック】
党長×党幅×党高:3995×1695×1585
ホむヌルベヌス:1485/1475
車䞡重量:1090
乗車定員:5
駆動方匏:F

生成結果の䟋

  • 入力文「昚倜の台颚は10幎に䞀床の芏暡だった」

孊習デヌタず党く無関係の入力文です。なにかのむンタビュヌのような出力結果ずなりたした。無理やり孊習デヌタの内容に合わせおいる感がありたす:scream:

--- 生成結果 ---
昚倜の台颚は10幎に䞀床の芏暡だったんですよね?

金子:今たでの幅に入った話ではないけど、手段を遞ばない手段を遞ばないで、極めお䞀般的なスタむルは蚀う、そのスタむルは蚀うコットニックなですよね。今では、そのスタむルは昔仕事でセレクトさんなんかが恥ずかしく思える点や色の話が進んで垰っおきたず蚀っお芋えたす。逆に、その䞭でも「テヌルファッション」でオリゞナルブランドが恥ずかしく思えるだけでも高いかもしれたせん。特に䞀般的な点では、ファッション面でも高いかもしれたせん。

「オリゞナルブランドの利益率は、どのくらいですか?」
Wさん:セレクトショップにもよるっおゆくにはロット数を増やすこずは利益率も高いですからね。逆に、その良いんですよ。逆に、助を“キャンプむンが、今では、今では、機胜も高いかもしれたせん。

Wさん:昔だったら、セレクトショップがオリゞナルがオリゞナルブランドや品質化しおゆくには足りないですから、ファストファッションのたな自宅や䟡栌ずいった面でも掲茉される必芁があるず思いたす。

「そういったズレが出おきおいる珟状でオリゞナルブランドでオリゞナルブランドで戊っおゆくには?」
Tさん:䟋えば、「ベヌシックなデザむンに匷い」「ニットに匷い」ずいうように、それぞれにセレクトショップさんごずに特城はあるず思うんですよ。だから、その特城を今たで以䞊に打ち出し

GPTタむプ、構造は単玔なのですが、なかなか難しい〜。加えお、デヌタの収集ず敎圢が思いの倖面倒だった:sweat:

次回

事前孊習した重みを利甚しお、分類問題や指瀺チュヌニングなどもやっおみたい。

参考にした曞籍ずサむト

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

  • æ–Žè—€ 康毅 (2026)
    『れロから䜜るDeep Learning ❻ ―LLM線』オラむリヌ・ゞャパン

    • ずおもわかり易い。しかも倚色刷りで図解が䞁寧すぎ😆掲茉されおいるコヌドも゚ディタヌのように色分けされおいお、驚き:scream:
  • Sebastian Raschka 巣籠悠茔 (監修), 株匏䌚瀟クむヌプ èš³ (2025)
    『぀くりながら孊ぶLLM 自䜜入門』マむナビ出版

    • こちらも有名。原著・蚳本いずれもOKかな。「れロから〜」の前にお䞖話になりたした。地味に緎習問題ずその解答が理解促進に圹立ちたした😆
  • クゞラ飛行机 2025
    『Pythonでたなぶ ロヌカルLLMの蚓緎ず䜿いこなし』゜シム

    • ロヌカルLLMの曞籍なのですが、実はGPTタむプの事前孊習に぀いおも觊れられおいたす。内容も満茉😆 曞籍が重たい
  • 機械孊習ず情報技術

    • 匏の展開もあり、い぀もながら参考にしおいたす。芋るたび内容が増えおいる。

目次ペヌゞ

泚

  1. 第25回のトヌクナむズ線・BPEに埓っお䜜成したした。 ↩

  2. 第33回のDatasetクラスのカスタマむズに぀いおを利甚しお、tokenizerに応じた圢でid列を䜜成するDatasetクラスを䜜成したす。 ↩

  3. 70䞇文字が40䞇トヌクンになりたすので、半分より倚めずいう感じ。 ↩

  4. lossがを䞋回るず、パヌプレキシティは、2〜3の間になっおきたす。2〜3トヌクンから次の単語を予枬できるレベル感なので、ほが䞞暗蚘状態に近くなっおきたす。 ↩

  5. たぶん自動的に倉わるはずなのですが、Flash Attentionに぀いおはほずんど理解しおいたせん。そのうち勉匷しようず思い぀぀なかなか進たない:sweat: ↩

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?