Transformerを実装して少し理解した気になる会(その1)
0. なぜTransformerを学ぶのか
なんかLLMを使うだけってなんかもったいない、せっかくなら仕組みを知っておくと一つ賢くなれると思うから
sushichan044.iconいい話
思ったより単純な仕組みであるということを知れる
それぞれの機構がどのような役割を果たしているか(なんとなく)知れる
その先に新しくモデルを組んだりするときに、したいことに合わせてモデルを組めるようになる...かも
1. Transformerに使われている基礎的なモジュール(LinearやEmbeddingなど)について理解して実装する
Transformerに使われているモジュールはすべて基礎的なモジュールで構成されている
というかだいたいのDNNはそこまで複雑なモジュールは入ってない
https://scrapbox.io/files/690d768fa2e2d3585fd8fc02.png https://scrapbox.io/files/690d7712a48a7c378809788f.png
https://scrapbox.io/files/690d77594dc3e23e483f6167.png
max(0, x)はReLUとも呼ばれている
(画像内のFeed Forward = FFN)
https://scrapbox.io/files/690d77a562c837a7f30f44d2.png
(画像内のPositional Encoding = PE)
でてきたモジュールを並べてみる
Softmax
Linear
Embedding
Norm
MatMul
Mask
Concat
それぞれ解説
Softmax
出力値の定義域が [0, 1]
sushichan044.icon 入力ベクトルの各要素を [0, 1] の範囲に変換できてお得ということ?
確率分布か
y-chan.icon確率を表したい場合は[0, 1]だとお得
https://scrapbox.io/files/690d9ac62b3ce177641e9927.png
https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F202772%2F180636ea-eee3-1c11-fd8a-872eb1fd836e.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=7bb874773a1a52b65cdb57497cf0e409 (引用: softmax関数を直感的に理解したい) 主に、最終層の後ろでクラスの最後の分類確率の出力に使われる
実際Transformerの最終層はSoftmaxである
つまり、「このトークン(単語)が次の出力だろう」という確率をTransformerは予測している
y-chan.icon GPT とかだと next token の確率分布に使う
合計が 1 で各要素の値域が [0, 1] である確率分布を next token 予測と呼んでいる
このような、ただただ値を変換する処理を「活性化関数」と呼ぶ
値域を変更したり、特定の値以下を不要な値としてカットしたり
ちなみに、Softmaxをこの式のとおりに実装するとオーバーフローしてNaNが出てしまうので、x_jの最大値を求めて、あらかじめ引いておくことで、数値的安定性をもたせる事ができるらしい
y-chan.icon Copilotに助けられた、いい話
Linear
Affine層や全結合層(Fully-Connected Layer)などとも呼ばれる
https://scrapbox.io/files/690e19fe9f9879e3d534256a.png
計算的には、 x(h)が1x3行列、wが3x2行列 bが1x2行列としたとき、x * w + b = uとなり、計算結果uが1x2行列として得られる
wはウェイト、bはバイアスと一般的に呼ばれる
何を表すか?
ウェイトは入力データが出力にどの程度影響するかを表し、バイアスは入力に依存せず出力を調整するための要素である。
Linear(厳密にはウェイト)によって、データをつなげながら、次元数を調節する
sushichan044.icon 次元数を調整 ( 重要情報 )
入力ベクトルの次元を重み行列で操作する行為を指していそう
bias があると入力が零ベクトルでも零以外を出力できる
Embedding
直訳すると埋め込み(そんなこと聞いてない)
単語などをニューラルネットワークに入力する際、直接単語を入力することはできないので、基本的には単語を前処理する辞書におけるインデックスを入力する場合が多い
このインデックスは単なる整数なので、ニューラルネットワークに直接入力しても意味を学習できない
ウェイトみたいに意味を持つわけではないので
なので、one-hot vectorというものに変換して、その上でLinearにおけるウェイトだけをかけて、その後扱いやすい次元数に変換する
one-hot vectorとは、表したい情報に対応する位置だけを1にし、それ以外の要素をすべて0にすることで情報を表現する手法である。
例えば、 a = [0, 3, 3, 4, 5, 1, 2]、次元数6としたときに、以下のようなone-hot vectorができる
sushichan044.icon 列方向で要素の値に対応する index が 1 になっている、なるほど
データの要素数と同じサイズの単位行列を掛け算する
code:eye.py
>> np.eye(6)
code:python
>> import numpy as np
(縦ベクトルがかけないので横ベクトルにするが、)0は[1, 0, 0, 0, 0, 0]になる
1は[0, 1, 0, 0, 0, 0]
2は[0, 0, 1, 0, 0, 0]...という感じ、もちろん次元数をもっと増やせばもっと膨大な数の単語にも対応できる
というわけでEmbeddingをなんとなく式にすると
e = onehot(x) * w、ここでeは出力(embedding)、onehot()はone-hot vectorを作る関数、xは入力、wはウェイトを表し、たとえ次元数が膨大でも一定の次元数に抑えて扱いやすくする役割を持つ
12/14追記
e = onehot(x) * wを使うと、想像以上にメモリが必要になってしまう場合があるので、e = w[x]みたいな形でいいらしい
Norm
ノーマライズを意味
何を基準にノーマライズするかで名前が変わる
Batch Norm/Layer Normとか
やってる処理としては、平均と分散(標準偏差)を計算して、それをもとにだいたい[-3, 3]の範囲に変換(スケーリング)する(平均を0、分散1となるようにする)
値域をある程度抑えることで、学習の際に使う「勾配」が発散して学習が進まなくなるのを防ぐ
x_mean = mean(x), x_var = var(x)としたとき、x_std = sqrt(x_var)となるので、出力をx_normとすると
x_norm = (x - x_mean) / x_stdとなる
安定性を持たせるため、x_std = sqrt(x_var + epsilon)とされる場合が多い(epsilon = 1e-6などの微小値)
ちなみに、よく深層学習に用いられるノーマライズは更に学習可能なパラメータgammaとbetaを持っている
y = x_norm * gamma + betaとなる
sushichan044.icon Layer Norm の実装時は、 Tensor.var はデフォルトで不偏分散を計算していることに注意が必要
MatMul
ここらへんからモジュールと呼べるか怪しい
普通に行列の掛け算
y-chan.icon 外積と呼ぶべきかも
4x5行列と5x2行列を掛けて4x2行列にするような処理
np.matmul(x, y) もしくは x @ yで書ける
Mask
0/1のフラグみたいなもの
情報として使ってほしくないことには0をかけて情報量をなくせば、その情報に依存しなくできる(できなくする)し、逆に1をかけてそのまま情報を素通りさせればそのまま使えるって感じ
helkun.icon天才的すぎる...
sushichan044.icon とても mask だ
未来の情報にマスクをかければ、過去の情報だけに依存させられる
sushichan044.icon 重要情報そう
Concat
Multi-Head Attentionの内部に使われている
複数の処理を(並列で)したあと、それらの情報をつなぎ合わせるには2つの方法がある
Add: 単純に足し算をする
Concat: それぞれの処理の結果がそのまま残るように、次元数を増やす形で結合する
こんな感じ
code:python
>> np.random.rand(3, 5)
>> a = np.random.rand(3, 5)
>> a
>> a.shape
(3, 5)
>> b = np.random.rand(3, 10)
>> b
array([[0.12917485, 0.53909101, 0.84426061, 0.81538771, 0.64795821,
0.91288811, 0.2027281 , 0.08542709, 0.23381734, 0.28550428],
[0.38518447, 0.24074668, 0.99068404, 0.15476465, 0.83188632,
0.26631188, 0.62610963, 0.1360501 , 0.35012788, 0.37512518],
[0.63688684, 0.3031926 , 0.78998039, 0.71264175, 0.89040817,
0.4456058 , 0.67283662, 0.02846763, 0.32202787, 0.70194772]])
>> b.shape
(3, 10)
>> np.concatenate(a, b, axis=-1) array([[0.76910703, 0.4255553 , 0.53774521, 0.13394228, 0.8093624 ,
0.12917485, 0.53909101, 0.84426061, 0.81538771, 0.64795821,
0.91288811, 0.2027281 , 0.08542709, 0.23381734, 0.28550428],
[0.61024389, 0.21493785, 0.77738803, 0.21459408, 0.43100987,
0.38518447, 0.24074668, 0.99068404, 0.15476465, 0.83188632,
0.26631188, 0.62610963, 0.1360501 , 0.35012788, 0.37512518],
[0.3976669 , 0.05044641, 0.09395755, 0.75249684, 0.99892888,
0.63688684, 0.3031926 , 0.78998039, 0.71264175, 0.89040817,
0.4456058 , 0.67283662, 0.02846763, 0.32202787, 0.70194772]])
>> np.concatenate(a, b, axis=-1).shape (3, 15)
情報をベクトル演算的に混ぜたいのか、別の情報としてそれぞれ意味を持たせて次の層に渡したいかで使い分けをする場合が多い
ちなみに、経験則的に、複雑度やパラメータ数の多くないネットワークではあんまり差はなかった
sushichan044.icon 差とは
y-chan.icon 学習結果への影響
Concatの利点としては、Addと違ってどこかの次元数が異なっていても別にOKである
helkun.icon片方の次元が合ってたらとりあえずOK
sushichan044.icon PPAP だ
y-chan.icon ワロタ
ここまでの総括
深層学習モデルの各モジュール・計算自体は意外と難しいことはしていない
ホントはConvolution(畳み込み)などいろいろあるが、まあ今回は使わないので一旦無視で良さそう
というわけで実装していく
今回使うのはPyTorchというフレームワーク
さっきまでちょこちょこでていたのはNumPyという数値計算向けのフレームワーク
PyTorchはNumPyとの互換性が非常に良く、相互変換ができたり、APIがよく似ていたりする
PyTorchとNumPyの違いとして大きいのは、次元を表すのがdim(PyTorch)かaxis(NumPy)か
あとはどちらかにあってどちらかにない関数とかもあるけど基本的に操作感は一緒
PyTorchは、学習可能なパラメータについてはtorch.nn.Parameterとして書くと定義できる
self.gamma = nn.Parameter(torch.ones(normalized_shape))
sushichan044.icon iteration ごとに最適化されていく値としてマークできる
トピック2は別ページへ