0
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?

LLM攻略 [第一回 Transformer編(2) Q・K・VからAttention scoreを作る]

0
Posted at

Transformer攻略(2) Q・K・VからAttention scoreを作る

はじめに

こちらは第二回目です。

前回、AIに仕事を取られないエンジニアになるぞ、という気持ちでTransformerを触り始めました。
前回は、文章をtokenに分けて、IDにして、Embeddingして、位置情報を足すところまでやりました。

全体の流れでいうと、ここまでです。

文章
↓
トークン化
↓
トークンIDに変換
↓
単語Embeddingに変換
↓
位置Embeddingを足す
↓
Query・Key・Valueを作る
↓
Self-Attention
↓
Multi-Head Attention
↓
Feed Forward
↓
Transformer Blockを通す
↓
次のトークンを予測する
↓
小さな文章生成モデルになる

今回はその続きで、Attentionに入ります。

正直、ここから急に難しくなりました。
Q, K, V とか、q @ k.transpose(-2, -1) とか、最初に見たときは「何をやっているんだこれは」という感じでした。

なので今回は、完璧な理論理解というより、まずはshapeと処理の流れを追って、Attentionで何が起きているのかを掴むことを目標にしました。


前回の振り返り

前回やったことをかなりざっくり書くと、以下です。

文章はそのままだとモデルが扱えないので、まず数値にします。

例えば今回のコードでは、以下の文を使いました。

I like small models

これをtoken IDにして、Embeddingして、位置情報を足すと、最終的にこういうshapeになります。

positioned: (1, 4, 8)

意味はこうです。

1 = batch数
4 = token数
8 = 1 tokenあたりのベクトル次元

今回のAttentionは、この positioned を入力として使います。


今回やること

今回から、Scaled Dot-Product Attentionを実装します。

この記事では、その前半として、QKVを用意し、Attention scoreを作るところまで進めます。

名前は強そうですが、まずはこう考えました。

各tokenが、他のtokenをどれくらい見るかを決める処理

流れはこんな感じです。

一言でいうと、

QとKで「どれを見るか」を決める
決まった割合でVを混ぜる

です。

この記事では、図のうちscoresを作るところまで、ひとつずつ見ていきます。


Q, K, Vって何

最初に分からなかったのが Q, K, V です。

名前はそれぞれ以下です。

記号 名前 ざっくりした役割
Q Query 自分が何を探しているか
K Key 自分がどう見つけられたいか
V Value 見つけられたときに渡す中身

検索で考えると少し分かりやすかったです。

Q = 検索欄に入れる言葉
K = 検索対象についたタグ
V = 実際に読む本文

まず QK を比べて、どの情報を見るかを決めます。
そのあと、決まった割合で V の中身を受け取ります。

今回の実装では、理解を優先してかなり単純化しています。

q = positioned
k = positioned
v = positioned

本来はLinear層を通して Q, K, V を別々に作るようですが、今回はAttentionの計算を追いたいので、同じTensorに別名をつけています。

ここは最初、「同じものを代入しているだけなら意味あるのか?」と思いました。
ただ、後続の計算では役割が違います。まずは名前と役割に慣れることを優先し、今回やりたいことからもずれるため、単純化します。


scoreを作る

次に、QK を比べてscoreを作ります。

コードはこれです。

d_k = q.size(-1)
scores = q @ k.transpose(-2, -1) / math.sqrt(d_k)

さっぱりわかりませんでした。
なので、まずは意味よりshapeから見ます。

q:             (1, 4, 8)
k:             (1, 4, 8)
k.transpose:   (1, 8, 4)

q @ k.T:       (1, 4, 4)

4 x 4 になるのは、4個のtokenが、それぞれ4個のtoken全部と比べられるからです。

                 key
              I   like  small  models
query I       ?     ?      ?       ?
query like    ?     ?      ?       ?
query small   ?     ?      ?       ?
query models  ?     ?      ?       ?

この ? がscoreです。

つまりscoreは、

各token同士の「関係ありそう度」の表

みたいなものだと理解しました。

図にするとこうです。

なぜ sqrt(d_k) で割るのか

ここも最初はよく分かりませんでした。

q @ k.T だけでもscoreは作れます。
では、なぜわざわざ sqrt(d_k) で割るのか。

自分の理解では、scoreが大きくなりすぎるのを防ぐためです。

内積は、ベクトルの次元数が増えるほど、足し合わせる数も増えます。
そうするとscoreが大きくなりやすいです。

scoreが大きすぎると、次に出てくるsoftmaxがかなり極端になります。

[0.9999, 0.0000, 0.0000, 0.0001]

みたいになると、ほぼ1つのtokenしか見なくなります。
それを少しならすために、sqrt(d_k) で割っています。

ここはまだ完全に腹落ちしているというより、

softmaxが極端になりすぎるのを防ぐ調整

くらいの理解です。


実装

今回のコードです。

前半は前回と同じで、token ID、Embedding、位置情報を作っています。
後半でQKVを用意し、Attention scoreを計算します。

import math

import torch
from torch import nn


vocab = {
    "<pad>": 0,
    "<unk>": 1,
    "I": 2,
    "like": 3,
    "small": 4,
    "models": 5,
}

sentence = "I like small models"
tokens = sentence.split()
token_ids = [vocab[token] for token in tokens]
input_ids = torch.tensor([token_ids])

embedding_dim = 8
embedding = nn.Embedding(
    num_embeddings=len(vocab),
    embedding_dim=embedding_dim,
)
embedded = embedding(input_ids)

max_len = 20
position = torch.arange(max_len, dtype=torch.float32).unsqueeze(1)
div_term = torch.exp(
    torch.arange(0, embedding_dim, 2).float()
    * (-math.log(10000.0) / embedding_dim)
)

pe = torch.zeros(max_len, embedding_dim)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)

positioned = embedded + pe[:, :embedded.size(1)]

q = positioned
k = positioned
v = positioned

d_k = q.size(-1)
scores = q @ k.transpose(-2, -1) / math.sqrt(d_k)

print("tokens:", tokens)
print("positioned.shape:", positioned.shape)
print("q.shape:", q.shape)
print("k.transpose.shape:", k.transpose(-2, -1).shape)
print("scores.shape:", scores.shape)

実行すると、shapeはこうなりました。

tokens: ['I', 'like', 'small', 'models']
positioned.shape: torch.Size([1, 4, 8])
q.shape: torch.Size([1, 4, 8])
k.transpose.shape: torch.Size([1, 8, 4])
scores.shape: torch.Size([1, 4, 4])

今回ハマったところ

今回ハマったところを、自分用にもう一回短くまとめます。

scores のshapeがなぜ (1, 4, 4) なのか

最初、positioned(1, 4, 8) なので、scoreも (1, 4, 8) っぽくなるのかなと思っていました。

でもscoreはtoken同士の関係表です。

4 token が 4 token を見る

ので、4 x 4 になります。

sqrt(d_k) で割る理由

ここはまだ完全には腹落ちしていません。
ただ、今の理解では、softmaxが極端になりすぎるのを防ぐための調整です。

こういう「まだ怪しいけど、まずはこの理解で進む」ポイントが、Transformerには結構ありそうです。


全体まとめ

今回の流れをまとめると、以下です。

shapeだけで追うと、こうです。

positioned: (1, 4, 8)
q:          (1, 4, 8)
k:          (1, 4, 8)
k.T:        (1, 8, 4)
scores:     (1, 4, 4)

今回の理解を一言でまとめるなら、

QとKを使って、各token同士の「関係ありそう度」を表すscoreを作る

です。

scoreはまだ生の点数です。
次回はsoftmaxで「各tokenをどれくらい見るか」という割合に変換し、その割合でVを混ぜます。

0
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
0
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?