はじめに
LLMのキモと言えるのが「Transformer」、そして「Transformer」を理解する上でネックになってくるのが「Self Attention(自己注意)」機構だと勝手に思っています。
さて、この革新的な概念であった「Self Attention(自己注意)」ですが、2017年に『Attention Is All You Need』という名前でGoogleの8人の研究者によって発表されました。
その後、研究が重ねられLLMは一気に精度が上がり、およそ5年後の2022年11月30日に「ChatGPT」として発表され一世を風靡したことは皆さんもご記憶だと思います。
ちなみに、この2022年11月30日という日付は、 後世の歴史教科書に載るくらいの重要な日付として位置付けられるのではないかと思っています。
それはさておき、「Self Attention」の説明の時に良く出てくる 「単語感の重みのグラフ」 が欲しいと思ったのですが、
- 日本語に対応
- ちょっとしたUI付属
のものがパッと見当たらず、 サクッとStreamlitアプリとして用意しました。
興味のある方に適宜遊んで欲しく、ここに公開します。
環境・準備
一応ローカルで動かす前提で記していきますが、Google Colabなどでも問題なく動くと思います。
ただColabの場合Port Tunnelingが入ってStreamlitアプリが見れないトラブルもあったので、自身で動かすだけならローカルをお勧めしておきます。
python3が入っている前提です。
仮想環境を使うのであれば(おすすめ)
mkdir プロジェクト名
cd プロジェクト名
python3 -m venv .
source ./bin/activate
mkdir app
こんな感じで環境を準備します。
そして必要なライブラリをインストール
pip install streamlit torch transformers matplotlib fugashi ipadic
Streamlitアプリのコード
プログラムコードを用意します。
ファイル名はattention.pyなどとしておきましょう。
# 概要:
# 日本語BERTモデルにテキストを入力し、アテンション(単語間の注意の強さ)を
# ヒートマップとして可視化するStreamlitアプリです。
# 各トークンがどのトークンに注目しているかを直感的に確認できます。
#
# 必要なモジュールのインストール:
# pip install streamlit torch transformers matplotlib fugashi ipadic
#
# 起動方法
# streamlit run attention.py
#
# 使い方
# 入力欄に適当な文章を入れて、コマンド(Ctl)+エンターを押してください
#
# おすすめの例文:
# 「猫がネズミを追いかけた。」
# → 主語・目的語・動詞の関係がシンプルで見やすい
# 「彼女は美しい花を庭に植えた。」
# → 形容詞と名詞の結びつきに注目できる
# 「太郎は花子が好きだと言った。」
# → 埋め込み節を含む構造で、長距離依存のアテンションが現れやすい
# 「今日は晴れですが、明日は雨でしょう。」
# → 接続詞をはさんだ対比構造を確認できる
# ライブラリのインポート
import streamlit as st # Webアプリ用フレームワーク
import torch # PyTorch(モデルの計算に使用)
from transformers import AutoTokenizer, AutoModel # BERTモデルとトークナイザ
import matplotlib.pyplot as plt # グラフ描画
import matplotlib
import matplotlib.font_manager as fm # フォント管理
# 文字化け防止
# 環境によってインストール済みフォントが異なるため、候補を順に試して使えるものを自動選択します
_jp_fonts = ["Hiragino Sans", "Yu Gothic", "Meiryo", "IPAGothic", "Noto Sans CJK JP", "TakaoGothic"]
_available = {f.name for f in fm.fontManager.ttflist} # 現在の環境で使えるフォント一覧を取得
for _font in _jp_fonts:
if _font in _available:
matplotlib.rcParams["font.family"] = _font # 最初に見つかった日本語フォントを適用
break
model_name = "cl-tohoku/bert-base-japanese-v3" # 日本語対応BERTモデル
# 他の選択肢としては
# rinna/japanese-roberta-base
# nlp-waseda/roberta-base-japanese
# など
# トークナイザの読み込み(テキストをモデルが理解できる数値列に変換するツール)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# モデルの読み込み
# output_attentions=True にすることで、各層のアテンション重みも取得できます
model = AutoModel.from_pretrained(
model_name,
output_attentions=True
)
# テキスト入力欄を表示し、ユーザーの入力を受け取ります
text = st.text_area("Input")
# クリックで開閉できる例文一覧を表示します
with st.expander("例文を見る"):
st.markdown("""
- 猫がネズミを追いかけた。
- 彼女は美しい花を庭に植えた。
- 太郎は花子が好きだと言った。
- 今日は晴れですが、明日は雨でしょう。
""")
if text:
# テキストをトークナイズし、PyTorchのテンソル形式に変換します
inputs = tokenizer(text, return_tensors="pt")
# モデルに入力を渡して推論を実行します
# outputs.attentions には全層・全ヘッドのアテンション重みが含まれます
outputs = model(**inputs)
# 第1層・第1バッチ・第1ヘッドのアテンションを取り出します
# shape は (トークン数, トークン数) になります
attention = outputs.attentions[0][0][0].detach()
# 入力IDを人間が読めるトークン文字列に変換します
tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
# アテンション行列をヒートマップとして可視化します
fig, ax = plt.subplots(figsize=(10, 8))
# 値が大きいほど明るく表示されます
ax.imshow(attention)
# 各セルにアテンションスコアを数値で表示します
for i in range(len(tokens)):
for j in range(len(tokens)):
score = attention[i, j].item()
# 背景が暗い場合は白文字、明るい場合は黒文字にして読みやすくします
color = "white" if score < 0.5 else "black"
ax.text(j, i, f"{score:.2f}", ha="center", va="center", fontsize=7, color=color)
# X軸・Y軸にトークン文字列を設定します
ax.set_xticks(range(len(tokens)))
ax.set_xticklabels(tokens, rotation=90) # X軸ラベルは縦書きにして読みやすくします
ax.set_yticks(range(len(tokens)))
ax.set_yticklabels(tokens)
ax.set_xlabel("Key(注意を向ける先)")
ax.set_ylabel("Query(注意を向けている元)")
# 作成したグラフをStreamlitアプリ上に表示します
st.pyplot(fig)
# ---
# 接続グラフ: トークン同士を線で結び、アテンションの強さを線の太さ・濃さで表します
n = len(tokens)
fig2, ax2 = plt.subplots(figsize=(8, max(4, n * 0.5)))
ax2.set_xlim(-0.3, 1.3)
ax2.set_ylim(-0.5, n - 0.5)
ax2.invert_yaxis() # トークンを上から順に並べます
ax2.axis("off")
# スコアが小さすぎる線は省略して見やすくします
threshold = 0.05
for i in range(n):
for j in range(n):
score = attention[i, j].item()
if score > threshold:
# alpha(透明度)と linewidth をスコアに比例させます
ax2.plot([0, 1], [i, j],
color="steelblue",
alpha=min(score * 3, 1.0),
linewidth=score * 4)
# 左側に Query(注意を向けている元)、右側に Key(注意を向ける先)を表示します
for i, token in enumerate(tokens):
ax2.text(-0.05, i, token, ha="right", va="center", fontsize=9)
ax2.text(1.05, i, token, ha="left", va="center", fontsize=9)
ax2.set_title("アテンション接続グラフ(線の太さ・濃さ = アテンションの強さ)", fontsize=10)
st.pyplot(fig2)
実行
起動方法は以下です。
streamlit run attention.py
このファイルはstreamlitのファイルなので、
python3 attention.py
では 動きません。
警告がドバドバ表示されるかも知れませんが、今回の動作には支障はないので無視して大丈夫です。(torchvisionを導入することで消えるかも知れないですが未確認)
自動的にlocalhost:850*などがブラウザで開かれます。
最初はモデルのロードなどで少し待たされますが、以下の画面が出るはずです。
これがStreamlitアプリです。フロントのコードを一切書かなくてもこういうものができるので便利ですよね〜
input欄に好きな文を入力しても良いのですが、「例文を見る」をクリックするといくつか典型的な注意関係を持つ例文が表示されます。
最初はこの中から試してみることをお勧めします。
なぜかというと、自由に入力すると案外曖昧な結果が出てきて、解釈しにくかったりするからです。
では、 「猫がネズミを追いかけた。」 の例文をコピペしてみましょう。
注意機構の説明で良く持ち出されるパターンです。
入力したら、「コマンド(Ctrl)+エンター」を押下します。
すると、少し処理時間がかかるかも知れませんが、以下のように注意関係が2つのグラフで表示されます。
解説サイトなどでおなじみのグラフが表示されました!
結果を考察
まず、CLSとSEPですが、
- CLS -> 入力の先頭を表すトークン
- SEP -> 文の区切りを表すトークン
になります。
あとは、ほぼ適切にトークン分割ができていることが窺えます。
実は、他の例文に出てくる「花子」などの人名はうまく分割できないんですね…ここはモデル次第でしょう。
注意関係を見ていきましょう。
若干、直感に反するスコアもあるし、納得できるものも見つかると思います。
この例で言えば
- 「猫」→「追いかけ」(主語ー動詞の関係で注意されている)
- SEP(文末)が「を」「追いかけた」「。」から注意されている。なぜ「を」?
-
「が」が「を」を注意(同じ助詞の仲間?)
等々、色々な考察が可能です。
ところで、「注意度(Attention)」のベースになっているのは、各単語を高次元空間にマッピングしたベクトルの 「内積」 です。
類似度が高いペアの内積は相対的に大きくなり、数学的調整を経てスコアに反映されます。
ただし、この「類似度」が曲者で、 人間が認識している類似度と異なる 場合もあります。
例えば3次元空間で言えば、横から見るとピッタリくっついているのに上から見るとかなり離れているということがありえますよね?
意味空間も同じですが、次元数は 「数百、数千」 が当たり前。なのでどこから見るかで「近い」「遠い」の結果がかなり異なり、場合によっては人間にとって「?」になってしまうのです。
そこで、ベクトルを見る視線を複数持たせ、結果を統合することによって実用的な結果を出せる工夫(Multi-Head-Attention)が使われているのですが、本記事はアプリの紹介が主なので詳細は別途ご参照願います。
どうやってスコアを計算しているのか、参考までに…
とは言っても、スコアの算出が気になる方のために、GPT5-miniに解説をお願いし少し手直ししました。
数式も出てきますが、適度に流し読みしていただければ雰囲気は掴めるかも知れません。
Attention(注意)と Q・K(内積) の関係メモ
このメモは、注意(attention)のスコア算出の仕組みと、Q(Query)・K(Key)の内積(QK)が類似性スコアに与える影響についてまとめたものです。
Q(Query)は注意 「元」
K(Key)は注意 「先」
どこから取っているか
- Hugging Face Transformers のモデル出力
outputs.attentionsは形状(num_layers, batch_size, num_heads, seq_len, seq_len)。 - プログラムは
outputs.attentions[0][0][0]を使って「第1層・第1バッチ・第1ヘッド」の注意行列(seq_len × seq_len)を可視化している。
注意重みの数学的定義
- Transformer のスケルトンは次の通り:
$$\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\Big(\frac{QK^{\top}}{\sqrt{d_k}}\Big),V$$
- ここで $Q$(Query)と $K$(Key) の内積 $QK^{\top}$ を $\sqrt{d_k}$ でスケーリングし、softmax を取ると、各クエリに対するキーの確率分布(各行の和が1、要素は0〜1)になる。
- プログラムで可視化しているのは softmax 後の「注意重み」(
outputs.attentionsの値)である。
ロジット(logit)とは
-
ロジットとは softmax に入る前の 「生のスコア」 を指します。Transformer では各クエリと各キーの互換性を表す値で、数式では
$$\text{logits} = \frac{QK^{\top}}{\sqrt{d_k}}$$
と表されます。
-
ロジットは正負どちらの値も取り得て、そのままでは確率ではありません。softmax を取ることで、行ごと(クエリごと)に正規化された注意重み(確率分布)に変換されます。
→ つまりQueryごとに全Keyの合計が1.0になる
QK(内積)とスコアの関係
- 一般に、あるクエリベクトルとキーベクトルの内積が大きいほど、softmax 前のロジットが大きくなり、softmax 後の注意重みも相対的に大きくなる。つまり 「QとKが似ている(内積が大きい)ペアは高い注意スコアを持ちやすい」。
- ただし注意点:
- 「似ている」の判断はモデル学習で得られた埋め込み空間上の内積や角度に依存するため、必ずしも人間的な意味的類似性と一致するとは限らない。
- Q・K は線形射影(学習パラメータ)を通して生成されるため、同一トークンでも文脈やヘッドによって Q/K が変わる。
- softmax は相対的な比較を行うため、あるキーへの重みは他の全キーとの相対値にも依存する。
実装上の注意
- 複数ヘッド・複数層が存在するため、1つのヘッド・1層だけを見ていると偏った情報になる。全ヘッド平均や層平均を取ると全体傾向が見やすい。
例(層を指定して全ヘッド平均を取る):
### layer を選び,バッチ0 の全ヘッド平均を使う例
attn = outputs.attentions[layer][0].mean(dim=0) # shape: (seq_len, seq_len)
- トークン化(サブワード分割)や特殊トークン([CLS],[SEP])の存在に留意すること。
-
outputs.attentionsは softmax 後の値。pre-softmax の生の $QK^{\top}/\sqrt{d_k}$ を見たい場合は、モデル実装の内部(Attention 層)にアクセスする必要がある。
まとめ(簡潔)
- QK(内積)が大きければ通常は attention スコア(重み)も高くなる傾向にある。だが「意味的類似性かどうか」は学習表現・文脈・ヘッドごとの役割などの条件に依存する。


