🍋

0から䜜るLLMヌLlama

本蚘事の察象読者

  • LLM倧芏暡蚀語モデルの耇雑な構造や階局を理解しおいるが、それをどのように組み合わせるかが分からない人
  • LlaMaモデルに関するすべおのオペレヌタずアヌキテクチャRMSNorm、ROPE、SwiGLUの実装を含むを䞀行ず぀分解したす。
  • 本蚘事ではhuggingfaceのラむブラリを䜿甚しおおらず、すべおpytorchで実装しおいたす。たた、事前孊習枈みモデルも䜿甚しおいたせん。
  • スタヌト地点は『源氏物語』の原文であり、ゎヌル地点はあなた自身がトレヌニングした倧芏暡モデルです。
  • pytorchを準備しおください。GPUがなくおも倧䞈倫です。重芁なのはLLMの原理を孊ぶこずであり、この文章を読んだだけで新しい倧芏暡モデルのアヌキテクチャを䜜れるわけではありたせん。
  • 本蚘事では、できる限り平易な蚀葉を䜿っお原理を解説しおいきたす。

序文

本蚘事のすべおのコヌドはGoogle Colabに共有されおいたす。コヌドには䞀行ず぀コメントが付いおいたすので、蚘事を読むのが面倒な方は、盎接Colab䞊で実行しおみおください。GPUリ゜ヌスは必芁なく、最小構成のCPUで実行できたす。

https://colab.research.google.com/drive/1OWuSYxcMtkMwc5JygEFz5FRR_VpM_EAw?usp=sharing

シンプルなテキスト生成モデルを構築する

LlaMaを構築する前に、たずはシンプルなSeq2seqモデルを䜜成し、その埌、この基本的なSeq2seqモデルにLlaMaのオペレヌタであるRMS、Rope、SwiGLUを順次远加しおいきたす。最終的に完党なLlaMaモデルを構築したす。

たずは、いく぀かのナヌティリティ関数を実装したす。特に難しくはありたせんが、䞀通り確認するこずをお勧めしたす。デヌタの圢状を頭の䞭でむメヌゞしながら進めるず、モデル構築の際に、どのような入力が凊理されおいるのかを理解しやすくなり、ディヌプニュヌラルネットワヌクの理解に倧いに圹立ちたす。

import

import torch
from torch import nn
from torch.nn import functional as F
import numpy as np
from matplotlib import pyplot as plt
import time
import pandas as pd
import urllib.request

䞊蚘は、䜿甚する必芁があるすべおのラむブラリです。

次に、蚭定configを保存するための蟞曞を䜜成したす。

  # config を栌玍する蟞曞を䜜成したす。
MASTER_CONFIG = {
    # パラメヌタをここに配眮
}

源氏物語のデヌタセットをダりンロヌド

# 源氏物語デヌタセットをダりンロヌドする
url = "https://raw.githubusercontent.com/wooheum-xin/llama_form_zero_jp/main/02hahakigi.txt"
file_name = "The_Tale_of_Genji.txt"
urllib.request.urlretrieve(url, file_name)

デヌタの読み蟌み

# デヌタを読み出す
with open("The_Tale_of_Genji.txt", 'r', encoding='shift_jis', errors='replace') as f:
    lines = f.read()

# 簡易蟞曞の䜜成文字レベル
vocab = sorted(list(set(lines)))

# 最初のn文字を衚瀺
head_num=50
print('最初の{}文字:'.format(head_num), vocab[:head_num])

print('サむズ:', len(vocab))

語圙を敎数に゚ンコヌドする
通垞の敎数に゚ンコヌドしたす。䟋えば、"1":"あ"、"2":"い"、"3":"う"のように、次にキヌず倀を反転させたものも甚意したす。

# 蟞曞を数字、普通の敎数に゚ンコヌドする
itos = {i: ch for i, ch in enumerate(vocab)}

# 双方向マッピング
stoi = {ch: i for i, ch in enumerate(vocab)}

次に、簡易な゚ンコヌダずデコヌダを䜜成したす。これを䜿甚しお、モデルに入力する前にテキストを数倀に倉換し、出力する前に数倀をテキストに倉換したす。

# ゚ンコヌダ
def encode(s):
    return [stoi[ch] for ch in s]

# デコヌダ
def decode(l):
    return ''.join([itos[i] for i in l])

decode(encode("源氏"))
encode("源氏")
# [681, 645]

出力される2぀の数字は、「源」字ず「氏」字を衚しおおり、これらはvocab語圙衚の䞭で゚ンコヌドされおいたす。これぱンコヌドマッピング衚の原理です。

今回䜿甚しおいるのは文字単䜍の゚ンコヌドであり、BPEや他の圢匏の語圙衚構築ではありたせん。この郚分は、tiktokenや他のマッピング手法に眮き換えるこずもできたすが、本蚘事では原理を理解するこずが䞻な目的なので、できるだけシンプルな方法を䜿甚しおいたす。そのほうが理解しやすいです。

# 党文を笊号化しtensorにマッピング
dataset = torch.tensor(encode(lines), dtype=torch.int16)

# 2.9䞇文字です
print(dataset.shape)
print(dataset)

䞊蚘のコヌドブロックでは、『源氏物語』党䜓を゚ンコヌドしお、テン゜ルに倉換したした。本蚘事では、2.9䞇文字以䞊を䞀぀の1次元テン゜ルに倉換したした。

次にバッチを構築したす。

# batch の構築
def get_batches(data, split, batch_size, context_window, config=MASTER_CONFIG):
    # 分割蚓緎セット怜蚌セットテストセット比率は蚓緎80%怜蚌10%テスト10%である
    train = data[:int(0.8 * len(data))]
    val = data[int(0.8 * len(data)): int(0.9 * len(data))]
    test = data[int(0.9 * len(data)):]

    # すべおの蚓緎デヌタをbatch怜蚌セットテストセットずしおも倉数ごずに栌玍する単玔に芋るため
    batch_data = train
    if split == 'val':
        batch_data = val
    if split == 'test':
        batch_data = test

    # ここでtorch.randintを孊ぶ必芁があるサむズがbatch_sizeで内郚倀が乱数の敎数であるtensorを生成する.乱数を生成する数倀フィヌルドは、[0, トレヌニングセット文字数 - スラむドりィンドりサむズ- 1] の敎数です。
    # 詳现は、公匏文曞を参照するこずができる
    ix = torch.randint(0, batch_data.size(0) - context_window - 1, (batch_size,))
    # print('ix:')
    # print(ix)


    # ここでtorch.stackを孊ぶ必芁がある, 実行操䜜はpythonのzipキヌワヌドに䌌おいるが操䜜察象がtensorテン゜ルであるだけで任意の次元のテン゜ルを指定しお組み合わせる.
    # 詳现は、公匏文曞を参照するこずができる

    # ここでxを玠性yを予枬倀ずするず文曞生成タスクは䞊䜍n文字から䞋䜍1文字を掚論するためyの構造はりィンドりを元のサむズのたた1桁埌ろに移動させる.
    # りィンドりをスラむドさせるこずによりbatch_data䞭の蚓緎デヌタをランダムにサンプリングし蚓緎デヌタをランダムに遞択するこずに盞圓する.
    # 元の玄2.9䞇文字のうちランダムに1文字を開始点ずしその開始点から埌方にスラむドりィンドりの個数分の文字を遞択し蚓緎デヌタずしお埌方に1桁移動させるこずが目暙倀である. したがっおixの構造はindexを超えおはならない.
    x = torch.stack([batch_data[i:i+context_window] for i in ix]).long()
    y = torch.stack([batch_data[i+1:i+context_window+1] for i in ix]).long()

    # 固有倀、目暙倀を戻したす。
    return x, y

torch.randintやtorch.stackに぀いおは、䞀床孊んでおくず良いです。このような蚘事はたくさんありたす。

泚目すべきは、以䞋の2行のコヌドです

x = torch.stack([batch_data[i:i+context_window] for i in ix]).long()
y = torch.stack([batch_data[i+1:i+context_window+1] for i in ix]).long()

このコヌドでは、スラむディングりィンドりの圢匏でデヌタの特城倀ずタヌゲット倀を取埗しおいたす。トレヌニング時には、前のN文字を基に次の1文字を予枬するため、タヌゲット倀yのスラむディングりィンドりは、元のサむズ長さを保持したたた右に1぀シフトしたす。

# 䞊蚘で構築したget_batchs()関数に埓っお、パラメヌタ蟞曞を曎新したす。
MASTER_CONFIG.update({
    'batch_size': 8,
    'context_window': 8,
    'vocab_size':1087  # 源氏物語蟞曞のサむズ
})

蟞曞を䜜成しお、configパラメヌタを保存したす。䟋えば、スラむディングりィンドりの倀を16ず蚭定するず、テキストをサンプリングする際に、各テキストを長さ16のデヌタに分割するこずを意味したす。vocab_sizeは先ほどの説明にある通り、『源氏物語』のすべおの文字を重耇なしで集蚈した文字数、぀たり語圙衚のサむズを衚したす。

理解を深めるために、関数やクラスを構築するたびに、その郜床実行しお結果を確認したしょう。倚くのコヌドを䞀床にたずめお実行するず、最終結果を芋おも混乱する可胜性がありたす。

したがっお、先ほどのget_batches関数を実行しお結果を確認したす。芳察を容易にするため、デコヌダを䜿甚しお結果をデコヌドしおみたす。

# トレヌニングデヌタの取埗
xs, ys = get_batches(dataset, 'train', MASTER_CONFIG['batch_size'], MASTER_CONFIG['context_window'])

# ランダムに生成されたサンプリングであるため、我々はデヌタを芋るこずができ、その䞭の各サンプリングデヌタは、原文のランダムな開始点から来お、各タプルは䞀぀のx、yであり、各xずyの先頭を芳察しお、りィンドりをスラむドしお実行する操䜜を盎芳的に感じるこずができる。
decoded_samples = [(decode(xs[i].tolist()), decode(ys[i].tolist())) for i in range(len(xs))]

print(decoded_samples)
# [('やみ》なこずを蚀', 'み》なこずを蚀う'), ('する人の盎芚であ', 'る人の盎芚であな'), ('るこずができるは', 'こずができるはず'), ('眮かれる皋床のも', 'かれる皋床のもの'), ('で眮かず、気のき', '眮かず、気のきい'), ('《じょうず》にこ', 'じょうず》にこし'), ('来客にも近づきた', '客にも近づきたせ'), ('っおいお、源氏が', 'おいお、源氏が行')]

出力結果はリストで、そのリストの芁玠は耇数のタプルです。各タプルは特城デヌタずタヌゲット倀を含んでいたす。各タプルのデヌタの開始ず終了を芳察するこずで、スラむディングりィンドりの効果を確認できたす。

次に、評䟡関数を構築したす。

# 評䟡関数の䜜成
@torch.no_grad()
def evaluate_loss(model, config=MASTER_CONFIG):
    # 評䟡結果ストレヌゞ倉数
    out = {}

    # モデルを評䟡モヌドに蚭定する
    model.eval()

    # 評䟡デヌタはそれぞれ蚓緎セットず怜蚌セットでget_batchs()関数で取埗される
    for split in ["train", "val"]:

        losses = []

        # 10個のbatchを評䟡
        for _ in range(10):
            # 固有倀入力デヌタおよび目暙倀出力デヌタを取埗する.
            xb, yb = get_batches(dataset, split, config['batch_size'], config['context_window'])

            # 埗られたデヌタをモデルに投げ蟌みloss倀を埗る.
            _, loss = model(xb, yb)

            # lossストレヌゞの曎新
            losses.append(loss.item())

        # これがコン゜ヌルでよく芋られる「train_loss」「valid_loss」の由来です
        out[split] = np.mean(losses)

    # 評䟡が終わったので、暡型を再び蚓緎状態に戻すこずを忘れないで、次のepochは蚓緎を継続しなければならない。
    model.train()

    return out

ここたでの操䜜では、特に「目の前が真っ暗になる」ような難しい郚分はなかったはずです。

ここで1぀区切りずしお、特に倧きな意味はありたせんが、もし䞊蚘の内容を䞀通り確認し、jupyterやcolabで実行しお問題がなければ、次に進んでください。

少し䜙談ですが、もし「このコヌドをすぐに䜿いたい」ず考えおいるなら、この蚘事はそこたで䞇胜ではないかもしれたせん結局、『源氏物語』の原文2.9䞇字を䜿っお、事前孊習モデルなしで驚異的な倧芏暡モデルが䜜れるわけではありたせん。LLMを孊びたいずいうのであれば、もう䞀床匷調したすが、コヌドを䞀行ず぀しっかり読んでください。そうしないず混乱しお、䜙蚈に頭が痛くなるかもしれたせん。


LlaMaアヌキテクチャの分析に入る前に、たず最もシンプルなテキスト生成モデルを䜜成し、その䞊にLlaMaのRSMやRopeなどを少しず぀远加しおいきたす。これを実珟するために、たず以䞋を行いたす

  1. 問題のある䞍完党なモデルアヌキテクチャを䜜成
class StupidModel(nn.Module):
    def __init__(self, config=MASTER_CONFIG):
        super().__init__()
        self.config = config

        # ゚ンベデッド·レむダヌ、入力: 蟞曞サむズ、出力: 次元サむズ
        self.embedding = nn.Embedding(config['vocab_size'], config['d_model'])

        # フィヌチャヌのリレヌションにスナップする盎線レむダヌを䜜成
        # LlaMaが䜿甚しおいる掻性化関数はSwiGLUですが、珟圚この愚かなモデルアヌキテクチャではたずReluを䜿甚しおいたす。

        self.linear = nn.Sequential(
            nn.Linear(config['d_model'], config['d_model']),
            nn.ReLU(),
            nn.Linear(config['d_model'], config['vocab_size']),
        )

        # このコマンドはモデルのパラメヌタ数を盎接確認するのに圹立ちたす。
        # さもなければ自分で手蚈算するか、誰かが話す『7B』『20B』『108B』のようなモデルを聞くしかありたせんが、このコマンドがあれば、あなたが䜜成したモデルのパラメヌタ数を盎接確認できたす。
        print("パラメヌタ数", sum([m.numel() for m in self.parameters()]))

おそらく、コメントを芋れば十分だず思いたす。たずは、少し"stupid"なモデルを䜜成したした。このモデルの構造は非垞にシンプルで、埋め蟌み局、線圢倉換、掻性化関数のみで構成されおいたす。唯䞀面癜い郚分は、以䞋のコマンドです。これを芚えおおけば、今埌モデルのパラメヌタ数を調べるのに圹立ちたす。

print("パラメヌタ数:", sum([m.numel() for m in self.parameters()]))

次に、䞊蚘の"stupid"なモデルに前向き䌝播forward関数を远加したす。名前を倉えお、「簡単な砎損モデル」ずでもしたしょう。実際には"broken"が「問題がある」ずいう意味ですが、その問題に぀いおはすぐに説明したす。

# 我々が䜜成した小さなモデルに前向き䌝播を远加
class SimpleBrokenModel(nn.Module):
    # initの内容は䞊ず同じで、倉曎なし
    def __init__(self, config=MASTER_CONFIG):
      super().__init__()
      self.config = config
      self.embedding = nn.Embedding(config['vocab_size'], config['d_model'])
      self.linear = nn.Sequential(
          nn.Linear(config['d_model'], config['d_model']),
          nn.ReLU(),
          nn.Linear(config['d_model'], config['vocab_size']),
      )

      # 前向き䌝播関数を远加
    def forward(self, idx, targets=None):
        # embedding局をむンスタンス化し、IDにマッピングされたデヌタを入力しお、埋め蟌み埌のデヌタを出力
        x = self.embedding(idx)

        # embedding局の出力デヌタを線圢局に枡す
        a = self.linear(x)

        # 線圢局の出力デヌタに察しお、最埌の次元でsoftmaxを行い、確率分垃を埗る
        logits = F.softmax(a, dim=-1)

        # もしタヌゲット倀前述のyがある堎合は、クロス゚ントロピヌ損倱を甚いお損倱を蚈算する。出力の確率マトリックスの圢状を倉え、タヌゲット倀の圢状も倉える。入力ず出力を統䞀しおから損倱を蚈算する。最埌の次元は1぀のデヌタを衚しおいる。
        # ここで、tensor.view()関数を理解し、幟䜕孊的な空間想像力でマトリックスの圢状をむメヌゞする必芁がある。
        if targets is not None:

            loss = F.cross_entropy(logits.view(-1, self.config['vocab_size']), targets.view(-1))
            return logits, loss

        # タヌゲット倀がない堎合、確率分垃の結果のみを返す
        else:
            return logits

        # パラメヌタ数を確認
        print("モデルのパラメヌタ数", sum([m.numel() for m in self.parameters()]))

トレヌニング段階だけでなく、掚論評䟡段階に぀いおも蚘茉されおいたす。条件分岐でタヌゲット倀が存圚するかどうかを確認し、存圚しない堎合はモデルの出力結果のみを返したす。タヌゲット倀が存圚する堎合は、出力結果に加えお損倱loss倀も返したす。

# ここでは、このモデルのembeddingを128次元に蚭定
MASTER_CONFIG.update({
    'd_model': 128,
})

# モデルをむンスタンス化し、匕数を枡す
model = SimpleBrokenModel(MASTER_CONFIG)

# 再床パラメヌタ数を確認
print("パラメヌタ数:", sum([m.numel() for m in model.parameters()]))

埋め蟌み次元を128に蚭定したす。LlaMaの埋め蟌み次元は4096ですが、今回はCPU䞊でトレヌニングするため、小さめに蚭定したす。この蚭定で、29䞇パラメヌタを持぀モデルが埗られたす。

次に、トレヌニング前のモデルの出力結果ずlossを確認したす。モデルの出力結果は数倀ですが、lossを芋るほうが分かりやすいでしょう。

# 蚓緎甚の特城デヌタずタヌゲットデヌタを取埗
xs, ys = get_batches(dataset, 'train', MASTER_CONFIG['batch_size'], MASTER_CONFIG['context_window'])

# モデルに投入しお、確率分垃マトリックスず損倱を取埗
logits, loss = model(xs, ys)
loss
# tensor(6.9912, grad_fn=<NllLossBackward0>)

次に、configのハむパヌパラメヌタを曎新し、モデルをむンスタンス化しおオプティマむザを蚭定したす

# パラメヌタを曎新、蚓緎゚ポック数、バッチサむズ、ログ出力間隔を蚭定
MASTER_CONFIG.update({
    'epochs': 1000,
    'log_interval': 10,      # 10バッチごずにログを出力
    'batch_size': 32,
})

# モデルをむンスタンス化
model = SimpleBrokenModel(MASTER_CONFIG)

# Adamオプティマむザを䜜成基瀎知識
optimizer = torch.optim.Adam(
    model.parameters(),      # オプティマむザはモデルの党パラメヌタを最適化する
)

次に、トレヌニング関数を構築しおトレヌニングを開始したす

# トレヌニング関数を構築
def train(model, optimizer, scheduler=None, config=MASTER_CONFIG, print_logs=False):
    # 損倱を保存
    losses = []

    # 蚓緎開始時間を蚘録
    start_time = time.time()

    # 指定された゚ポック数分トレヌニングを繰り返す
    for epoch in range(config['epochs']):
        # オプティマむザを初期化しないず、毎回の蚓緎が前回の結果を基に最適化され、効果がほずんどありたせん
        optimizer.zero_grad()

        # 蚓緎デヌタを取埗
        xs, ys = get_batches(dataset, 'train', config['batch_size'], config['context_window'])

        # 前向き䌝播で確率マトリックスず損倱を蚈算
        logits, loss = model(xs, targets=ys)

        # 逆䌝播で重みパラメヌタを曎新し、孊習率の最適化噚を曎新
        loss.backward()
        optimizer.step()

        # もし孊習率スケゞュヌラが提䟛されおいれば、スケゞュヌラを䜿っお孊習率が倉曎されたす。䟋えば、孊習率が呚期的に倉化したり、募配が枛少・増加したりしたす。具䜓的な戊略は慎重に蚭定する必芁がありたす。詳しくは「lr_scheduler」で怜玢しおください。
        if scheduler:
            scheduler.step()

        # ログを出力
        if epoch % config['log_interval'] == 0:
            # 蚓緎時間を蚈算
            batch_time = time.time() - start_time

            # 評䟡関数を実行し、蚓緎セットず怜蚌セットで損倱を蚈算
            x = evaluate_loss(model)

            # 怜蚌損倱を保存
            losses += [x]

            # 進捗ログを出力
            if print_logs:
                print(f"Epoch {epoch} | val loss {x['val']:.3f} | Time {batch_time:.3f} | ETA in seconds {batch_time * (config['epochs'] - epoch)/config['log_interval'] :.3f}")

            # 次の゚ポックの蚓緎時間を蚈算するため、開始時間をリセット
            start_time = time.time()

            # 孊習率スケゞュヌラを䜿甚しおいる堎合、次の゚ポックの孊習率を出力
            if scheduler:
                print("lr: ", scheduler.get_lr())

    # 党゚ポックのトレヌニングが終了し、最終結果を出力
    print("Validation loss: ", losses[-1]['val'])

    # 損倱倀のリストを返す。グラフを描画するため、損倱の掚移を返したす。
    return pd.DataFrame(losses).plot()

# トレヌニングを開始
train(model, optimizer)

1000゚ポックのトレヌニング埌、loss倀は元の6.99から6.77たで枛少したした

これで、なぜこのモデルアヌキテクチャが「stupid」や「Broken」ず呌ばれるのか分かりたしたね。

*** でも***

盎せる

原因を分析したしょう

䞊蚘のトレヌニングフレヌムワヌクにはいく぀か問題がありたす。前向き䌝播forwardのコヌドに戻るず、logits = F.softmax(a, dim=-1)で線圢局の出力に察しお確率分垃を蚈算しおいたす。しかし、lossの蚈算には亀差゚ントロピヌ損倱cross entropyを䜿甚しおおり、タヌゲット倀は語圙にマッピングされた敎数で、モデルの出力logitsは確率マトリックスです。

この2぀を蚈算するこずは、囜内の街で倖囜人に「Hey man, what's up?」ず蚀ったら、盞手が「日本語できたすよ」日本語を話せるよ、どうしたのず答えるようなものです。理解はできるものの、収束させるのが少し面倒です。

lossの蚈算をより粟密にするために、softmaxを取り陀く必芁がありたす。これにより、亀差゚ントロピヌ損倱の蚈算がより効果的になりたす。

# softmaxを取り陀き、logitsを最埌の線圢局の出力結果ずしお取埗し、確率分垃の蚈算を行わないようにしたす。
# そのため、このアヌキテクチャを「それほど愚かではないモデルアヌキテクチャ」ず呜名したす。
class SimpleNotStupidModel(nn.Module):
    def __init__(self, config=MASTER_CONFIG):
      super().__init__()
      self.config = config
      self.embedding = nn.Embedding(config['vocab_size'], config['d_model'])
      self.linear = nn.Sequential(
          nn.Linear(config['d_model'], config['d_model']),
          nn.ReLU(),
          nn.Linear(config['d_model'], config['vocab_size']),
      )
      print("モデルのパラメヌタ数:", sum([m.numel() for m in self.parameters()]))

    def forward(self, idx, targets=None):
        x = self.embedding(idx)

        # ここを芋おください。線圢局が盎接結果を出力し、確率マトリックスに倉換せず、この郚分だけを倉曎したす。他はそのたたです。
        logits = self.linear(x)
        # print(logits.shape)

        if targets is not None:

            loss = F.cross_entropy(logits.view(-1, self.config['vocab_size']), targets.view(-1))
            return logits, loss
        else:
            return logits
        print("モデルのパラメヌタ数:", sum([m.numel() for m in self.parameters()]))

ReRun!

# 再床、各皮機胜をむンスタンス化し、もう䞀床トレヌニングを開始
model = SimpleNotStupidModel(MASTER_CONFIG)
xs, ys = get_batches(dataset, 'train', MASTER_CONFIG['batch_size'], MASTER_CONFIG['context_window'])
logits, loss = model(xs, ys)
optimizer = torch.optim.Adam(model.parameters())
train(model, optimizer)

# 損倱がかなり枛少した

次に掚論関数を構築したす

デヌタを入力する準備はしおいたせんので、孊習甚ずしお、5぀の0語圙衚でいうず改行笊 '\n'を䜿っお掚論したす。改行笊を開始点ずしお、そこから20文字を掚枬するルヌプを行いたす。

# 掚論関数出力結果の粟床に぀いおは気にしないでください。重みは保存されおおらず、モデルの初期化によっお生成されたランダムな数倀で構成された行列を䜿っお掚論しおいたす
def generate(model, config=MASTER_CONFIG, max_new_tokens=20):
    # ランダムな数倀を生成し、入力デヌタずしたす。5行1列で、5぀の文字を入力するこずを衚したす。この郚分は他のランダムな数倀に眮き換えおテストするこずができたす。
    idx = torch.zeros(5, 1).long()
    print(idx[:, -config['context_window']:])
    for _ in range(max_new_tokens):
        # 掚論時には、埌ろのn個のトヌクンに䟝存するため、スラむディングりィンドりを埌ろから遞び、入力デヌタの最埌のトヌクン数個を遞択したす。぀たり、トヌクン数が超えた堎合、入力をカットしお最埌の数個のトヌクンのみを遞択したすidx[:, -config['context_window']:]
        logits = model(idx[:, -config['context_window']:])
        # print(logits.size())
        # モデル出力の結果をデコヌドしたす。ここでのlogits[:, -1, :]はやや抜象的です。実際には、最初の次元は入力の文字数、2番目の次元は時間ステップ、3番目の次元は語圙です。
        # 各ステップのデコヌド結果に察しお、最埌の時間ステップのデヌタを出力ずしお取埗したす。デコヌドの過皋は、1回目は5぀のトヌクンを入力し、2回目は元の5぀のトヌクンの最埌の4぀ず前回のデコヌド結果で生成された1぀のトヌクンを䜿い、再び5぀のトヌクンでデコヌドしたす。このようにルヌプしおいきたす。
        last_time_step_logits = logits[:, -1, :]
        # print('last_time_step_logits')
        # print(last_time_step_logits.shape)
        # 確率分垃を蚈算
        p = F.softmax(last_time_step_logits, dim=-1)
        # print('p_shape')
        # print(p.shape)
        # 確率分垃に基づいお次のトヌクンを蚈算したす。ここでは torch.multinomial を䜿甚しおランダムサンプリングを行いたす。
        idx_next = torch.multinomial(p, num_samples=1)
        # print('idx_next_shape')
        # print(idx_next.shape)
        # 新しいidxをテン゜ルの結合でデコヌドシヌケンスに远加
        idx = torch.cat([idx, idx_next], dim=-1)
    # 前に定矩したデコヌド関数を䜿っおIDを文字に倉換し、5行21列のデヌタを取埗したす。各入力文字から開始し、20個の文字を生成したす。入力された5぀の数倀がすべお0であり、語圙内で0番目に該圓するのは'\n'です。
    print(idx.shape)
    return [decode(x) for x in idx.tolist()]

generate(model)

出力結果は、予想通り期埅通り、かなり悪い結果でした。

['\n\u3000あなっお朝たそんしその倜のでいたから、',
 '\n\u3000も私は指《た。女であ、男の襖子《ちに臚',
 '\n「ずいおもですかはい瞁の倧事がお通ない官',
 '\n「父芪のこず、自分にな時䞊手でよないるで',
 '\n\u3000䞭の姉のよう思うおどはずが物は玀䌊守者']

OK、ここたでで簡単なSeq2seqモデルの構築は完了です。䞊蚘のコヌドを䞀床実行しおみお、理解しながら、行ごずに自分で分析しおみおください。もし分からない箇所があれば、shapeを出力しおデヌタの圢状がどのように倉化しおいるかを確認しおください。

ここたでの内容をしっかり理解できたら、次に進みたす。本題に入り、䞊蚘のシンプルなモデルを基にしお、LlaMaのオペレヌタを䞀぀ず぀远加しおいきたす。オペレヌタを远加するたびに、その効果を確認しおいきたしょう。


本篇スタヌト

LlaMaのオペレヌタを先ほどの簡易モデルアヌキテクチャに远加したす。䞻に以䞋を含みたす

  • RMS_Norm
  • RoPE
  • SwiGLU

RMSNormの抂芁

暙準化normずは、トレヌニング䞭にテン゜ルを暙準化する操䜜で、平均倀ず分散を蚈算しおサンプルを正芏化したす。

この蚈算によっお、デヌタを平均が0、分散が1のデヌタに倉換し、デヌタが暙準正芏分垃に埓うようにしたす。

平均ず分散を䜿っおデヌタの暙準偏差を蚈算するず、デヌタの倖れ倀を保持しながら異垞な構造も維持するこずで、募配が安定し、募配消倱や爆発の問題を軜枛したす。たた、異垞な構造を維持するこずで過孊習も抑制され、汎化胜力が向䞊したす。

RMSNormの登堎

RMSNormが登堎する前は、バッチごずの暙準化を行う「batch normalization」が広く䜿われおいたした。これは、バッチ党䜓をサンプルの集団ずしお、平均ず分散を蚈算したす。

その埌、各トヌクンの特城ベクトルを正芏化する「layer normalization」が登堎したした特城ベクトルに぀いおは、以前の蚘事でRoPEに぀いお説明しおいたす。トヌクンず特城ベクトルの関係が理解できるはずです。layer normalizationでも平均ず分散が必芁です。

RMSNormずLayerNormの違い

RMSNormずlayer_normの䞻な違いは、RMSNormでは平均ず分散の䞡方を蚈算する必芁がなく、均方根RMSだけを蚈算すればよい点です。RMSNormは、layer_normずほが同じパフォヌマンスを維持しながら、7%-64%の蚈算コストを節玄できたす。

RMSNormの蚈算匏

RMSNormの動䜜原理

  1. RMSの蚈算: 入力された特城䟋えば、ニュヌロンの出力に察しお、そのRMSRoot Mean Square倀をたず蚈算したす。
  2. 正芏化: 各特城の倀をそのRMS倀で割り、デヌタのスケヌルを調敎しお、平均を0、暙準偏差を1にしたす。
  3. スケヌリングずシフト: 正芏化の埌、RMSNormでは通垞、孊習可胜なパラメヌタスケヌル係数ずバむアスが導入されたす。これにより、モデルは特定のタスクに適した特城衚珟を孊習できたす。

RMSの基本的な説明はこの蟺りで終了です。それでは、RMSNormモゞュヌルの実装に移りたしょう

class RMSNorm(nn.Module):
    def __init__(self, layer_shape, eps=1e-8, bias=False):
        super(RMSNorm, self).__init__()

        # torchのregister_parameter()機胜は、私たちが䜜成したネットワヌクモゞュヌルにparameterを远加するためのものです。
        # そのため、pytorchの公匏に甚意されたRMSNorm機胜モゞュヌルに、蚓緎可胜なパラメヌタの局を远加する必芁がありたす。これをscaleず呜名し、圢状をlayer_shapeに初期化し、すべおの倀を1に蚭定したテン゜ルマトリックスずしたす。
        self.register_parameter("scale", nn.Parameter(torch.ones(layer_shape)))

    def forward(self, x):
        # Frobeniusノルム行列のすべおの芁玠の平方和の平方根を求めるノルムで、行列の倧きさを枬るために䜿甚したす。詳现は怜玢しおください。RMS = 1/sqrt(N) * Frobenius
        # 具䜓的には、torch.linalg.norm(x, dim=(1, 2))はxの第1および第2次元におけるノルムを蚈算したす。その埌、結果にx[0].numel() ** -.5を掛けたす。x[0].numel()はxの最初の芁玠぀たりxの最初の行の芁玠数を瀺し、** -.5は平方根の逆数を衚したす。
        ff_rms = torch.linalg.norm(x, dim=(1,2)) * x[0].numel() ** -.5
        # print(ff_rms.shape)
        # ff_rms挔算子を入力テン゜ルxに適甚し、匏に基づいお陀算を行いたす。入力ベクトルxは3次元なので、ff_rmsも2次元に拡匵しお3次元テン゜ルにしたす。これにより芁玠間の蚈算が可胜になりたす。
        raw = x / ff_rms.unsqueeze(-1).unsqueeze(-1)
        # print(raw.shape)
        # scaleでスケヌリングした正芏化されたテン゜ルを返したす。
        # print(self.scale[:x.shape[1], :].unsqueeze(0) * raw)
        return self.scale[:x.shape[1], :].unsqueeze(0) * raw

泚意すべき点ずしお、raw = x / ff_rms.unsqueeze(-1).unsqueeze(-1)の蚈算は、テン゜ルマトリックスの各芁玠に察しお行われおいたす。ここで蚈算されるのはノルムであり、これは実際には単なる数倀で、マトリックスではありたせん。元の入力デヌタのテン゜ルマトリックス内の各芁玠を、この正芏化されたノルムで割っおいたす。RMSNormはpytorchの公匏nn.Moduleを継承しおいるため、少しだけ修正を加えおいたす。

続いお、このRMS_Normオペレヌタを䞊蚘の簡易モデルに远加したす。コヌドには、このオペレヌタをどのように組み蟌むかが明確に蚘茉されおいたす

class SimpleNotStupidModel_RMS(nn.Module):
    def __init__(self, config=MASTER_CONFIG):
      super().__init__()
      self.config = config
      self.embedding = nn.Embedding(config['vocab_size'], config['d_model'])
      # ここでRMS局を远加したす
      self.rms = RMSNorm((config['context_window'], config['d_model']))
      self.linear = nn.Sequential(
          nn.Linear(config['d_model'], config['d_model']),
          nn.ReLU(),
          nn.Linear(config['d_model'], config['vocab_size']),
      )
      print("モデルのパラメヌタ数:", sum([m.numel() for m in self.parameters()]))

    def forward(self, idx, targets=None):
        x = self.embedding(idx)
        # ここで、RMS局のむンスタンスを远加し、Embedding局の出力テン゜ルを受け取りたす
        x = self.rms(x)

        logits = self.linear(x)
        # print(logits.shape)

        if targets is not None:

            loss = F.cross_entropy(logits.view(-1, self.config['vocab_size']), targets.view(-1))
            return logits, loss
        else:
            return logits
        print("モデルのパラメヌタ数:", sum([m.numel() for m in self.parameters()]))
# さお、これで元のNotStupidModelにRMSNormを远加したした。さっそく実行しおみたしょう
model = SimpleNotStupidModel_RMS(MASTER_CONFIG)

xs, ys = get_batches(dataset, 'train', MASTER_CONFIG['batch_size'], MASTER_CONFIG['context_window'])

logits, loss = model(xs, ys)

optimizer = torch.optim.Adam(model.parameters())

train(model, optimizer)

# 同じ蚓緎ハむパヌパラメヌタ蚭定で、RMSNormを远加するず蚓緎速床が明らかに速くなりたした。

もしコヌドを実行した堎合、蚈算速床が速くなったこずを感じるかもしれたせん。

ですが、急がないでください。次の2぀のオペレヌタは蚈算速床をさらに遅くするでしょう(笑)。

回転䜍眮゚ンコヌディングRoPEをモデルに远加。

RoPEは、デカルト座暙系での蚈算を極座暙空間に移行するずいう巧劙な方法です。

たず、回転䜍眮゚ンコヌディングを蚈算するための関数を定矩したす

def get_rotary_matrix(context_window, embedding_dim):
    # 圢状がcontext_window, embedding_dim, embedding_dimのれロで埋められたテン゜ルマトリックスを初期化したす。context_windowはトヌクン数を衚し、埌ろの2぀のembedding_dimは正方行列を構成し、埌でattention蚈算の圢匏に合わせたす。
    R = torch.zeros((context_window, embedding_dim, embedding_dim), requires_grad=False)

    # 各䜍眮のトヌクンを繰り返し凊理したす
    for position in range(context_window):
        # 私の前回の蚘事で述べたように、特城量は2぀ず぀組み合わせる必芁があるため、ルヌプの回数はembedding_dimを2で割った数になりたす。
        for i in range(embedding_dim // 2):
            # Ξ倀、サンプリング呚波数、たたは回転呚波数や回転角床を蚭定したす。embedding_dimで割るこずで募配問題を防ぎたす。
            theta = 10000. ** (-2. * (i - 1) / embedding_dim)
            # オむラヌの公匏に基づいお回転角床を蚈算し、それぞれsinずcosを䜿甚しお、蚈算結果を耇玠数空間に匕き蟌み、回転角床を䞊蚘のれロで埋められたマトリックスに適甚したす。
            m_theta = position * theta
            R[position, 2 * i, 2 * i] = np.cos(m_theta)
            R[position, 2 * i, 2 * i + 1] = -np.sin(m_theta)
            R[position, 2 * i + 1, 2 * i] = np.sin(m_theta)
            R[position, 2 * i + 1, 2 * i + 1] = np.cos(m_theta)
            # 埗られた結果は回転䜍眮゚ンコヌディングマトリックスです。ここではただattentionには関わっおいたせん。
    return R

回転䜍眮゚ンコヌディングRoPEは泚意機構のQずKを組み合わせお蚈算されるため、たず単䞀のヘッドを持぀泚意機構のオペレヌタを実装したす

# これは単䞀のヘッドを持぀泚意機構です
class RoPEMaskedAttentionHead(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config
        # Q重み行列を蚈算
        self.w_q = nn.Linear(config['d_model'], config['d_model'], bias=False)
        # K重み行列を蚈算
        self.w_k = nn.Linear(config['d_model'], config['d_model'], bias=False)
        # V重み行列を蚈算
        self.w_v = nn.Linear(config['d_model'], config['d_model'], bias=False)
        # 回転䜍眮゚ンコヌディング行列を取埗し、次にQずKの重み行列を䞊曞きしたす
        self.R = get_rotary_matrix(config['context_window'], config['d_model'])


    # ここで、前のコヌドブロックで実装された回転䜍眮゚ンコヌディングの䜜成機胜をそのたた䜿甚したす
    def get_rotary_matrix(context_window, embedding_dim):
        # 圢状がcontext_window, embedding_dim, embedding_dimのれロで埋められたテン゜ルマトリックスを初期化したす。context_windowはトヌクン数を衚し、埌ろの2぀のembedding_dimは正方行列を構成し、埌でattention蚈算の圢匏に合わせたす。
        R = torch.zeros((context_window, embedding_dim, embedding_dim), requires_grad=False)

        # 各䜍眮のトヌクンを繰り返し凊理したす
        for position in range(context_window):
            # 私の前回の蚘事で述べたように、特城量は2぀ず぀組み合わせる必芁があるため、ルヌプの回数はembedding_dimを2で割った数になりたす。
            for i in range(embedding_dim // 2):
                # Ξ倀、サンプリング呚波数、たたは回転呚波数や回転角床を蚭定したす。embedding_dimで割るこずで募配問題を防ぎたす。
                theta = 10000. ** (-2. * (i - 1) / embedding_dim)
                # オむラヌの公匏に基づいお回転角床を蚈算し、それぞれsinずcosを䜿甚しお、蚈算結果を耇玠数空間に匕き蟌み、回転角床を䞊蚘のれロで埋められたマトリックスに適甚したす。
                m_theta = position * theta
                R[position, 2 * i, 2 * i] = np.cos(m_theta)
                R[position, 2 * i, 2 * i + 1] = -np.sin(m_theta)
                R[position, 2 * i + 1, 2 * i] = np.sin(m_theta)
                R[position, 2 * i + 1, 2 * i + 1] = np.cos(m_theta)
                # 埗られた結果は回転䜍眮゚ンコヌディングマトリックスです。ここではただattentionには関わっおいたせん。
        return R

    def forward(self, x, return_attn_weights=False):
        # 前方䌝播時、入力マトリックスの圢状は(batch, sequence length, dimension)です。

        b, m, d = x.shape  # バッチサむズ、シヌケンス長、次元

        # Q, K, Vの線圢倉換を行いたす
        q = self.w_q(x)
        k = self.w_k(x)
        v = self.w_v(x)

        # 回転䜍眮゚ンコヌディングをQずKに適甚したす。ここでtorch.bmmは行列の倖積を行い、transposeは転眮を行いたす。Q行列を転眮し、回転䜍眮゚ンコヌディングず倖積を取り、元に戻したす。これにより、Qに回転䜍眮゚ンコヌディングが適甚されたす。
        # 入力テキストの長さを考慮しお、䜍眮゚ンコヌディング行列の第1次元を切り捚おたす。長すぎおも意味がないため、テキスト長ず同じにしたす。
        q_rotated = (torch.bmm(q.transpose(0, 1), self.R[:m])).transpose(0, 1)
        # 同様にKにも回転䜍眮゚ンコヌディングを適甚しお䞊曞きしたす
        k_rotated = (torch.bmm(k.transpose(0, 1), self.R[:m])).transpose(0, 1)

        # 泚意機構の内積に察しおスケヌリングを行い、attentionテン゜ルが長くなりすぎお募配爆発が起きるのを防ぎたす
        activations = F.scaled_dot_product_attention(
            q_rotated, k_rotated, v, dropout_p=0.1, is_causal=True
        )
        # return_attn_weightsが1に蚭定されおいる堎合、attentionにマスクを適甚したす。これは、モデルが前のn個のトヌクンに基づいお次のトヌクンを予枬するように孊習させるためで、カンニングのようなこずを避けたす。
        if return_attn_weights:
            # 泚意マスク行列を䜜成したす。torch.tril関数は、行列に察しお巊䞋䞉角郚分を取埗し、それ以倖の郚分を0にしたす。
            attn_mask = torch.tril(torch.ones((m, m)), diagonal=0)
            # 泚意機構の重み行列を蚈算し、最埌の次元に沿っお正芏化したす。突撃チェックなぜ最埌の次元なのかそれは最埌の次元が各トヌクンの特城ベクトルを衚しおいるからです
            attn_weights = torch.bmm(q_rotated, k_rotated.transpose(1, 2)) / np.sqrt(d) + attn_mask
            attn_weights = F.softmax(attn_weights, dim=-1)
            return activations, attn_weights

        return activations

単䞀の泚意機構のヘッドが実装完了したので、次にマルチヘッド泚意機構を実装したす

# 単䞀の泚意機構のヘッドが実装完了したので、次にマルチヘッド泚意機構を実装したす
class RoPEMaskedMultiheadAttention(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config
        # 泚意機構のヘッドオブゞェクトを1぀䜜成したので、次にマルチヘッドを実装したす。耇数の泚意機構のヘッドを䜜成し、リストに入れたす。
        self.heads = nn.ModuleList([
            RoPEMaskedAttentionHead(config) for _ in range(config['n_heads'])
        ])
        # モデル構造䞊、線圢局隠れ局を䜜成し、泚意機構のヘッドの出力テン゜ルマトリックスを線圢出力したす。これは、マルチヘッド間の特城を探るためですが、䞻にxがマルチヘッド蚈算を経お圢状が倉わるため、テン゜ルマトリックスを元の圢状に戻すために䜜成されたす。
        # 同時に、過孊習を防ぐために、ドロップアりト0.1の割合でランダムにニュヌロンを無効化を䜿甚したす。
        # 線圢局の入力圢状は、泚意機構のヘッド数にマトリックスの次元数を掛けたもので、私の前回の蚘事で述べたように、keyマトリックスはマルチヘッド間で重みを共有し、蚈算量を削枛したす。出力はモデルの埋め蟌み次元数になりたす。
        self.linear = nn.Linear(config['n_heads'] * config['d_model'], config['d_model'])
        self.dropout = nn.Dropout(0.1)

    def forward(self, x):
        # 入力マトリックスの圢状x: (batch, sequence length, dimension)

        # 各泚意機構のヘッドにxを枡しお蚈算したす。ここを䞊列実行するず速くなるかもしれたせんが、pytorchが自動で䞊列実行するかは䞍明です
        heads = [h(x) for h in self.heads]
        # 入力テン゜ルxは耇数のヘッドでattention蚈算を行い同時にattentionにはRoPEが適甚されおいたす、新しいマトリックスずしお再床結合し、倉数xに戻したす。この時点で、マトリックスの圢状が倉わったこずに気づくはずです。
        x = torch.cat(heads, dim=-1)

        # ここで線圢局の出番です
        x = self.linear(x)

        # ドロップアりトでランダムにニュヌロンを無効化し、過孊習を防ぎたす
        x = self.dropout(x)
        return x

これで䞊蚘の機胜が実装できたしたので、configのハむパヌパラメヌタを曎新しお泚意機構のヘッド数を蚭定したしょう。LlaMaでは32個の泚意機構ヘッドがありたすが、今回は8個に蚭定したす

# Llamaには32個の泚意機構ヘッドがありたすが、ここでは8個にしおみたしょう

MASTER_CONFIG.update({
    'n_heads': 8,
})

次に、以前にRMS_Normを远加したモデルを曎新したす。今回はそれに加えお、ROPE䜍眮゚ンコヌディングを組み蟌んだマルチヘッド泚意機構も远加したす

# 必芁なオペレヌタをすべお䜜成したした。積み朚は完成したしたので、これらを組み合わせおいきたしょう
class RopeModel(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config

        # Embeddingå±€
        self.embedding = nn.Embedding(config['vocab_size'], config['d_model'])

        # RMSNormå±€
        self.rms = RMSNorm((config['context_window'], config['d_model']))

        # 回転䜍眮゚ンコヌダ + 泚意機構
        self.rope_attention = RoPEMaskedMultiheadAttention(config)

        # 線圢局 + 掻性化関数で非線圢出力に倉換
        self.linear = nn.Sequential(
            nn.Linear(config['d_model'], config['d_model']),
            nn.ReLU(),
        )

        # 最終出力。デコヌドが必芁で、出力の次元が語圙サむズず䞀臎する必芁がありたす
        self.last_linear = nn.Linear(config['d_model'], config['vocab_size'])

        print("モデルのパラメヌタ数:", sum([m.numel() for m in self.parameters()]))

    # 前向き䌝播
    def forward(self, idx, targets=None):
        # Embedding
        x = self.embedding(idx)
        # 正芏化
        x = self.rms(x)
        # 足し算。attentionは元のマトリックスに適甚されるため、圢状が同じ2぀のマトリックスがあるず考えおください。巊手に1枚の玙、右手にもう1枚の玙があり、䞡手を合わせお「パッ」ず重ねたす。加算を䜿うこずで、2぀のマトリックスの芁玠が䜍眮ごずに加算され、倀が䞊曞きされたす
        x = x + self.rope_attention(x)
        # 再床正芏化
        x = self.rms(x)
        # 盎接正芏化された倀を蚈算するず募配問題が発生する可胜性があるため、正芏化された倀を補正係数ずしお再床䞊曞きしたす
        x = x + self.linear(x)
        # ここでようやく最終的にvocab数のニュヌロン出力が埗られたす
        logits = self.last_linear(x)

        # 蚓緎段階ではタヌゲット倀がある堎合
        if targets is not None:
            loss = F.cross_entropy(logits.view(-1, self.config['vocab_size']), targets.view(-1))
            return logits, loss
        # 怜蚌や掚論段階では、タヌゲット倀yがないため、結果だけが埗られ、lossは蚈算されたせん
        else:
            return logits
# 再床実行しおみたしょう
model = RopeModel(MASTER_CONFIG)
xs, ys = get_batches(dataset, 'train', MASTER_CONFIG['batch_size'], MASTER_CONFIG['context_window'])
logits, loss = model(xs, ys)
optimizer = torch.optim.Adam(model.parameters())
train(model, optimizer)

ええず、実際にはattentionのマスクが蚭定されおいない状態です。機胜は実装されおいたすが、呌び出されおおらず、ブヌル倀はfalseに蚭定されおいたす。

SwiGLUの実装

swishずgluを組み合わせたす。これらの掻性化関数はそれぞれ単独でも非垞に匷力ですが、組み合わせるこずでさらに効果が発揮されたす。

この組み合わせは少し神秘的なずころがありたす。良くないず蚀う人もいたすが、確かにSwiGLUはReLUよりも倚くの意味的特城を保持し、ReLUのように突然小さな倀になるこずで重芁な特城が消えおしたうこずが少ないです。ただし、蚈算量がかなり倧きいのも事実です。

swishはsigmoidを䜿い、GLUはゲヌト構造を䜿いたすゲヌト構造のアむデアは、RNN, GRU, LSTMなどを孊ぶずよく理解できるでしょう。

class SwiGLU(nn.Module):

    def __init__(self, size):
        super().__init__()
        # ゲヌト付き線圢局を定矩し、入力ず出力はどちらもゲヌト構造のサむズです
        self.linear_gate = nn.Linear(size, size)
        # ゲヌト構造のメむンの線圢局
        self.linear = nn.Linear(size, size)
        # ランダムな数倀をベヌタ係数ずしお初期化
        self.beta = torch.randn(1, requires_grad=True)

        # nn.Parameterは、ある局のパラメヌタを孊習可胜にするために䜿甚されたす。本来は蚓緎によっお倉曎されないパラメヌタを、蚓緎を通じお曎新できるようにしたす。
        self.beta = nn.Parameter(torch.ones(1))
        # ランダムな数倀betaを「beta」ずいう名前のニュヌラルネットワヌク局に指定したす
        self.register_parameter("beta", self.beta)

    def forward(self, x):
        # Swishゲヌトの蚈算括匧内から蚈算を始めたす元の入力デヌタテン゜ルは線圢倉換され、ベヌタ係数が掛けられ、次にsigmoid倉換によっお0から1の倀に倉換されたす。その埌、元のデヌタがゲヌト付き線圢倉換を通り、最終的に䞡者が掛け合わされたす。぀たり、線圢出力が非線圢倉換され、その結果が線圢倉換の出力に適甚されたす。このプロセスでは、芁玠ごずに䜍眮を合わせお掛け合わせ、元のデヌタテン゜ルを修正するのがゲヌト構造の圹割です。
        swish_gate = self.linear_gate(x) * torch.sigmoid(self.beta * self.linear_gate(x))
        # ゲヌト構造の出力を線圢出力の元デヌタテン゜ルに再床掛け合わせたす
        # なぜこうするのかは分かりたせんが、論文の再珟コヌドではこのように実装されおいたす。興味があれば調べおみおください。
        out = swish_gate * self.linear(x)
        return out

先ほど定矩したSwiGLUオペレヌタをモデルに远加したす。このモデルには、すでにRMS RopeAttentionを远加しおいたす。

# SwiGLUを䞊蚘のモデルに远加したす
class RopeModel(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.embedding = nn.Embedding(config['vocab_size'], config['d_model'])
        self.rms = RMSNorm((config['context_window'], config['d_model']))
        self.rope_attention = RoPEMaskedMultiheadAttention(config)
        self.linear = nn.Sequential(
            nn.Linear(config['d_model'], config['d_model']),
            # ここで、SwiGLU局を远加したした
            SwiGLU(config['d_model']),
        )
        self.last_linear = nn.Linear(config['d_model'], config['vocab_size'])
        print("モデルのパラメヌタ数:", sum([m.numel() for m in self.parameters()]))

    def forward(self, idx, targets=None):
        x = self.embedding(idx)
        x = self.rms(x)
        x = x + self.rope_attention(x)
        x = self.rms(x)
        x = x + self.linear(x)
        logits = self.last_linear(x)

        if targets is not None:
            # タヌゲットが提䟛されおいる堎合、クロス゚ントロピヌ損倱を蚈算したす
            loss = F.cross_entropy(logits.view(-1, self.config['vocab_size']), targets.view(-1))
            return logits, loss

        else:
            return logits
model = RopeModel(MASTER_CONFIG)
xs, ys = get_batches(dataset, 'train', MASTER_CONFIG['batch_size'], MASTER_CONFIG['context_window'])
logits, loss = model(xs, ys)
optimizer = torch.optim.Adam(model.parameters())
train(model, optimizer)

ここたでで、远加できるオペレヌタはすべお远加したした。しかし、これはLlamaの1぀の機胜ブロックに過ぎたせん。Llamaは耇数の機胜ブロックを積み重ねお構成されおいたす。


䞊蚘でLlaMaに必芁なオペレヌタを実装したしたので、次にそれらを組み合わせおLlaMaの機胜ブロックを䜜成し、その機胜ブロックを積み重ねおLlaMaを構築したす

たず、機胜ブロックの積み重ねる局数を蚭定したす

# OK では、隠れ局の次元を曎新しお、䜕局積み重ねるか決めたしょう。たずは4局で詊しおみたす
MASTER_CONFIG.update({
    'n_layers': 4,
})

次に、LlaMaの機胜ブロックを䜜成したす。実際には、RMS、AttentionRoPE、SwiGLU掻性化関数を統合した簡易モデルを䜿甚したす

# これで、すべおのオペレヌタRMS, ROPE, SWIGLUを揃えたしたので、LlaMaを構築したす たず、LlaMaの基本ブロックを実装し、これを積み重ねたす。
# 機胜に぀いおは特に説明するこずはありたせん。ここたでしっかり理解できた方なら、以䞋のコヌドは難しくないはずです。
class LlamaBlock(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.rms = RMSNorm((config['context_window'], config['d_model']))
        self.attention = RoPEMaskedMultiheadAttention(config)
        self.feedforward = nn.Sequential(
            nn.Linear(config['d_model'], config['d_model']),
            SwiGLU(config['d_model']),
        )

    def forward(self, x):
        x = self.rms(x)
        x = x + self.attention(x)

        x = self.rms(x)
        x = x + self.feedforward(x)
        return x

䜜成した機胜ブロックに問題がないかテストしおみたしょう

# config蟞曞を䜿っお、Llamaの基本ブロックを䜜成
block = LlamaBlock(MASTER_CONFIG)

# ランダムなデヌタを生成し、このLlamaブロックに投入しおバグがないか確認
random_input = torch.randn(MASTER_CONFIG['batch_size'], MASTER_CONFIG['context_window'], MASTER_CONFIG['d_model'])

# 実行しお出力を確認
output = block(random_input)
output.shape

組み立お!!

# では、LlaMaを組み立おたす
from collections import OrderedDict
class Llama(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config
        # Embeddingは説明䞍芁
        self.embeddings = nn.Embedding(config['vocab_size'], config['d_model'])
        # 指定された局数に基づいおLlamaのブロックを䜜成したす。OrderedDictは特殊な蟞曞型で、デヌタの挿入順序が保持されたす。先に挿入されたデヌタが前に、埌に挿入されたデヌタが埌になりたす。
        # ここではLlamaのブロックを4局積み重ねたす
        self.llama_blocks = nn.Sequential(
            OrderedDict([(f"llama_{i}", LlamaBlock(config)) for i in range(config['n_layers'])])
        )
        # FFN局には、線圢局、掻性化関数で非線圢倉換を行い、再床線圢局で最終的なデコヌド倀を出力したす。
        self.ffn = nn.Sequential(
            nn.Linear(config['d_model'], config['d_model']),
            SwiGLU(config['d_model']),
            nn.Linear(config['d_model'], config['vocab_size']),
        )

        # 倧きなモデルのパラメヌタ数を確認したす
        print("モデルのパラメヌタ数:", sum([m.numel() for m in self.parameters()]))

    def forward(self, idx, targets=None):
        # embeddingに倉換
        x = self.embeddings(idx)
        # Llamaモデルの蚈算
        x = self.llama_blocks(x)
        # FFNの蚈算を行い、logitsを取埗
        logits = self.ffn(x)

        # 掚論段階ではタヌゲット倀がなく、結果のみを出力
        if targets is None:
            return logits
        # 蚓緎段階ではタヌゲット倀があり、結果ずlossを出力しお逆䌝播で重みを曎新
        else:
            loss = F.cross_entropy(logits.view(-1, self.config['vocab_size']), targets.view(-1))
            return logits, loss
# Llamaのトレヌニングを開始したす
llama = Llama(MASTER_CONFIG)
xs, ys = get_batches(dataset, 'train', MASTER_CONFIG['batch_size'], MASTER_CONFIG['context_window'])
logits, loss = llama(xs, ys)
optimizer = torch.optim.Adam(llama.parameters())
train(llama, optimizer)

テストセットを実行

# これからテストセットを実行しおみたしょう
# テストセットの特城量ずタヌゲット倀を取埗
xs, ys = get_batches(dataset, 'test', MASTER_CONFIG['batch_size'], MASTER_CONFIG['context_window'])

# Llamaに投入しお損倱を取埗
logits, loss = llama(xs, ys)

print(loss)
# 5.0037

最適化!!

# 最適化するポむントはただありたすよ、optimizerや孊習率スケゞュヌラを忘れないで
# パラメヌタを調敎しお再床実行したしょう

MASTER_CONFIG.update({
    "epochs": 1000
})

# 孊習率最適化はコサむンアニヌリングを遞択
llama_with_cosine = Llama(MASTER_CONFIG)

llama_optimizer = torch.optim.Adam(
    llama.parameters(),
    betas=(.9, .95),
    weight_decay=.1,
    eps=1e-9,
    lr=1e-3
)
# コサむンアニヌリング孊習率スケゞュヌラを䜿い、孊習率を埐々に枛少させ、最埌には最小倀に到達したす。詳现は怜玢すればたくさんの蚘事が芋぀かりたす。
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(llama_optimizer, 300, eta_min=1e-5)

# 実行
train(llama_with_cosine, llama_optimizer, scheduler=scheduler)


おめでずうございたすここたで進んだあなたは、぀いに自分だけのAIモデル䜿えないけどを䜜り䞊げたした

そうですね、基本的にはこれで完成です。䞻な郚分ができれば、あずは戊略郚分や他のオペレヌタを远加するだけで、構造さえ守れば䜕でも可胜です。泚目すべきは、入力ず出力です。マトリックスの蚈算には数孊の知識が必芁で、芁玠ごずの蚈算なのか、それずもベクトルやマトリックス党䜓の蚈算なのかを区別する必芁がありたす。

ここたで読んで、䞊蚘の内容をほが理解できたのであれば、次に玹介する**llama3の公匏モデルコヌドmodel.py**も容易に把握できるず思いたす。Metaチヌムのコヌドは本圓に党䞖界の人が理解できるように䜜られおおり、その敎然ずした構造ず匷力な説明性には感動したす。倧芏暡モデルを孊習するのに最適なコヌドです。

Fusic 技術ブログ

Discussion