Rを利用して、TransformerのAttention機構におけるSoftmaxを確認します。
Softmaxとは
Softmaxとは、任意の実数のベクトルを、合計が1になる「確率分布」に変換する関数です。
機械学習において、モデルの出力を「確率」として解釈したい場面(分類問題で各クラスに属する確率を出したい、など)で広く使われます。
数式は次の通りです。
\[\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}}\]
つまり「各要素を\(e\)のべき乗にしてから、全体の合計で割る(正規化する)」という操作です。
一見、\(x_i / \sum_j x_j\) のように単純に合計で割るだけでも「合計が1になる分布」は作れそうです。
しかし、これには以下の問題が生じます。
- 入力に負の数が含まれると、負の確率が出てきてしまい、確率として成立しない
- 入力全体の水準(スケール)に結果が左右されてしまい、同じ相対的な差を持つ入力でも、水準次第で確率差が大きく見えたり、ほとんど消えてしまったりする。
例えば、差はどちらも「1」の場合、
入力A: c(1, 2, 3) (差は常に1)
入力B: c(101, 102, 103) (差は常に1。Aに100を加えたのみ)単純な正規化(x / sum(x))では、
Aの結果: 1/6=0.167, 2/6=0.333, 3/6=0.500 → 差がはっきり見える
Bの結果: 101/306=0.330, 102/306=0.333, 103/306=0.336 → ほぼ均等になり、差が消えてしまうつまり、同じ「差が1」という情報のはずなのに、Bのように全体の水準が大きいだけで、確率としてはほとんど差がないように見えてしまいます。
\(e^x\)(指数関数)を使うことで、
- どんな実数でも必ず正の値になる、
- 値の差が指数的に強調される(大きい値はより大きく、小さい値はより小さく評価される)、
という2つの性質が同時に得られます。
また、\(e^x\)は\(x\)が大きいとすぐに巨大な数になってしまい、浮動小数点演算で「無限大(Inf)」になってしまうことがあります。
その回避のため、「各値から最大値を引いてから\(e^x\)を計算する」という工夫をします。
これは数学的にsoftmaxの結果を一切変えない(shift invariance、シフト不変性)ことが証明できる操作です。
Part 1: Softmax の定義
\[\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}}\] 「どんな実数ベクトルも、合計が1になる正の値の並び(= 確率分布) に変換する」関数です。
softmax <- function(x) {
exp(x) / sum(exp(x))
}
scores <- c(2.0, 1.0, 0.5, -1.0)
probs <- softmax(scores)
cat("=== 入力 (スコア) ===\n")
print(scores)
cat("\n=== Softmax適用後 (確率) ===\n")
print(round(probs, 4))
cat(sprintf("\n合計 (必ず1になる): %.6f\n", sum(probs)))=== 入力 (スコア) ===
[1] 2.0 1.0 0.5 -1.0
=== Softmax適用後 (確率) ===
[1] 0.6095 0.2242 0.1360 0.0303
合計 (必ず1になる): 1.000000全ての値が正になり、合計が1になっています。
また、元のスコアの大小関係 (2.0 > 1.0 > 0.5 > -1.0) が、確率の大小関係にもそのまま保たれています(単調性)。
Part 2: 単純な正規化(合計で割るだけ)では問題の生じる理由
# 単純な正規化: 各値を合計で割るだけ
plain_normalize <- function(x) {
x / sum(x)
}
cat("=== 単純な正規化 vs Softmax の比較 ===\n")
# 負の値を含む例
scores_with_negative <- c(3.0, -2.0, 1.0)
cat("入力:", scores_with_negative, "\n")
cat(
"単純な正規化:", round(plain_normalize(scores_with_negative), 4),
" <- 負の「確率」が出てしまう\n"
)
cat(
"Softmax: ", round(softmax(scores_with_negative), 4),
" <- 全て正の値で合計1\n"
)
# 値の差の強調具合を比較する例
#
# 以下の2つを意図的に対比させています:
# scores_close: 差が小さく(0.1刻み)、値自体も小さい
# -> e^x はxが小さい範囲ではテイラー展開 e^x ≈ 1+x により
# ほぼ線形になるため、単純な正規化とSoftmaxの結果は
# ほとんど一致する (指数関数の非線形性がまだ効かない)
# scores_far: 差が大きい(4刻み)
# -> e^x の非線形性がはっきり効き、Softmaxは単純な正規化
# よりも明確に「勝者(最大値)」に確率を強く寄せる
#
# つまりSoftmaxが単純な正規化と違う結果を生むのは、あくまで
# 「値の差が大きい」場合に限られます
scores_close <- c(1.0, 1.1, 1.2) # わずかな差
scores_far <- c(1.0, 5.0, 9.0) # 大きな差
cat("\n差が小さい入力:", scores_close, "\n")
cat(" 単純な正規化:", round(plain_normalize(scores_close), 4), "\n")
cat(" Softmax: ", round(softmax(scores_close), 4), "\n")
cat("\n差が大きい入力:", scores_far, "\n")
cat(" 単純な正規化:", round(plain_normalize(scores_far), 4), "\n")
cat(" Softmax: ", round(softmax(scores_far), 4), "\n")=== 単純な正規化 vs Softmax の比較 ===
入力: 3 -2 1
単純な正規化: 1.5 -1 0.5 <- 負の「確率」が出てしまう
Softmax: 0.8756 0.0059 0.1185 <- 全て正の値で合計1
差が小さい入力: 1 1.1 1.2
単純な正規化: 0.303 0.3333 0.3636
Softmax: 0.3006 0.3322 0.3672
差が大きい入力: 1 5 9
単純な正規化: 0.0667 0.3333 0.6
Softmax: 3e-04 0.018 0.9817 Softmax は、単純な正規化の場合よりも、入力の差が大きいほど、確率の差もより極端に強調する傾向が確認できます。
Part 3: シフト不変性 (Shift Invariance)
全ての入力に同じ定数を足しても、Softmaxの出力は変わらない、という性質です。
これは \(\frac{e^{x_i + c}}{\displaystyle\sum_j e^{x_j + c}}\) を展開すると\(e^c\) が分子分母で約分されて消えることから数学的に導けます。
\[
\text{softmax}(x_i + c) = \frac{e^{x_i + c}}{\displaystyle\sum_j e^{x_j + c}}
\]
まず分子を指数法則 \(e^{a+b} = e^a e^b\) を使って分解します。
\[
= \frac{e^{x_i}\, e^{c}}{\displaystyle\sum_j e^{x_j + c}}
\]
分母についても、各項\(e^{x_j+c}\)を同様に分解します。\(e^c\)は\(j\)に依存しない定数なので、和の外に出せます。
\[
\sum_j e^{x_j + c} = \sum_j e^{x_j} e^{c} = e^{c} \sum_j e^{x_j}
\]
これを代入すると、
\[
\text{softmax}(x_i + c) = \frac{e^{x_i}\, e^{c}}{e^{c} \displaystyle\sum_j e^{x_j}}
\]
分子・分母に共通する\(e^c\)を約分すると、
\[
= \frac{e^{x_i}}{\displaystyle\sum_j e^{x_j}} = \text{softmax}(x_i)
\]
すなわち、
\[
\text{softmax}(x_i + c) = \text{softmax}(x_i) \quad \text{for any constant } c
\]
が成り立ちます。
これが「シフト不変性」の数学的な根拠です。
\(c = -\displaystyle\max_k x_k\) を選ぶことで、指数計算前の最大値を必ず0以下に抑えられるため、結果はまったく変えずにexp()のオーバーフローを防ぐことが出来ます。
x <- c(2.0, 1.0, 0.5, -1.0)
x_shifted <- x + 100 # 全体に定数100を加える
cat("=== シフト不変性の確認 ===\n")
cat("元の入力の softmax: ", round(softmax(x), 6), "\n")
cat("+100 した入力の softmax: ", round(softmax(x_shifted), 6), "\n")=== シフト不変性の確認 ===
元の入力の softmax: 0.60946 0.224208 0.135989 0.030343
+100 した入力の softmax: 0.60946 0.224208 0.135989 0.030343 Part 4: オーバーフロー問題と、その対策
exp(x) は x が大きいとすぐに天文学的な数値になり、表現できる範囲を超えてしまいます (Inf になります)。
Part 3 のシフト不変性を利用し、「計算前に最大値を引く」ことで、数学的な結果を変えずにオーバーフローを防ぐことが出来ます。
x_large <- c(1000, 999, 998)
cat("=== オーバーフロー問題の実演 ===\n")
naive_softmax <- function(x) {
exp(x) / sum(exp(x)) # 対策なし
}
cat("対策なしの softmax:\n")
result_naive <- naive_softmax(x_large)
print(result_naive) # NaN になる (Inf / Inf = NaN)
# 数値的に安定な softmax: 計算前に最大値を引く
stable_softmax <- function(x) {
x_shifted <- x - max(x) # 最大値を0にする (シフト不変性より結果は不変)
exp(x_shifted) / sum(exp(x_shifted))
}
cat("\n数値的に安定な softmax (最大値を引いてから計算):\n")
result_stable <- stable_softmax(x_large)
print(round(result_stable, 4))=== オーバーフロー問題の実演 ===
対策なしの softmax:
[1] NaN NaN NaN
数値的に安定な softmax (最大値を引いてから計算):
[1] 0.6652 0.2447 0.0900Part 5: 温度 (Temperature) パラメータの効果
softmax(x / T) のように、入力全体を定数 T (温度) で割ってからsoftmax を適用することがあります。
-
T が小さい(<1)-> 分布がより「尖る」(一強になりやすい) -
T が大きい(>1)-> 分布がより「均される」(平坦になりやすい) -
T -> 0-> 最大値だけが1、他は0 (argmaxに近づく) -
T -> 無限大-> 完全に一様分布に近づく
softmax_with_temperature <- function(x, T = 1) {
softmax(x / T)
}
base_scores <- c(3.0, 1.0, 0.2, -1.0)
temperatures <- c(0.3, 1.0, 3.0, 10.0)
temp_results <- lapply(temperatures, function(T) {
data.frame(
category = factor(seq_along(base_scores)),
probability = softmax_with_temperature(base_scores, T),
temperature = paste0("T = ", T)
)
})
temp_df <- do.call(rbind, temp_results)
temp_df$temperature <- factor(temp_df$temperature, levels = paste0("T = ", temperatures))
cat("=== 温度による分布の変化 ===\n")
for (T in temperatures) {
cat(sprintf(
"T = %-5s : %s\n", T,
paste(round(softmax_with_temperature(base_scores, T), 3), collapse = " ")
))
}=== 温度による分布の変化 ===
T = 0.3 : 0.999 0.001 0 0
T = 1 : 0.823 0.111 0.05 0.015
T = 3 : 0.461 0.237 0.181 0.121
T = 10 : 0.308 0.252 0.233 0.207Part 6: ggplot2 による可視化
library(ggplot2)
# --- 図1: 基本のSoftmax変換 (スコア -> 確率) ---
plot_df1 <- data.frame(
category = factor(seq_along(scores)),
value = c(scores, probs),
type = rep(c("元のスコア", "Softmax後の確率"), each = length(scores))
)
plot_df1$type <- factor(plot_df1$type, levels = c("元のスコア", "Softmax後の確率"))
p1 <- ggplot(plot_df1, aes(x = category, y = value, fill = category)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = round(value, 2)), vjust = -0.5, size = 3.5) +
facet_wrap(~type, scales = "free_y") +
labs(
title = "Softmax: スコアを確率分布に変換する",
x = "カテゴリ", y = "値"
) +
theme_minimal(base_size = 12)
print(p1)
# --- 図2: 単純な正規化 vs Softmax (差の強調具合の比較) ---
compare_df <- data.frame(
category = factor(rep(seq_along(scores_far), 2)),
probability = c(plain_normalize(scores_far), softmax(scores_far)),
method = rep(c("単純な正規化 (x/sum(x))", "Softmax"), each = length(scores_far))
)
p2 <- ggplot(compare_df, aes(x = category, y = probability, fill = method)) +
geom_col(position = "dodge") +
geom_text(aes(label = round(probability, 3)),
position = position_dodge(width = 0.9), vjust = -0.5, size = 3.2
) +
labs(
title = "単純な正規化 vs Softmax",
subtitle = paste("入力:", paste(scores_far, collapse = ", ")),
x = "カテゴリ", y = "確率", fill = NULL
) +
theme_minimal(base_size = 12) +
theme(legend.position = "top")
print(p2)
# --- 図3: 温度パラメータによる分布の変化 ---
p3 <- ggplot(temp_df, aes(x = category, y = probability, fill = category)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = round(probability, 2)), vjust = -0.5, size = 3) +
facet_wrap(~temperature, nrow = 1) +
labs(
title = "温度 (Temperature) による Softmax 分布の変化",
subtitle = paste("元のスコア:", paste(base_scores, collapse = ", ")),
x = "カテゴリ", y = "確率"
) +
theme_minimal(base_size = 12) +
coord_cartesian(ylim = c(0, 1))
print(p3)以上です。



