第10章 形を合わせる:転置、reshape、ブロードキャスト

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

第9章までで、線形代数の主要な計算は一通り揃いました。第10章は、少し性格の異なる章です。理論というより、実務のための章です。ディープラーニングのコードを書き始めると、最も頻繁に遭遇するのが、形が合わないというエラーです。私の経験でも、初学者がつまずいて先に進めなくなる原因の大半は、このエラーです。しかも、厄介なことに、形が合っていないのにエラーにならず、間違った結果が出るという場合もあります。この章では、形を操作する三つの道具、転置、reshape、ブロードキャストを扱います。この連載の中で、最も即効性のある内容になります。

転置

転置とは何か

転置とは、行列の行と列を入れ替える操作です。

A = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}

この行列を転置すると、次のようになります。

A^T = \begin{pmatrix} 1 & 4 \\ 2 & 5 \\ 3 & 6 \end{pmatrix}

2行3列だった行列が、3行2列になりました。

元の1行目である [1, 2, 3] が、転置後の1列目になっています。

記法

転置は、右肩にTを付けて表します。

A^T

ダッシュを付ける流儀もありますが、ディープラーニングの文献ではTが一般的です。

形の変化

(m, n) の行列を転置すると、(n, m) になります。

行数と列数が入れ替わるだけです。要素の総数は変わりません。

転置の性質

いくつかの重要な性質があります。

二回転置すると、元に戻ります。

(A^T)^T = A

積の転置は、順序が逆になります。

(AB)^T = B^T A^T

この二つ目の性質は、第15章で誤差逆伝播を扱う際に、重要な役割を果たします。

なぜ転置が必要になるのか

行列積では、左の列数と右の行数が一致していなければなりません。

形が合わない場合、片方を転置することで、合わせられることがあります。

たとえば、(32, 768) と (3072, 768) は、そのままでは掛けられません。内側の768と3072が一致していないためです。

しかし、右側を転置して (768, 3072) にすれば、掛けられます。

(32, 768) \times (768, 3072) \rightarrow (32, 3072)

第9章で XW^T と書いた理由が、これです。

PyTorchのnn.Linearにおける転置

ここで、実務でよく混乱する点を扱います。

PyTorchで nn.Linear(768, 3072) を作ると、内部の weight は (3072, 768) の形で保持されます。

(output_dim, input_dim)

一方、入力データは (batch_size, 768) の形で渡されます。

このままでは掛けられないため、内部で weight を転置してから計算しています。

# PyTorchの内部で行われている計算のイメージ
output = input @ weight.T + bias

なぜ最初から (768, 3072) の形で保持しないのでしょうか。

理由は、実装上の慣例と、メモリ配置の効率によるものです。深く追う必要はありませんが、weight の形は出力次元が先、という事実は覚えておいてください。

reshape

reshapeとは何か

reshapeとは、要素の総数を変えずに、形を変える操作です。

12個の要素があるとき、次のような形にできます。

(12,) という1次元

(3, 4) という2次元

(4, 3) という2次元

(2, 6) という2次元

(2, 2, 3) という3次元

いずれも要素数は12で共通しています。

要素の並び順

reshapeで重要なのは、要素がどの順序で並べ直されるかです。

NumPyやPyTorchでは、最後の軸から順に埋めていく方式が標準です。これをC順序と呼びます。

[1, 2, 3, 4, 5, 6] を (2, 3) にreshapeすると、次のようになります。

\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}

横方向に埋めてから、次の行に移ります。

(3, 2) にすると、次のようになります。

\begin{pmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{pmatrix}

マイナス1の指定

reshapeでは、片方の次元にマイナス1を指定できます。

これは、残りは自動で計算してください、という意味です。

要素数が12のとき、reshape(3, -1) とすれば、自動的に (3, 4) になります。

バッチサイズが可変の場合など、実務では頻繁に使われます。

転置とreshapeの違い

ここが、最もつまずきやすい点です。

(2, 3) の行列を (3, 2) にする方法は、二つあります。転置とreshapeです。

しかし、結果はまったく異なります。

A = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}

転置すると、次のようになります。

A^T = \begin{pmatrix} 1 & 4 \\ 2 & 5 \\ 3 & 6 \end{pmatrix}

reshapeすると、次のようになります。

\begin{pmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{pmatrix}

形は同じ (3, 2) ですが、中身が違います。

転置は、要素の位置関係を保ったまま、行と列の役割を入れ替えます。

reshapeは、要素を一列に並べ直してから、指定の形に詰め直します。

この違いを理解していないと、形は合っているのに結果がおかしい、という状況に陥ります。しかもエラーにならないため、原因の特定が困難です。

ディープラーニングでの用途

reshapeは、次のような場面で使われます。

画像を全結合層に入力する際の平坦化です。(28, 28) の画像を (784,) にします。

複数のヘッドに分割する処理です。(32, 128, 768) を (32, 128, 12, 64) にします。Transformerのマルチヘッド注意で使われます。

バッチ軸と系列軸をまとめる処理です。(32, 128, 768) を (4096, 768) にして、全結合層に通します。

ブロードキャスト

ブロードキャストとは何か

ブロードキャストとは、形の異なるテンソルどうしの演算を、自動的に形を合わせて実行する仕組みです。

最も単純な例は、スカラーとの演算です。

x = np.array([1, 2, 3])
y = x + 10  # [11, 12, 13]

10という1個の数値が、すべての要素に加算されています。

これは、10が [10, 10, 10] に自動的に拡張されたと考えられます。

行列とベクトルの場合

もう少し複雑な例を見ます。

(3, 4) の行列と、(4,) のベクトルを足すとします。

ブロードキャストにより、ベクトルが3行分に複製され、各行に加算されます。

第9章で、バイアスの加算がこの仕組みで動いていることに触れました。

Y = XW^T + \mathbf{b}

XW^T が (32, 3072)、\mathbf{b} が (3072,) です。ブロードキャストにより、バイアスが32行すべてに加算されます。

ブロードキャストの規則

ブロードキャストが成立する条件は、次のとおりです。

二つのテンソルの形を、右端から順に比較します。

各位置について、次のいずれかであれば、その軸は適合します。

両者の大きさが等しい。

どちらかの大きさが1である。

どちらかにその軸が存在しない。

すべての軸が適合すれば、ブロードキャストが成立します。

具体例で確認する

適合する例を見ます。

(3, 4) と (4,) の場合。右端から比較すると、4と4で一致します。次の軸は、左側だけに3があります。存在しない側は適合とみなされるため、成立します。結果は (3, 4) です。

(3, 1) と (1, 4) の場合。右端は1と4で、片方が1なので適合します。次は3と1で、これも片方が1なので適合します。結果は (3, 4) です。

適合しない例を見ます。

(3, 4) と (3,) の場合。右端から比較すると、4と3で一致せず、どちらも1ではありません。したがって、エラーになります。

この最後の例は、直感に反すると感じるかもしれません。3行あるから3個の要素を各行に足せるのでは、と考えがちですが、比較は右端から行われるため、成立しないのです。

ブロードキャストの危険性

ブロードキャストは便利な機能ですが、意図しない計算が行われる原因にもなります。

たとえば、(3, 1) と (1, 3) を足すと、(3, 3) になります。

3個の要素と3個の要素を足したつもりが、9個の要素を持つ行列ができてしまいます。

しかも、エラーにはなりません。計算は正常に完了し、それらしい数値が出ます。

この種のバグは、発見が非常に困難です。予防策は、計算の要所で shape を表示することに尽きます。

エラーメッセージの読み方

実務で遭遇する典型的なエラーを、読み解いてみます。

例1:行列積の形が合わない

RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x128 and 256x64)

読み方は、次のとおりです。

一つ目の行列は32行128列、二つ目は256行64列です。

行列積では、一つ目の列数と二つ目の行数が一致していなければなりません。

128と256が一致していないため、エラーになっています。

対処法は、次のいずれかです。

一つ目の行列の列数を256にする。

二つ目の行列の行数を128にする。

いずれかを転置して形を合わせる。

例2:ブロードキャストできない

ValueError: operands could not be broadcast together with shapes (3,4) (3,)

読み方は、次のとおりです。

(3, 4) と (3,) をブロードキャストしようとして失敗しています。

右端から比較すると、4と3が一致せず、どちらも1ではありません。

対処法は、(3,) を (3, 1) にreshapeすることです。そうすれば、右端は4と1で適合し、次は3と3で一致します。

例3:reshapeできない

RuntimeError: shape '[3, 5]' is invalid for input of size 12

読み方は、次のとおりです。

要素数12のテンソルを、(3, 5) にしようとしています。

しかし、次のように、15と12は一致しません。

3 \times 5 = 15

reshapeでは要素の総数が変わらないため、この変換は不可能です。

対処法は、要素数を確認し、その約数の組み合わせで形を指定することです。12であれば、(3, 4)、(4, 3)、(2, 6)、(6, 2)、(2, 2, 3) などが可能です。

形を合わせる手順

エラーに遭遇したときの、体系的な対処手順を示します。

第一に、エラーメッセージから、二つのテンソルの形を読み取ります。

第二に、自分が意図していた計算を、形の変化として書き出します。(32, 128) と (128, 64) を掛けて (32, 64) を得たい、といった形です。

第三に、実際の形と、意図した形を比較します。どこがずれているかを特定します。

第四に、ずれの原因を考えます。転置が必要なのか、reshapeが必要なのか、そもそも前の処理が間違っているのか。

第五に、修正して、shape を表示して確認します。

この手順を踏めば、当てずっぽうで数値を変える必要はありません。

Pythonで確認する

import numpy as np

print("=== 転置 ===")
A = np.array([[1, 2, 3],
              [4, 5, 6]])
print(f"A(形{A.shape}):")
print(A)
print(f"\nA.T(形{A.T.shape}):")
print(A.T)
print(f"\n二回転置すると元に戻るか: {np.array_equal(A.T.T, A)}")

print("\n=== 積の転置の性質 ===")
B = np.array([[1, 0],
              [0, 1],
              [1, 1]])
print(f"A(形{A.shape}) @ B(形{B.shape})→ 形{(A @ B).shape}")
left = (A @ B).T
right = B.T @ A.T
print(f"(AB)^T:")
print(left)
print(f"B^T A^T:")
print(right)
print(f"一致するか: {np.array_equal(left, right)}")

print("\n=== reshape ===")
v = np.arange(1, 13)
print(f"元のベクトル(形{v.shape}): {v}")

for shape in [(3, 4), (4, 3), (2, 6), (2, 2, 3)]:
    reshaped = v.reshape(shape)
    print(f"\nreshape{shape}:")
    print(reshaped)

print("\n=== -1の使用 ===")
print(f"reshape(3, -1) → 形{v.reshape(3, -1).shape}")
print(f"reshape(-1, 6) → 形{v.reshape(-1, 6).shape}")
print(f"reshape(2, -1, 3) → 形{v.reshape(2, -1, 3).shape}")

print("\n=== 転置とreshapeの決定的な違い ===")
M = np.array([[1, 2, 3],
              [4, 5, 6]])
print(f"元の行列(形{M.shape}):")
print(M)
print(f"\n転置(形{M.T.shape}):")
print(M.T)
print(f"\nreshape(3, 2)(形{M.reshape(3, 2).shape}):")
print(M.reshape(3, 2))
print("\n形は同じ(3,2)ですが、中身がまったく違います。")

print("\n=== ブロードキャストの基本 ===")
x = np.array([1, 2, 3])
print(f"x = {x}")
print(f"x + 10 = {x + 10}")
print("スカラーが全要素に拡張されました。")

print("\n=== 行列とベクトルのブロードキャスト ===")
X = np.array([[1, 2, 3, 4],
              [5, 6, 7, 8],
              [9, 10, 11, 12]])
b = np.array([100, 200, 300, 400])
print(f"X(形{X.shape}):")
print(X)
print(f"b(形{b.shape}): {b}")
print(f"\nX + b(形{(X + b).shape}):")
print(X + b)
print("bが3行すべてに加算されました。")

print("\n=== ブロードキャストの成立条件 ===")
test_pairs = [
    ((3, 4), (4,)),
    ((3, 1), (1, 4)),
    ((3, 4), (3, 1)),
    ((3, 4), (3,)),
    ((2, 3, 4), (4,)),
]

for shape_a, shape_b in test_pairs:
    a = np.zeros(shape_a)
    b = np.zeros(shape_b)
    try:
        result = a + b
        print(f"{shape_a} + {shape_b} → {result.shape}  成立")
    except ValueError:
        print(f"{shape_a} + {shape_b} → エラー  不成立")

print("\n=== ブロードキャストの危険な例 ===")
a = np.array([1, 2, 3]).reshape(3, 1)
b = np.array([10, 20, 30]).reshape(1, 3)
print(f"a(形{a.shape}):\n{a}")
print(f"b(形{b.shape}): {b}")
result = a + b
print(f"\na + b(形{result.shape}):")
print(result)
print("3要素と3要素を足したつもりが、9要素の行列になっています。")
print("エラーにならないため、気づきにくいバグの原因になります。")

print("\n=== エラーメッセージの実例 ===")
print("--- 行列積の形が合わない ---")
try:
    np.zeros((32, 128)) @ np.zeros((256, 64))
except ValueError as e:
    print(f"{e}")

print("\n--- reshapeできない ---")
try:
    np.arange(12).reshape(3, 5)
except ValueError as e:
    print(f"{e}")

print("\n=== 実務でよくある形の操作 ===")
print("--- 画像の平坦化 ---")
image = np.random.randn(28, 28)
flat = image.reshape(-1)
print(f"{image.shape} → {flat.shape}")

print("\n--- バッチ画像の平坦化 ---")
batch_images = np.random.randn(32, 1, 28, 28)
flat_batch = batch_images.reshape(32, -1)
print(f"{batch_images.shape} → {flat_batch.shape}")

print("\n--- マルチヘッドへの分割 ---")
x = np.random.randn(32, 128, 768)
n_heads = 12
head_dim = 768 // n_heads
split = x.reshape(32, 128, n_heads, head_dim)
print(f"{x.shape} → {split.shape}  ({n_heads}ヘッド × {head_dim}次元)")

print("\n--- バッチと系列をまとめる ---")
merged = x.reshape(-1, 768)
print(f"{x.shape} → {merged.shape}")

print("\n=== PyTorchのnn.Linearの内部 ===")
try:
    import torch
    import torch.nn as nn
    
    linear = nn.Linear(768, 3072)
    print(f"nn.Linear(768, 3072)")
    print(f"  weight の形: {linear.weight.shape}  (出力次元, 入力次元)")
    print(f"  内部では weight.T(形{linear.weight.T.shape})が使われます")
    
    x_in = torch.randn(32, 768)
    y_out = linear(x_in)
    print(f"\n入力{tuple(x_in.shape)} → 出力{tuple(y_out.shape)}")
    
    manual = x_in @ linear.weight.T + linear.bias
    print(f"手動計算と一致するか: {torch.allclose(y_out, manual, atol=1e-6)}")
except ImportError:
    print("PyTorchがインストールされていないため、この部分はスキップします。")

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

つまずき1:転置とreshapeを同じものだと思う

先に説明したとおり、形は同じでも中身が違います。

判断基準は、要素の位置関係を保ちたいかどうかです。

行と列の役割を入れ替えたいなら、転置です。

単に形を詰め直したいなら、reshapeです。

つまずき2:ブロードキャストの比較が右端からであることを忘れる

(3, 4) と (3,) が足せないのは、直感に反します。

右端から比較するという規則を、明示的に覚えてください。

各行に3要素を足したい場合は、(3,) を (3, 1) にreshapeします。

つまずき3:エラーメッセージを読まずに数値をいじる

エラーメッセージには、二つのテンソルの形が明記されています。

この情報を読めば、何が問題かは特定できます。

読まずに当てずっぽうで修正すると、時間を浪費するだけでなく、別の箇所に問題を先送りすることになります。

演習

演習1:転置とreshapeの違いを確認する

次の行列について、転置した結果と、(3, 2) にreshapeした結果を、それぞれ書き出してください。

\begin{pmatrix} 1 & 3 & 5 \\ 2 & 4 & 6 \end{pmatrix}

演習2:ブロードキャストの成否を判定する

次の組み合わせについて、ブロードキャストが成立するかを判定し、成立する場合は結果の形を答えてください。

(5, 3) と (3,)

(5, 3) と (5,)

(5, 1) と (1, 3)

(2, 3, 4) と (3, 4)

(2, 3, 4) と (2, 4)

演習3:エラーを修正する

次のコードは、エラーになります。原因を特定し、修正してください。

import numpy as np

X = np.random.randn(32, 784)
W = np.random.randn(128, 784)
b = np.random.randn(128)

y = X @ W + b

演習4:形の変換を実装する

import numpy as np

print("=== 課題1:画像バッチを全結合層に通す ===")
images = np.random.randn(64, 3, 32, 32)
print(f"入力画像: {images.shape}")

flattened = images.reshape(64, -1)
print(f"平坦化後: {flattened.shape}")

W = np.random.randn(256, flattened.shape[1]) * 0.01
b = np.zeros(256)
output = flattened @ W.T + b
print(f"全結合層通過後: {output.shape}")

print("\n=== 課題2:マルチヘッドへの分割と復元 ===")
x = np.random.randn(8, 64, 768)
n_heads = 12
head_dim = 768 // n_heads
print(f"元の形: {x.shape}")

split = x.reshape(8, 64, n_heads, head_dim)
print(f"ヘッド分割後: {split.shape}")

transposed = split.transpose(0, 2, 1, 3)
print(f"ヘッドを前に移動: {transposed.shape}")

back = transposed.transpose(0, 2, 1, 3).reshape(8, 64, 768)
print(f"元に戻した形: {back.shape}")
print(f"元と一致するか: {np.allclose(x, back)}")

print("\n=== 課題3:バイアスの正しい加算 ===")
X = np.random.randn(5, 4)
b_correct = np.random.randn(4)
b_wrong = np.random.randn(5)

print(f"X の形: {X.shape}")
print(f"正しいバイアス(列方向)の形: {b_correct.shape}")
result = X + b_correct
print(f"加算結果: {result.shape}  成功")

print(f"\n行ごとに異なる値を足したい場合: {b_wrong.shape}")
try:
    X + b_wrong
except ValueError as e:
    print(f"そのままではエラー: {e}")

result2 = X + b_wrong.reshape(-1, 1)
print(f"reshape(-1, 1)してから加算: {result2.shape}  成功")

演習の解答例と解説

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

転置した結果。

\begin{pmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{pmatrix}

元の1行目である [1, 3, 5] が、転置後の1列目になっています。

reshapeした結果。

\begin{pmatrix} 1 & 3 \\ 5 & 2 \\ 4 & 6 \end{pmatrix}

要素を横方向に読んで [1, 3, 5, 2, 4, 6] とし、それを2個ずつ詰め直しています。

まったく異なる結果になることが確認できます。

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

(5, 3) と (3,) は成立します。右端が3と3で一致します。結果は (5, 3) です。

(5, 3) と (5,) は成立しません。右端が3と5で一致せず、どちらも1ではありません。

(5, 1) と (1, 3) は成立します。右端が1と3で片方が1、次が5と1で片方が1です。結果は (5, 3) です。

(2, 3, 4) と (3, 4) は成立します。右端から4と4、3と3で一致し、残りは片方に存在しないだけです。結果は (2, 3, 4) です。

(2, 3, 4) と (2, 4) は成立しません。右端は4と4で一致しますが、次が3と2で一致せず、どちらも1ではありません。

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

エラーの原因は、X @ W の部分です。

Xの形は (32, 784)、Wの形は (128, 784) です。

行列積では、Xの列数784と、Wの行数128が一致していなければなりませんが、一致していません。

修正方法は、Wを転置することです。

y = X @ W.T + b

(32, 784) \times (784, 128) \rightarrow (32, 128)

バイアスbの形は (128,) であり、ブロードキャストにより32行すべてに加算されます。

演習4では、実務でよく使う形の操作が確認できます。

課題1では、(64, 3, 32, 32) という4次元のデータを、(64, 3072) という2次元に平坦化しています。バッチ軸を残し、それ以外をまとめるという操作です。

課題2では、Transformerのマルチヘッド注意で使われる分割と復元を確認しています。転置を二回行うと元に戻ることも確認できます。

課題3では、バイアスの加算において、列方向と行方向で扱いが異なることを確認しています。行ごとに異なる値を足したい場合は、reshapeで軸を明示する必要があります。

講師向けの補足

この章は、理論より実践の章です。演習に多くの時間を割いてください。

特に効果的なのは、受講者に実際にエラーを起こさせることです。わざと形の合わないコードを書かせ、エラーメッセージを読ませ、修正させる。この流れを何度か繰り返すと、対処の型が身につきます。

転置とreshapeの違いは、必ず数値で示してください。形は同じだが中身が違うという事実は、実際に出力を見比べないと実感されません。

ブロードキャストの危険な例、つまり (3, 1) と (1, 3) を足すと (3, 3) になるという例は、必ず紹介してください。エラーにならないバグの怖さを知っておくことが、予防につながります。

また、困ったら shape を表示するという習慣を、繰り返し強調してください。これが、最も実務的な助言です。

理解度を確認する問い

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

転置とreshapeの違いを、要素の並び方という観点から説明してみてください。

(AB)^T は、どのように書き換えられるでしょうか。

ブロードキャストが成立する条件を、三つ挙げてください。

(3, 4) と (3,) を足そうとするとエラーになります。各行に3個の値を足したい場合、どうすればよいでしょうか。

nn.Linear(768, 3072) の weight の形はどうなっているでしょうか。また、なぜ内部で転置が必要になるのでしょうか。

まとめ

転置は、行列の行と列を入れ替える操作です。(m, n) が (n, m) になります。二回転置すると元に戻り、積の転置は (AB)^T = B^TA^T という性質を持ちます。行列積で形が合わないとき、転置によって合わせられることがあります。

reshapeは、要素の総数を変えずに形を変える操作です。要素は最後の軸から順に埋められます。マイナス1を指定すると、残りの次元が自動的に計算されます。

転置とreshapeは、同じ形にできても中身が異なります。転置は要素の位置関係を保ち、reshapeは一列に並べ直してから詰め直します。この違いを理解していないと、エラーにならないまま誤った結果が出ます。

ブロードキャストは、形の異なるテンソルどうしの演算を、自動的に形を合わせて実行する仕組みです。右端の軸から順に比較し、大きさが等しいか、どちらかが1か、どちらかに存在しなければ適合します。便利な機能ですが、意図しない計算が行われる原因にもなります。

エラーに遭遇したときは、メッセージから形を読み取り、意図していた形と比較し、ずれの原因を特定するという手順を踏んでください。当てずっぽうで数値を変えるより、はるかに速く解決できます。

そして最も実務的な助言は、計算の要所で shape を表示することです。これが、形に関するバグを防ぐ、最も確実な方法です。

次の第11章からは、第III部に入ります。ここまでは計算の手順を扱ってきましたが、第11章では、行列を掛けるという操作が、空間の中で何を起こしているのかを扱います。2次元の図を使って、回転、伸縮、射影といった変換を目で見ながら、線形変換の意味を理解していきます。

第11章に進む前に、演習3のエラー修正を、実際にコードを書いて確認しておいてください。エラーメッセージを読んで原因を特定するという経験が、実務での対処力に直結します。

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

投稿者プロフィール

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

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