第12章 次元の拡大と縮小の意味

こんにちは。ゆうせいです。

第11章では、2次元の図を使って、行列がどのような変換を行うのかを見てきました。ただし、そこで扱ったのは、2次元から2次元への変換だけでした。第12章では、次元そのものが変わる場合を扱います。ディープラーニングでは、次元を変える操作が頻繁に登場します。GPT-2のMLP層(多層パーセプトロン、Multi-Layer Perceptron)では、768次元をいったん3072次元に拡大し、また768次元に戻します。手書き数字の認識では、784次元の入力を、128次元、64次元と段階的に縮小し、最後は10次元にします。なぜこのような操作をするのでしょうか。拡大するとき、何が増えているのでしょうか。縮小するとき、何が失われているのでしょうか。この章では、その意味を考えます。

次元が変わる仕組み

行列の形が次元を決める

第8章で確認したとおり、行列とベクトルの積では、次のように次元が決まります。

(m, n) \times (n) \rightarrow (m)

入力の次元数は、行列の列数と一致していなければなりません。

出力の次元数は、行列の行数で決まります。

つまり、行列の形を選ぶことで、任意の次元に変換できます。

三つの場合

行列の形によって、三つの場合に分かれます。

行数が列数より多い場合、次元が増えます。拡大です。

行数と列数が等しい場合、次元は変わりません。

行数が列数より少ない場合、次元が減ります。縮小です。

第11章で扱った変換は、すべて二つ目の場合でした。この章では、一つ目と三つ目を扱います。

次元の縮小

まず、縮小から考えます。理解しやすいためです。

具体例

3次元から2次元への変換を考えます。

A = \begin{pmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \end{pmatrix}

2行3列の行列です。3次元のベクトルを受け取り、2次元のベクトルを返します。

A\begin{pmatrix} 3 \\ 5 \\ 7 \end{pmatrix} = \begin{pmatrix} 3 \\ 5 \end{pmatrix}

3番目の要素が、消えています。

これは、第11章で扱った射影です。3次元空間の点を、平面に落としています。

情報の損失

この変換では、情報が失われます。

[3, 5, 7][3, 5, -100] も、変換後は同じ [3, 5] になります。

変換後の値から、元の値を復元することはできません。

これが、次元を減らす操作の本質的な性質です。次元が減れば、必ず何らかの情報が失われます。

何が失われるかは行列が決める

ただし、どの情報を失うかは、行列の内容によって決まります。

先ほどの例では、3番目の要素だけが捨てられました。

しかし、次のような行列であれば、状況が変わります。

B = \begin{pmatrix} 1 & 1 & 1 \\ 1 & -1 & 0 \end{pmatrix}

B\begin{pmatrix} 3 \\ 5 \\ 7 \end{pmatrix} = \begin{pmatrix} 15 \\ -2 \end{pmatrix}

1行目は、三つの要素の合計を計算しています。

2行目は、1番目と2番目の差を計算しています。

この場合、すべての要素が結果に影響しています。単純に一つを捨てているのではなく、情報を混ぜ合わせて圧縮しているのです。

重要な情報を残す

ディープラーニングでは、行列の値が学習によって決まります。

学習の過程で、目的の達成に重要な情報を残し、不要な情報を捨てるような行列が獲得されます。

手書き数字の認識であれば、784次元の画像から、数字の識別に必要な特徴だけを取り出します。用紙のわずかな汚れや、線の微妙な太さの違いといった、識別に不要な情報は捨てられます。

これが、次元を減らす目的です。単に情報量を減らすのではなく、本質的な情報を抽出しているのです。

ボトルネックという考え方

意図的に次元を絞り込む構造を、ボトルネックと呼びます。

オートエンコーダと呼ばれるモデルでは、入力を小さな次元に圧縮し、そこから元の入力を復元しようとします。

小さな次元で復元できるということは、その次元に本質的な情報が凝縮されている、ということです。

情報を通す道を意図的に細くすることで、モデルに何が重要かを学ばせる。これが、ボトルネックの考え方です。

次元の拡大

次に、拡大を考えます。こちらのほうが、直感的には理解しにくい操作です。

素朴な疑問

2次元から3次元への変換を考えます。

C = \begin{pmatrix} 1 & 0 \\ 0 & 1 \\ 1 & 1 \end{pmatrix}

3行2列の行列です。

C\begin{pmatrix} 3 \\ 5 \end{pmatrix} = \begin{pmatrix} 3 \\ 5 \\ 8 \end{pmatrix}

2個だった数値が、3個になりました。

ここで、疑問が生じます。情報が増えたのでしょうか。

答えは、増えていません。

3番目の要素8は、1番目と2番目から計算されたものです。新しい情報は、一切加わっていません。

情報は増えないが、表現は変わる

では、拡大には意味がないのでしょうか。

意味はあります。ただし、それは情報を増やすことではなく、情報の表現の仕方を変えることです。

同じ情報でも、表現の仕方によって、扱いやすさが変わります。

分離しやすくするという効果

具体例で考えます。

平面上に、二種類の点が分布しているとします。原点に近い点が種類A、遠い点が種類Bです。

この二種類を、1本の直線で分けることはできません。円状に分布しているためです。

ここで、3次元目として原点からの距離を追加してみます。

[x, y] \rightarrow [x, y, x^2 + y^2]

すると、種類Aは下のほうに、種類Bは上のほうに配置されます。3次元空間では、水平な平面1枚で、きれいに分離できるようになります。

次元を増やすことで、分離が容易になったのです。

なお、この例で追加した3次元目は、x^2 + y^2 という非線形な計算です。線形変換だけでは、この効果は得られません。この点は、第13章で扱います。

ディープラーニングでの実例

GPT-2のMLP層では、次の変換が行われています。

768 \rightarrow 3072 \rightarrow 768

768次元をいったん3072次元に広げ、活性化関数を適用してから、768次元に戻します。

なぜ、いったん広げるのでしょうか。

広い空間で非線形な処理を行うほうが、より複雑な変換を表現できるためです。

768次元のまま活性化関数を適用するより、3072次元に広げてから適用するほうが、表現できるパターンが豊かになります。

そして、次の層に渡すために、768次元に戻します。この戻す操作が、縮小です。

記憶場所としての解釈

近年の研究では、MLP層の中間の広い次元が、モデルが学習した知識を保持する場所として機能している、という見方も示されています。

拡大する側の行列がどの知識に該当するかを判定する役割を果たし、縮小する側の行列が該当する知識の内容を出力する、という解釈です。

この見方では、3072という次元数は、記憶できる項目数に対応することになります。

ただし、これは確立された結論ではなく、有力な仮説の一つとして提案されているものです。

拡大と縮小の非対称性

拡大と縮小には、決定的な非対称性があります。

縮小は不可逆

次元を減らす操作では、情報が失われます。一度失われた情報は、二度と戻りません。

768次元を10次元にすれば、大量の情報が捨てられます。

拡大は情報を増やさない

次元を増やしても、情報量は増えません。もとの情報を、別の形で表現し直しているだけです。

組み合わせた場合

768 \rightarrow 3072 \rightarrow 768

この一連の変換で、情報量はどうなるでしょうか。

前半の拡大では、情報は増えません。

後半の縮小では、情報が失われる可能性があります。

したがって、全体としては、情報が減るか、変わらないかのどちらかです。

では、なぜこの構造に意味があるのでしょうか。

答えは、間に活性化関数が挟まっているからです。

768 \rightarrow 3072 \rightarrow (activation) \rightarrow 768

活性化関数という非線形な処理が、広い空間で適用されることに、意味があります。

もし活性化関数がなければ、この一連の変換は、768次元から768次元への一つの線形変換と等価です。わざわざ拡大する意味がありません。

この点は、次の第13章で詳しく扱います。

ランクという概念

やや発展的ですが、重要な概念に触れておきます。

ランクとは何か

行列のランクとは、その行列による変換が、何次元の空間に写すかを表す値です。

先ほどの例を見てみます。

C = \begin{pmatrix} 1 & 0 \\ 0 & 1 \\ 1 & 1 \end{pmatrix}

この行列は、2次元のベクトルを3次元のベクトルに変換します。

しかし、変換後の3次元ベクトルは、3次元空間全体に散らばるわけではありません。

3番目の要素は、常に1番目と2番目の合計です。この制約により、変換後の点は、3次元空間の中の2次元的な面の上にしか存在できません。

このとき、この行列のランクは2である、と言います。

情報量の上限

ランクは、その変換が保持できる情報の上限を表しています。

行列の形が3行2列であっても、ランクは最大でも2です。

rank \leq \min(m, n)

このことから、拡大では情報が増えないという事実が、あらためて確認できます。

低ランク近似という応用

この性質を利用した技術があります。

大きな行列を、ランクの小さい二つの行列の積で近似する方法です。

W \approx AB

W が768行3072列だとします。パラメータ数は約236万個です。

これを、768行8列の行列と、8行3072列の行列の積で近似すると、パラメータ数は次のようになります。

768 \times 8 + 8 \times 3072 = 6144 + 24576 = 30720

約3万個です。約77分の1に削減できます。

この考え方は、LoRAと呼ばれる、大規模モデルの効率的な微調整の手法に応用されています。

Pythonで確認する

import numpy as np

print("=== 次元の縮小:単純な射影 ===")
A = np.array([
    [1, 0, 0],
    [0, 1, 0]
])
print(f"行列(形{A.shape}):")
print(A)

vectors = [
    np.array([3, 5, 7]),
    np.array([3, 5, -100]),
    np.array([3, 5, 0])
]
print("\n3番目の要素が違っても、結果は同じ:")
for v in vectors:
    print(f"  {v} → {A @ v}")

print("\n=== 次元の縮小:情報を混ぜる ===")
B = np.array([
    [1, 1, 1],
    [1, -1, 0]
])
print(f"行列(形{B.shape}):")
print(B)
print("\nすべての要素が結果に影響します:")
for v in vectors:
    print(f"  {v} → {B @ v}")

print("\n=== 次元の拡大 ===")
C = np.array([
    [1, 0],
    [0, 1],
    [1, 1]
])
print(f"行列(形{C.shape}):")
print(C)

v2d = np.array([3, 5])
v3d = C @ v2d
print(f"\n{v2d} → {v3d}")
print("3番目の要素は、1番目と2番目の合計です。新しい情報はありません。")

print("\n=== 拡大後の点は、限られた領域にしか存在しない ===")
np.random.seed(0)
print("ランダムな2次元ベクトルを拡大した結果:")
for _ in range(5):
    v = np.random.randn(2).round(2)
    result = C @ v
    print(f"  {v} → {result.round(2)}  (3番目 = 1番目 + 2番目 = {result[0]+result[1]:.2f})")

print("\n=== ランクの確認 ===")
matrices = {
    "単位行列(3x3)": np.eye(3),
    "射影行列(2x3)": A,
    "拡大行列(3x2)": C,
    "すべて同じ行": np.array([[1, 2, 3], [1, 2, 3], [1, 2, 3]]),
    "ランダム(3x3)": np.random.randn(3, 3)
}

for name, M in matrices.items():
    rank = np.linalg.matrix_rank(M)
    print(f"{name:>16}: 形{M.shape}, ランク{rank}, 上限{min(M.shape)}")

print("\n=== 拡大してから縮小すると ===")
np.random.seed(42)
W1 = np.random.randn(3072, 768) * 0.02   # 拡大
W2 = np.random.randn(768, 3072) * 0.02   # 縮小

x = np.random.randn(768)

h = W1 @ x
y_linear = W2 @ h

W_combined = W2 @ W1
y_combined = W_combined @ x

print(f"入力の形: {x.shape}")
print(f"拡大後の形: {h.shape}")
print(f"縮小後の形: {y_linear.shape}")
print(f"\n合成行列の形: {W_combined.shape}")
print(f"段階的に計算した結果と合成行列の結果が一致するか: {np.allclose(y_linear, y_combined)}")
print(f"合成行列のランク: {np.linalg.matrix_rank(W_combined)}")
print("\n活性化関数がなければ、768→768の1つの変換と等価です。")

print("\n=== 活性化関数を挟むと ===")
def gelu_approx(x):
    return 0.5 * x * (1 + np.tanh(np.sqrt(2/np.pi) * (x + 0.044715 * x**3)))

h_activated = gelu_approx(W1 @ x)
y_nonlinear = W2 @ h_activated

print(f"活性化関数なしの出力(最初の5要素): {y_linear[:5].round(4)}")
print(f"活性化関数ありの出力(最初の5要素): {y_nonlinear[:5].round(4)}")
print("結果が異なります。活性化関数があると、単一の線形変換では表現できません。")

print("\n=== 低ランク近似によるパラメータ削減 ===")
original_shape = (768, 3072)
original_params = original_shape[0] * original_shape[1]
print(f"元の行列: {original_shape}, パラメータ数 {original_params:,}")

print(f"\n{'ランク':>8} | {'パラメータ数':>14} | {'削減率':>10}")
print("-" * 38)
for rank in [4, 8, 16, 32, 64]:
    low_rank_params = original_shape[0] * rank + rank * original_shape[1]
    reduction = (1 - low_rank_params / original_params) * 100
    print(f"{rank:>8} | {low_rank_params:>14,} | {reduction:>9.2f}%")

print("\n=== 次元を増やすと分離しやすくなる例 ===")
np.random.seed(1)

n = 200
angles = np.random.uniform(0, 2*np.pi, n)
r_inner = np.random.uniform(0, 1, n // 2)
r_outer = np.random.uniform(2, 3, n // 2)

inner = np.stack([r_inner * np.cos(angles[:n//2]), r_inner * np.sin(angles[:n//2])], axis=1)
outer = np.stack([r_outer * np.cos(angles[n//2:]), r_outer * np.sin(angles[n//2:])], axis=1)

print("2次元での分布:")
print(f"  内側の点の、原点からの距離: {np.linalg.norm(inner, axis=1).min():.2f} 〜 {np.linalg.norm(inner, axis=1).max():.2f}")
print(f"  外側の点の、原点からの距離: {np.linalg.norm(outer, axis=1).min():.2f} 〜 {np.linalg.norm(outer, axis=1).max():.2f}")
print("  x座標だけ、あるいはy座標だけでは、直線1本で分離できません。")

inner_3d = np.column_stack([inner, (inner**2).sum(axis=1)])
outer_3d = np.column_stack([outer, (outer**2).sum(axis=1)])

print("\n3次元目(距離の2乗)を追加すると:")
print(f"  内側の点の3次元目: {inner_3d[:,2].min():.2f} 〜 {inner_3d[:,2].max():.2f}")
print(f"  外側の点の3次元目: {outer_3d[:,2].min():.2f} 〜 {outer_3d[:,2].max():.2f}")
print("  3次元目の値で、きれいに分離できます。")

初学者がつまずきやすい点

つまずき1:拡大すると情報が増えると思う

次元を増やしても、情報量は増えません。

もとの情報を、別の形で表現し直しているだけです。

拡大は情報を増やす操作ではなく、表現を変える操作である、と明確に押さえてください。

つまずき2:縮小は常に悪いことだと思う

情報が失われるため、縮小は損失のように感じられます。

しかし、不要な情報を捨てることは、目的達成のために有益です。

顔認識であれば、背景の情報は不要です。捨てることで、本質的な特徴に集中できます。

つまずき3:拡大と縮小の意味を、活性化関数と切り離して考える

線形変換だけであれば、768 \rightarrow 3072 \rightarrow 768 は、768次元から768次元への一つの変換と等価です。

拡大に意味があるのは、間に非線形な処理が挟まるからです。

この点を理解していないと、なぜわざわざ拡大するのかが、腑に落ちません。

演習

演習1:次元の変化を答える

次の行列とベクトルの積について、結果の次元数を答えてください。また、拡大、縮小、変化なしのいずれかを判定してください。

(5, 10) の行列と、10次元のベクトル

(10, 5) の行列と、5次元のベクトル

(768, 768) の行列と、768次元のベクトル

(3072, 768) の行列と、768次元のベクトル

演習2:情報の損失を確認する

次の行列による変換で、[2, 3, 4] と同じ結果になる別のベクトルを、一つ挙げてください。

\begin{pmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \end{pmatrix}

また、次の行列の場合はどうでしょうか。

\begin{pmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix}

演習3:パラメータ数を比較する

512次元から2048次元への拡大と、2048次元から512次元への縮小を行う二つの行列があります。

それぞれのパラメータ数と、合計を計算してください。

また、これを512次元から512次元への一つの行列で置き換えた場合のパラメータ数と比較してください。

演習4:低ランク近似を実装する

import numpy as np

np.random.seed(0)

d_in, d_out = 768, 3072
W = np.random.randn(d_out, d_in) * 0.02
original_params = W.size

print(f"元の行列: 形{W.shape}, パラメータ数 {original_params:,}")

print(f"\n{'ランク':>6} | {'パラメータ数':>12} | {'削減率':>8} | {'近似誤差':>10}")
print("-" * 46)

U, s, Vt = np.linalg.svd(W, full_matrices=False)

for rank in [4, 8, 16, 32, 64, 128]:
    A = U[:, :rank] * s[:rank]
    B = Vt[:rank, :]
    W_approx = A @ B
    
    params = A.size + B.size
    reduction = (1 - params / original_params) * 100
    error = np.linalg.norm(W - W_approx) / np.linalg.norm(W)
    
    print(f"{rank:>6} | {params:>12,} | {reduction:>7.2f}% | {error:>10.4f}")

print("\nランクを上げるほど近似は正確になりますが、パラメータ数も増えます。")

演習の解答例と解説

演習1の解答は、次のとおりです。

(5, 10) と10次元は、結果が5次元です。10次元から5次元への縮小です。

(10, 5) と5次元は、結果が10次元です。5次元から10次元への拡大です。

(768, 768) と768次元は、結果が768次元です。変化なしです。

(3072, 768) と768次元は、結果が3072次元です。拡大です。これは、GPT-2のMLP層の前半に相当します。

演習2の解答は、次のとおりです。

一つ目の行列では、3番目の要素が捨てられます。したがって、[2, 3, 100] でも [2, 3, -50] でも、結果は [2, 3] になります。3番目の要素が何であっても構いません。

二つ目の行列では、1行目が1番目と2番目の合計、2行目が3番目の要素です。

[2, 3, 4] の結果は [5, 4] です。

同じ結果になる別のベクトルとしては、1番目と2番目の合計が5であればよいため、[1, 4, 4][0, 5, 4] などが挙げられます。

この行列では、1番目と2番目の個別の値は失われますが、その合計は保持されています。何を残し、何を捨てるかが、行列によって決まることが確認できます。

演習3の解答は、次のとおりです。

拡大の行列は、2048行512列です。

2048 \times 512 = 1048576

約105万個です。

縮小の行列は、512行2048列です。

512 \times 2048 = 1048576

こちらも約105万個です。

合計は、約210万個です。

一方、512次元から512次元への一つの行列であれば、次のようになります。

512 \times 512 = 262144

約26万個です。

拡大と縮小を組み合わせると、パラメータ数は約8倍になります。

線形変換だけであれば、この8倍のパラメータを使う意味はありません。等価な変換を、8分の1のパラメータで実現できるからです。

間に活性化関数が挟まることで、初めてこの構造に意味が生じます。

演習4では、ランクを上げるほど近似が正確になる一方で、パラメータ数も増えることが確認できます。実務では、この二つのバランスを見て、適切なランクを選びます。

講師向けの補足

この章の核心は、拡大では情報が増えないという点です。

多くの受講者は、次元が増えれば情報も増えると直感的に考えます。この誤解を、明示的に解いてください。

効果的な方法は、演習コードにあるように、拡大後の3番目の要素が常に1番目と2番目の合計であることを、複数の例で示すことです。新しい情報は一つもない、という事実が、数値で確認できます。

また、では、なぜ拡大するのかという問いへの答えを、この章では保留にしてください。完全な答えは、第13章の活性化関数の話とセットでなければ与えられません。

線形変換だけなら拡大に意味はない、しかし実際のモデルは拡大している、ということは線形変換以外の何かが関わっているはずだ、という形で、次章への期待を作ると効果的です。

低ランク近似とLoRAの話は、興味を引く話題です。ただし、深入りすると本筋から外れるため、紹介にとどめてください。

理解度を確認する問い

第12章の内容を、次の問いで確認してみてください。

次元を縮小する変換では、必ず情報が失われるでしょうか。また、それは常に悪いことでしょうか。

次元を拡大すると、情報量は増えるでしょうか。理由も説明してください。

768 \rightarrow 3072 \rightarrow 768 という構造が、線形変換だけでは意味を持たない理由を説明してください。

行列のランクとは、何を表す値でしょうか。

低ランク近似によって、なぜパラメータ数を削減できるのでしょうか。

まとめ

行列の形によって、変換前後の次元数が決まります。行数が列数より多ければ拡大、少なければ縮小です。

次元の縮小では、必ず情報が失われます。異なる入力が同じ出力になり、元の値を復元できません。ただし、何を残し何を捨てるかは行列の内容によって決まり、学習を通じて、目的に重要な情報を残すような行列が獲得されます。

次元の拡大では、情報量は増えません。追加された要素は、既存の要素から計算されたものです。拡大が意味を持つのは、情報を増やすためではなく、情報の表現の仕方を変えるためです。より広い空間に配置することで、分離や処理が容易になる場合があります。

行列のランクは、その変換が保持できる情報の上限を表します。ランクは、行数と列数の小さいほうを超えることはありません。この性質から、拡大で情報が増えないことが確認できます。

768 \rightarrow 3072 \rightarrow 768 という構造は、線形変換だけであれば、768次元から768次元への一つの変換と等価であり、パラメータを8倍使う意味がありません。この構造に意味が生じるのは、間に非線形な処理が挟まるからです。

次の第13章では、その非線形な処理、すなわち活性化関数を扱います。線形変換だけでは何が足りないのか、なぜ層を重ねる意味がなくなってしまうのかを、数式と具体例で確認します。線形代数の限界を知ることで、かえって線形代数の役割が明確になります。

第13章に進む前に、演習4のコードを実行し、ランクとパラメータ数、近似誤差の関係を確認しておいてください。行列が持つ情報量という考え方が、実感を伴って理解できるはずです。

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
海外放浪の末、2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。