概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。2020年代、文章といえばTransformer。ということでTransformer Encoderを利用したテキスト分類いわゆるBERT風のネットワークでテキスト分類の演習をやってみました。
今回は注意機構に登場する注意行列 (attention matrixやattention weightsと呼ばれるもの) を可視化してみたいと思います。論文や解説サイトで登場する図、面倒そうだけど、どうやって描くの![]()
演習用のファイル
- データのファイル: data_90.zip
- モデル: data_90_bert_type.model.モデルファイルは第22回にて学習・保存したファイルでOK
- コード: sample_23.ipynb
1. 注意行列 (Attention Matrix)
テキスト分類を行うとき、埋め込み層を用いて文章を行列化することから始めました。
-
トークナイズ(IDベクトル化)
文章を単語やサブワード単位に分割し、各要素をIDに変換- 例:"アカウント/を/作り/たい" → [46,45,77,16]
-
埋め込み(Embedding)
IDを意味を持つベクトル(分散表現)に変換- 各単語をベクトル(分散表現ベクトル)で表現する
-
文章の行列化:
分散表現ベクトルを利用して文章を行列で表現- 文章を行列(分散表現行列)で表現する
- ベクトル表現された単語を文の単語順に並べることで「単語数 × 分散表現ベクトル」の行列が構成される
分散表現行列同士の掛け算をすることで単語同士の関連度を求めたいという発想が注意機構と呼ばれるものになります。関連度を表す行列が注意行列 (Attention Matrix)・注意重み (Attention Weights)と呼ばれています。
1.1. 注意行列ってどこ?
入力文章を分散表現によって行列化したものをXとします。このXに対して、3つの異なる全結合層(線形変換)を適用することで、以下の3つの行列を生成します。
- $Q$(Query: クエリ)= Linear_q($X$) = $XW^Q$
- $K$(Key: キー)= Linear_k($X$) = $XW^K$
- $V$(Value: 値)= Linear_v($X$) = $XW^V$
行列$Q$と転置した行列$K^T$を掛け算することで、(系列長 × 系列長) のサイズを持つ行列が得られます。行列の各要素$(i, j)$は、$i$番目の単語のクエリと$j$番目の単語のキーとの内積を表しており、両者の関連性の強さを数値化したものになります。
この行列に対してsoftmax関数を適用し、各行の要素を確率分布に変換したものが 注意行列(Attention Matrix) となります。$\text{softmax}(QK^T)$と表現できます1。
注意行列の各要素は0から1の値を取り、各行の合計は1になります。行列ですが、学習される重みでもあるので、 Attention Weights とも呼ばれます。
注意行列は、「各行に対応する単語が、各列に対応する単語に対して、どれくらいの注意(重要度)を割り当てているか」を確率として表現していると解釈できます。例えば、注意行列の$(i, j)$要素が大きい値を持つ場合、$i$番目の単語は$j$番目の単語と強い関連性があることを意味します。
図1では、黄色が<bos>から他の単語への関連度を、ピンク色が「登録」から他の単語への関連度を表しています。系列長×系列長の行列(図1では5×5行列)が注意行列の基本形となります。
1.2. マルチヘッド (Multi Head) と注意行列
マルチヘッドアテンション(MultiHead Attention)に移ります。マルチヘッドアテンションは、複数の異なる観点から注意機構を並列に適用することで、より豊かな表現を獲得する手法です。
PyTorchでのマルチヘッドアテンションにおける「マルチヘッド」とは、Q、K、V行列の列方向(分散表現の次元)をヘッドの数で分割するイメージです。詳しくは第22.5回の内容【MultiHeadAttention】を参考にしてください。
- $Q=[Q_1, Q_2]$
- $K=[K_1, K_2]$
- $V=[V_1,~V_2]$
head $i$という言葉を、$\text{Attention}(Q_i, K_i, V_i)$ そのものではなく、$Q$、$K$、$V$ を分割して得られる $i$番目に対応する$Q_i$や$K_i$、$V_i$の意味で用います。
図2を例に簡単な解説を![]()
Q、K、Vがそれぞれ (系列長5 × 特徴量次元4) の行列で表現されているとします。ここでヘッド数を2に設定した場合、各行列は列方向に2分割されます。
- head1:左側2列(黄色部分)→ 5×2 の行列
- head2:右側2列(緑色部分)→ 5×2 の行列
5×2の小さな行列を利用して、各ヘッド毎に注意行列を求めるのがマルチヘッドアテンションの注意行列となります。
head1とhead2それぞれについて、図3のように個別に注意行列 (5×5の2種類の行列) が計算されます。これにより、各ヘッドは異なる特徴量で、異なる種類の単語間の関係性を捉えることができそうです。複数のヘッドを用いることで、単一の注意機構では捉えきれない多様な文脈情報を、並列に学習・表現できるようになりそうです。これがTransformerの高い表現力の源泉の一つとなっていると考えられています。
例えば、head1は「品詞と位置関係の情報に注目(パステル調の5×5行列)」、head2は「意味的な情報に注目(ビビッド調の5×5行列)」するなど異なる役割を学習することが可能と考えられています2。
Transformer Encoderの注意行列は、簡単に言うなら 各ヘッド毎の $\text{softmax}(QK^T)$ になります。注意行列にV行列を掛け算したり、……というTransformerの解説は他のわかりやすい記事や解説本に丸投げ3![]()
2. 注意行列の抽出と可視化
各ヘッド毎の $\text{softmax}(QK^T)$に相当する部分を抽出してヒートマップで表現してみたいと思います。
2.1. 注意行列の値を抽出
前回のテキスト分類ではPyTorch既存のTransformerEncoderネットワークを利用してネットワークを記述しました。
encoder_layer = nn.TransformerEncoderLayer(d_model = ...)
transformer_encoder = nn.TransformerEncoder(encoder_layer,...)
この形から注意行列を直接抽出するのが思いの外大変というかうまくできなかった〜
そこで、注意行列を戻り値として得られるように、nn.TransformerEncoderLayerクラスを継承したCustomEncoderLayerクラスと、nn.TransformerEncoderを継承したCustomTransformerEncoderクラスを作成する方法にしました4。
attention weightsを直接取得できる場合は、2.3. 行列を色付けしてみる(可視化)へGO。
PyTorchの公式の実装
- TransformerEncoderLayerのコードが参考になります。
基本的なアイディアはTransformerEncoderLayerやTransformerEncoderを継承してforwardの部分だけ注意行列を取得できるように上書きするだけです。
class CustomEncoderLayer(nn.TransformerEncoderLayer):
"""注意行列を返すように変更したTransformerEncoderLayer"""
def forward(self, src, src_mask=None, src_key_padding_mask=None):
# (1) MultiheadAttention (Self-Attention)
src2, attn_weights = self.self_attn(
src,
src,
src,
attn_mask=src_mask,
key_padding_mask=src_key_padding_mask,
need_weights=True, # 注意行列を使うのでTrueに変更
average_attn_weights=False # ヘッド毎の注意行列を取得するとします。このXに対して、3つの異なる全結合層(線形変換)を適用することで、以下の3つの行列を生成しますためFalseに変更
)
# Add & Norm
src = src + self.dropout1(src2)
src = self.norm1(src)
# Feed Forward
src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
# Add & Norm
src = src + self.dropout2(src2)
src = self.norm2(src)
# (2) 注意行列を一緒に返すように調整
return src, attn_weights
説明メモ
既存のTransformerEncoderLayerにあるMultiheadAttentionの注意行列のオプションをTrueにして、returnに重み行列を追加するだけです。変更点は3箇所です。
- (1) 既存のTransformerEncoderLayerで利用されているMultiheadAttentionクラスのインスタンス部分のオプションを変更します5。
- 注意行列を利用するので
need_weights=Trueとします。公式の実装transformer.pyの中でFalseになっているっぽいのでTrueに変更します。 - ヘッド毎に注意行列を求めたいので
average_attn_weights=Falseとします。デフォルト値はTrueですべてのヘッドの平均が計算されるようになっています。
- 注意行列を利用するので
- (2)
return xをreturn src, attn_weightsと注意行列も含める形に変更します。
TransformerEncoderLayerを重ねて利用するためのクラスであるTransformerEncoderクラスにも注意行列を抽出する仕組み追加します。
class CustomTransformerEncoder(nn.TransformerEncoder):
"""CustomEncoderLayerを受け取り、注意行列も返すように変更したEncoderクラス"""
def forward(self, src, mask=None, src_key_padding_mask=None):
output = src
all_attn_weights = [] # (1) この部分を追加 注意行列のリスト
# (2) encoder layerの繰り返し部分
for mod in self.layers:
output, attn_weights = mod(
output,
src_mask=mask,
src_key_padding_mask=src_key_padding_mask
)
all_attn_weights.append(attn_weights)
# normレイヤーがある場合
if self.norm is not None:
output = self.norm(output)
# (3) 最終層の出力と、全レイヤーの注意行列のリストを返す
return output, all_attn_weights
説明メモ
公式実装のレイヤーを繰り返す部分に注意行列を追加する形になります。修正部分は3箇所となります。
- (1) レイヤー毎の注意行列を格納するリストを作成
- (2) 公式実装のfor mod in self.layersの部分に追記する形です。注意行列のattn_weightsとリストへの追加を追記します。
- (3)
return outoutをreturn output, all_attn_weightsに変更します。
加筆修正したCustomEncoderLayerとCustomEncoderを使い、前回のネットワークを書き換えます。
# 初期設定
WORDS = 146 # 単語数
SEQ_LEN = 11 # x.shape[1]、入力するIDベクトルの長さ
D_MODEL = 16 # 分散表現ベクトルの次元
CLASSES = 3 # 分類数
class DNN(nn.Module):
def __init__(self, pad_token_id: int=3): # ここでpadding token idを指定デフォルトは0になっている
super().__init__()
self.pad_token_id = pad_token_id
# (1) トークン埋め込み <pad>をpad_token_id=3に設定
self.token_embedding = nn.Embedding(num_embeddings=WORDS, embedding_dim=D_MODEL, padding_idx=self.pad_token_id)
# (2) 学習可能な位置埋め込み(0〜max_len-1)
self.pos_embedding = nn.Embedding(num_embeddings=SEQ_LEN, embedding_dim=D_MODEL)
# (3) 変更箇所1 Transformer Encoder
encoder_layer = CustomEncoderLayer(
d_model=D_MODEL, # 分散表現ベクトルの次元
nhead=4, # multi head attentionのheadの数
dim_feedforward=32, # 中間層の次元数(d_modelの4倍程度が多いみたい)
dropout=0.1,
batch_first=True, # (batch, seq_len, d_model) で扱えるように
)
self.transformer_encoder = CustomTransformerEncoder(encoder_layer,num_layers=6)
# (4) 文ベクトル → クラス数
self.fc = nn.Linear(in_features=D_MODEL, out_features=3)
def forward(self, x):
# (5) TransformerEncoderの<pad>用マスク
# pad_token_id (<pad>になる部分)が True になるように mask を作成する
src_key_padding_mask = (x == self.pad_token_id)
# ---- 埋め込み ----
# トークン埋め込み
tok_emb = self.token_embedding(x) # (batch, seq_len=11, d_model=16)
# 位置埋め込み
# ハードコードされている「11」は何を表しているの?
# 文の長さが11。0〜10までの数値で単語の位置をあらわしています。
pos_emb = self.pos_embedding(torch.arange(11, device=x.device)) # (seq_len=11, d_model=16)
# (6) 分散表現行列=トークン埋め込み + 位置埋め込み
x = tok_emb + pos_emb.unsqueeze(0) # (batch, seq_len=11, d_model=16)
# (7) Transformer Encoder
h, all_attn_weights = self.transformer_encoder(x, src_key_padding_mask=src_key_padding_mask)
# (8) <BOS>トークン(先頭)に情報を集約
pooled = h[:, 0, :] # [batch, d_model]
y = self.fc(pooled) # [batch, num_labels=3]
# 変更箇所2 注意行列を追加
return y, all_attn_weights
model = DNN()
model.to(device)
説明メモ
- (1) トークンの埋め込み:単語を分散表現ベクトルへ変換します。
- (2) 位置の埋め込み:単語の出現位置の番号を分散表現ベクトルへ変換します。
- (3) CustomEncoderLayerとCustomEncoderを利用してネットワークを作成します。
- (4) 分類問題なので最後に全結合層を付け加えます。
- (5) <pad>の部分を利用しないようにマスクをつけます。
- (6) 単語の分散表現と単語の位置の分散表現を足し算します。
- (7) ここがポイント!
all_attn_weightsという注意行列のリストを指定します。 - (8) 文頭<bos>の情報だけを利用してFCへ入力します。
-
return y, all_attn_weights: 戻り値に attention weights を追加するのを忘れずに
2.2 注意行列が取得できているか確認
ネットワーク構造が等価なので、第22回【文章分類・Transformer】で学習したモデルをそのまま利用することができるはず![]()
model_file = "model/data_90_bert_type.model"
model.load_state_dict(torch.load(model_file, weights_only=True))
# <All keys matched successfully> これが表示されるはず
もちろん、いままでと同様にデータを読み込んで学習することも可能です。すでに学習済みから始めます。下記のコード、突然 x[1]が登場していますが、詳細はsample_23.ipynbで確認してください。x[i]で $i$ 番目の文章を長さを揃えたIDベクトルを表しています。
model.eval()
# x[1]: 1番目の系列長11のIDベクトル
X = x[1].unsqueeze(0) # 注意行列を表示させたいデータを適当に選ぶ
with torch.no_grad():
output, attentions = model(X)
print(f"出力の形状: {output.shape}")
print(f"Transformerレイヤーの数: {len(attentions)}")
for idx, attn in enumerate(attentions):
print(f"{idx}層: 注意行列の形状: {attn.shape}")
# 出力の形状: torch.Size([1, 3])
# Transformerレイヤーの数: 6
# 0層: 注意行列の形状: torch.Size([1, 4, 11, 11])
# 1層: 注意行列の形状: torch.Size([1, 4, 11, 11])
# 2層: 注意行列の形状: torch.Size([1, 4, 11, 11])
# 3層: 注意行列の形状: torch.Size([1, 4, 11, 11])
# 4層: 注意行列の形状: torch.Size([1, 4, 11, 11])
# 5層: 注意行列の形状: torch.Size([1, 4, 11, 11])
説明メモ
- 学習に使われたデータ
x[1]による検証 -
['<bos>', '入会', 'の', '方法', 'を', '教え', 'て', '<eos>', '<pad>', '<pad>', '<pad>']を利用して、注意行列の情報が取得できているか確認します。 - 6層でヘッド数が4なので、それぞれの層で4個の11×11(系列長×系列長)の注意行列が取得できていることが確認できます。
2.3. 行列を色付けしてみる(ヒートマップ)
print(attentions)みたいに数字で確認してもよいのですが、見栄えが整うように表示してみます。第$k$レイヤ、$j$ヘッドの注意行列をmatplotlibを使ってヒートマップで表現してみました
output, attentions = model(X)で抽出されるattentionsがリストである点、リストの要素がtorch.tensorやcudaだったりする点に注意が必要です。
import matplotlib.pyplot as plt
def visualize_attention(attention_weights, title_info=""):
"""
注意行列を可視化
attention_weights: np.ndarray、(seq_len, seq_len)の注意行列
title_info: str、タイトル情報
"""
plt.figure(figsize=(5,5))
plt.imshow(attention_weights, cmap="Greens", aspect="auto")
plt.colorbar()
plt.xlabel("Key")
plt.ylabel("Query")
plt.title(title_info)
plt.tight_layout()
plt.show()
説明メモ
- imshow()で(seq_len, seq_len)の行列を表示するだけ。
- 引数であるattention_weightsがnumpy配列の注意行列になります。
表示したいデータ、Transformerレイヤー番号とヘッド番号を指定して、注意行列をヒートマップとして表示するだけです。
「<bos>/入会/の/方法/を/教え/て/<eos>/<pad>/<pad>/<pad>」で試してみました。
# x[1]:「入会の方法を教えて」の文章
with torch.no_grad():
output, attentions = model(x[1].unsqueeze(0))
# 5層0ヘッドの重みを抽出
layer_idx = 5
head_idx = 0
batch_idx = 0
attention_weights = attentions[layer_idx][batch_idx, head_idx].cpu().numpy()
title_info = f"Attention Weights - Layer {layer_idx}, Head {head_idx}"
# ヒートマップ表示
visualize_attention(attention_weights, title_info=title_info)
上記のコードそのままだとメモリが0,1,2と番号になります。演習のファイルでは図4のように文字に変換してあります。
softmax は各 Query 行ごとに横方向(Key 方向)に向かって計算していました。図4の各行は「ある Query トークンが、Key 側の各トークンにどれくらいの注意(確率的重み)を割り当てているか」を緑色の濃淡で表現しています。
5層(最終層)0ヘッド目の説明メモ
- 白色が0で、緑が濃くなるほど数値が大きくなります。
- <pad>の部分がちゃんと0になっています。
-
plt.imshow(attention_weights, cmap="Greens", aspect="auto")で最小値、最大値を固定すると注意行列の比較しやすいはず。しかし、実際試すとわかりますが、このサンプルだと逆にわかりにくい
なんか白っぽいんですよね。 - head番号0では、どの単語も「方法」や「教え」あたりに注目しているようです。「入会」=「ログインに分類」ではないのか!と思ってしまいます。
- このheadがあるから、「入会したけど、やめたい」みたいなのに対応できるのかな?
他のヘッドも可視化してみました。
layer_idx = 5
head_idx = 2
batch_idx = 0
attention_weights = attentions[layer_idx][batch_idx, head_idx].cpu().numpy()
title_info = f"Attention Weights - Layer {layer_idx}, Head {head_idx}"
# ヒートマップ表示
visualize_attention(attention_weights, title_info=title_info)
5層(最終層)2ヘッド目の説明メモ
- ヘッド番号2では、どの単語も「入会」「の」に注目しているようです。「入会」=「ログインに分類」の直感的な理解になっています。
この要領でいろいろ表示してみました。色の濃淡の比率が揃っていないので注意してください。揃えると、白っぽくなって見づらくなる気がする![]()
2.4. 2部グラフを使って関連度を可視化
ヒートマップの濃淡を直線で表示してみます。コメント行が多いので一見長く見えますが、文字の表示と対応する座標毎に線分を引くだけの作業です。思いの外面倒![]()
import matplotlib.pyplot as plt
import japanize_matplotlib
import numpy as np
def visualize_attention_bipartite(attention_weights, tokens, title_info="", threshold=0.15 ,figsize=(3,4)):
"""
二部グラフ形式で注意重みを可視化
attention_weights : np.ndarray、(seq_len, seq_len)の注意行列
tokens : list、単語のリスト(左右の文字を表す)
title_info : str、タイトルの情報
"""
seq_len = len(tokens)
fig, ax = plt.subplots(figsize=figsize)
# ---- 軸を決める ----
# (1) 横軸と縦軸の位置を決める(0,0)〜(1,1)の四角のイメージ
# 横軸の位置を決める。2箇所必要なので0と1で指定
left_x = 0
right_x = 1
# 単語の垂直位置(上下反転:1から0へ)
# 縦軸のメモリを単語数分準備する(0〜1を単語数で分割する)
# 今回の例だと11個
y_positions = np.linspace(1, 0, seq_len)
# ---- 文字表示 ----
# (2) 2部グラフの起点と終点
# 左側(起点) Q の単語を順番に表示(右寄せ)
for i, (token, y) in enumerate(zip(tokens, y_positions)):
ax.text(left_x - 0.02, y, token, ha="right", va="center", fontsize=10)
# 右側(終点) K の単語を順番に表示(左寄せ)
for i, (token, y) in enumerate(zip(tokens, y_positions)):
ax.text(right_x + 0.02, y, token, ha="left", va="center", fontsize=10)
# ---- 線分を描画 ----
# Qの各単語からKの各単語への接続を描画
# (3)
for i in range(seq_len): # 左側 Q
weights = attention_weights[i] # Qの単語からKの単語への重み
# weightsを先頭から数値の大きさに従い線を描画
# (left_x, y_position[i]) ---> (right_x, y_position[j])
# (4)
for j in range(seq_len): # 右側 K
weight = weights[j]
if weight > threshold: # threshold以下は無視
alpha = min(weight * 2, 1.0) # 透明度 0〜1の範囲なので適宜調整
linewidth = weight * 3 # 線の太さ 1.5が標準なので適宜調整
ax.plot([left_x, right_x], [y_positions[i], y_positions[j]], color="blue", linestyle="-", alpha=alpha, linewidth=linewidth)
# (5) グラフの設定
ax.set_xlim(-0.3, 1.3) # 横軸ちょっと広め
ax.set_ylim(-0.05, 1.05) # 縦軸ちょっと広め
ax.axis("off")
ax.set_title(title_info)
plt.tight_layout()
return fig
説明メモ
- (1) 大枠を決める部分。(0,0)と(1,1)の四角形をイメージしています。
- (2) 2部グラフの起点(左側)と終点(右側)をそれぞれ
ax.textでプロットします。文字の位置をhaやvaを使い調整すると見やすくなります。 - (3) attention_weightsを順番に取り出します。この重さに応じて透明度や線分の太さを変更します。
- (4) attention_weightsの重みを線分の透明度と太さにして、起点から終点へ線分を引きます。
- (0, y_position[i]) ---> (1, y_position[j])へ
ax.plotを使い線分を引きます。
3層(実際は4回目の層)の2ヘッド(実際は3つ目のヘッド)のattention weightsを描画してみます。
with torch.no_grad():
output, attentions = model(x[1].unsqueeze(0))
# 条件の設定
# 特定のレイヤーとヘッドの注意行列を指定
layer_idx = 3
head_idx = 2
attention_weights = attentions[layer_idx][0, head_idx].cpu().numpy() # (seq_len, seq_len) np.adarray
tokens = ['<bos>', '入会', 'の', '方法', 'を', '教え', 'て', '<eos>', '<pad>', '<pad>', '<pad>'] # 単語のリスト
threshold = 0.15 # 閾値
title_info = f"重み閾値: {threshold}\nLayer {layer_idx}, Head {head_idx}"
fig = visualize_attention_bipartite(
attention_weights,
tokens,
title_info = title_info,
threshold=threshold
)
plt.show()
重み閾値を0.15にしているので、線がやや少なめになります。単語に対する重みのが異なるので、透明度や線の太さの調整がうまく働いています。
5層目(最終層)の全ヘッドを横に並べてみました。Head 2の進化の様子がわかります。
- head 0 : 「方法を教え」という内容的な部分に注目しているっぽい
- head 1 : 文頭、文末を確認しているのかな?
- head 2 : 「入会」やはりこれですよね。
- head 3 : 閾値をもう少し小さくするとまんべんなくどの単語も見ている図になります。
参考文献
Attention Weightsの可視化といえば、ということで3種類ほど論文を。もちろん「Attention is 〜」にも図がありますが![]()
ここからはかなり適当です![]()
![]()
![]()
1番目はBERTの注意行列が「どんな意味を持つのか」的な内容でattention weightsの図もたくさんある。2番目はheadが増えると注意行列の多様性も増えるぞ〜的な内容で、でも増え続けるわけじゃないよという感じ。3番目のはどうやって作図するのかな
どうも、attention weightsを直接描画するタイプじゃないんだよね。
- Kevin Clark, Urvashi Khandelwal, Omer Levy, Christopher D. Manning, (2019) "What Does BERT Look At? An Analysis of BERT’s Attention"
- Hyeongu Yun, Taegwan Kang, and Kyomin Jung, (2021) "Analyzing and Controlling Inter-Head Diversity in Multi-Head Attention"
- Catherine Yeh, Yida Chen, Aoyu Wu, Cynthia Chen, Fernanda Viégas, and Martin Wattenberg, (2023) "AttentionViz: A Global View of Transformer Attention"
attention weightsの解釈可能性に関する研究。2つのタイトルが面白いですよね。LLMでTransformer全盛になったけど議論の行方はどうなったのかな?
- Sarthak Jain, Byron C. Wallace, (2019) "Attention is not Explanation"
- Sarah Wiegreffe, Yuval Pinter, (2019) "Attention is not not Explanation"
次回
マルチ繋がりということで文章データのマルチラベル分類を扱ってみたいと思います。
目次ページ
注
sunbluesomeさんのzenn記事Transformerを理解したい
Transformerの仕組みを体系的に理解したい1章・2章
Transformerの仕組みを体系的に理解したい3章
-
分散表現の次元で割り引いたり、QやKを正規化するなどの変更が加えられることが多いようです。 ↩
-
「考えられる」と断定していないのは、注意行列の解釈について、議論があるからです。参考文献の4番目や5番目を参照してください。 ↩
-
Transformer自体の解説や詳しい実装は他の方に委ねたいと思います。
Qittaならyukioroさんの「QKV注意機構の原理と...」と「Transformerブロックを...」。マルチヘッドまで解説してあるmk-mokumoku(渡邊琢資)さんの「マルチヘッド注意機構とは?...」が参考になります。
解説例が日本語であるHarumitsu Nobuta@halhornさんの作って理解する Transformer / Attentionなどなど沢山の方々が解説しています
↩ -
Transformerの細かい中身に入らないようにしていたのですが、ちょっと残念
↩ -
通常は中身に入らずMultiheadAttentionクラスを利用して直接記述するのが適切かと思います。 ↩









