第3章 行列とテンソル:数値の並びを積み重ねる

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

第2章では、スカラーとベクトルを扱いました。数値1個と、数値が1列に並んだもの。ここまでは、比較的すんなり理解できたのではないでしょうか。第3章では、その並びをさらに積み重ねていきます。ベクトルを何本も並べると行列になり、行列をさらに積み重ねるとテンソルになります。ディープラーニングのコードを書いていると、torch.Size([32, 128, 768]) といった表示を頻繁に目にします。この三つの数字が何を表しているのかを読み解けるようになることが、この章の目標です。実は、実務で最も多くの時間を奪われるエラーの原因は、この形の理解不足にあります。

行列とは何か

行列とは、数値を縦横の格子状に並べたものです。

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

この例では、横に3個、縦に2個の数値が並んでいます。

横の並びを行、縦の並びを列と呼びます。上の例は、2行3列の行列です。

行と列の覚え方

行と列を取り違える人は、非常に多くいます。

覚え方として、漢字の形を使う方法があります。行という字には横棒が多く含まれており、横方向を表します。列という字には縦棒が多く含まれており、縦方向を表します。

もう一つの方法は、数え方の順序を口に出して確認することです。行列の大きさは、必ず行が先、列が後という順序で表します。2行3列であれば、縦に2、横に3です。

この順序は、後の章で行列の掛け算を扱う際に、決定的に重要になります。ここで確実に押さえてください。

要素の指定方法

行列の各要素は、行と列の番号で指定します。

a_{ij}

iが行番号、jが列番号です。行が先、列が後という順序は、大きさを表すときと同じです。

先ほどの例で言えば、a_{23} は2行目3列目の要素であり、値は6です。a_{12} は1行目2列目の要素であり、値は2です。

なお、数学では1から数え始めるのに対し、プログラミングでは0から数え始めるのが一般的です。数式の a_{11} は、Pythonでは A[0, 0] に対応します。この対応は、数式をコードに翻訳する際に、間違いやすい箇所です。

形の表記

プログラムでは、行列の大きさを shape として表現します。

2行3列の行列であれば、(2, 3) と表示されます。

この表記も、行が先、列が後です。数学の記法と一致しています。

行列の特別な形

いくつかの特別な形には、名前が付いています。

正方行列

行数と列数が等しい行列です。

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

ディープラーニングでは、入力と出力の次元数が同じ層で登場します。第1章で触れた自己注意のQuery、Key、Valueを生成する行列は、768行768列の正方行列です。

単位行列

対角線上が1、それ以外が0の正方行列です。

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

この行列を掛けても、相手は変化しません。数値の1と同じ役割を果たします。

AI = IA = A

第15章で誤差逆伝播を扱う際、残差接続の微分にこの単位行列が現れます。深いネットワークの学習を支える、重要な役割を持つ行列です。

対角行列

対角線上以外がすべて0の行列です。単位行列は、対角行列の特別な場合です。

D = \begin{pmatrix} 2 & 0 & 0 \\ 0 & 5 & 0 \\ 0 & 0 & 3 \end{pmatrix}

第11章で扱いますが、この形の行列は、各方向を独立に伸縮させる変換に対応します。

ゼロ行列

すべての要素が0の行列です。

O = \begin{pmatrix} 0 & 0 \\ 0 & 0 \end{pmatrix}

バイアスの初期値として使われることがあります。

テンソルとは何か

テンソルとは、数値の並びを、より一般的に扱うための言葉です。

軸の数によって、次のように対応します。

軸が0本であれば、スカラーです。

軸が1本であれば、ベクトルです。

軸が2本であれば、行列です。

軸が3本以上であれば、一般にテンソルと呼ばれます。

ただし、実際のディープラーニングの文脈では、スカラーもベクトルも行列も、まとめてテンソルと呼ぶことが一般的です。PyTorchでは、すべてのデータが torch.Tensor という型で扱われます。

したがって、テンソルという言葉が出てきたら、軸が何本かは分からないが数値の並びである、という程度に受け取っておけば十分です。

3次元テンソルの具体例

軸が3本のテンソルを、具体例で考えます。

カラー画像は、縦、横、色の三つの軸を持ちます。

縦が224画素、横が224画素、色が赤緑青の3種類であれば、形は (224, 224, 3) となります。

これは、224行224列の行列が、3枚重なったものと考えることもできます。

4次元テンソルの具体例

画像を複数枚まとめて処理する場合、軸がもう1本増えます。

32枚の画像をまとめて処理するなら、形は (32, 224, 224, 3) となります。

一番外側の軸が、何枚目の画像かを表します。この軸を、バッチ軸と呼びます。

ディープラーニングで頻出する形

実務でよく目にする形を、整理します。

表形式データ

(batch_size, num_features)

たとえば (64, 10) であれば、64件のデータがあり、それぞれ10個の特徴量を持つ、という意味です。

画像データ

PyTorchでは、次の順序が標準です。

(batch_size, channels, height, width)

たとえば (32, 3, 224, 224) であれば、32枚のカラー画像、それぞれ224画素四方、という意味です。

注意点として、ライブラリによって軸の順序が異なります。TensorFlowでは、チャンネルが最後に来る (32, 224, 224, 3) という順序が標準です。

この違いは、複数のライブラリを併用する際に、混乱の原因になります。使っているライブラリの規約を、必ず確認してください。

言語データ

(batch_size, sequence_length, embedding_dim)

たとえば (16, 512, 768) であれば、16件の文章、それぞれ最大512トークン、各トークンが768次元のベクトル、という意味です。

第1章で例に挙げた torch.Size([32, 128, 768]) は、この形です。32件の文章、それぞれ128トークン、各トークンが768次元、という読み方になります。

重み行列

(output_dim, input_dim)

あるいは、実装によっては逆の順序になります。

PyTorchの nn.Linear では、内部的に出力次元が先、入力次元が後の順で保持されています。この点は、第10章で転置を扱う際に、あらためて説明します。

要素数とメモリ量の計算

形が分かると、要素の総数が計算できます。すべての軸の大きさを掛け合わせるだけです。

形が (32, 128, 768) であれば、次のようになります。

32 \times 128 \times 768 = 3145728

約315万個の数値が含まれています。

メモリ量への換算

1個の数値が何バイトを占めるかは、データ型によって決まります。

float32であれば、1個あたり4バイトです。

float16であれば、1個あたり2バイトです。

先ほどの例をfloat32で保持する場合、必要なメモリは次のようになります。

3145728 \times 4 = 12582912

約12.6メガバイトです。

この計算は、実務で頻繁に必要になります。バッチサイズを大きくしたらメモリ不足になった、という状況で、どれくらい減らせばよいかを見積もる際に使います。

また、モデルのパラメータ数からメモリ量を計算することもできます。1億2400万個のパラメータをfloat32で保持するなら、次のようになります。

124000000 \times 4 = 496000000

約496メガバイトです。学習時には、勾配や最適化アルゴリズムの状態も保持する必要があるため、実際にはこの数倍のメモリが必要になります。この点は、第14章で詳しく扱います。

軸の順序を意識する

テンソルを扱う際、最も重要なのは、各軸が何を表しているかを常に意識することです。

形が (32, 128, 768) と表示されたとき、それぞれの数字が何を意味するのかを、自分で説明できなければなりません。

言語モデルであれば、32はバッチサイズ、128は系列長、768は埋め込み次元です。

もし (128, 32, 768) となっていれば、系列長とバッチサイズが入れ替わっています。この状態で計算を進めると、意味のない結果が出ます。しかも、エラーにならない場合があるため、気づきにくいのです。

実務では、コードの要所で shape を表示させる習慣が、バグの早期発見につながります。

Pythonで確認する

import numpy as np

print("=== スカラーからテンソルまで ===")

# スカラー(軸0本)
s = np.array(5)
print(f"スカラー: 形{s.shape}, 軸の数{s.ndim}, 要素数{s.size}")

# ベクトル(軸1本)
v = np.array([1, 2, 3])
print(f"ベクトル: 形{v.shape}, 軸の数{v.ndim}, 要素数{v.size}")

# 行列(軸2本)
m = np.array([[1, 2, 3],
              [4, 5, 6]])
print(f"行列: 形{m.shape}, 軸の数{m.ndim}, 要素数{m.size}")

# 3次元テンソル(軸3本)
t = np.zeros((2, 3, 4))
print(f"3次元テンソル: 形{t.shape}, 軸の数{t.ndim}, 要素数{t.size}")

print("\n=== 行列の要素へのアクセス ===")
print(f"行列全体:\n{m}")
print(f"1行目: {m[0]}")
print(f"数式のa_11(Pythonではm[0,0]): {m[0, 0]}")
print(f"数式のa_23(Pythonではm[1,2]): {m[1, 2]}")
print(f"1列目全体: {m[:, 0]}")

print("\n=== 特別な行列 ===")
print(f"単位行列(3行3列):\n{np.eye(3)}")
print(f"対角行列:\n{np.diag([2, 5, 3])}")
print(f"ゼロ行列(2行3列):\n{np.zeros((2, 3))}")

print("\n=== 単位行列を掛けても変化しない ===")
A = np.array([[1.0, 2.0], [3.0, 4.0]])
I = np.eye(2)
print(f"A:\n{A}")
print(f"A @ I:\n{A @ I}")
print(f"I @ A:\n{I @ A}")
print(f"どちらもAと一致するか: {np.array_equal(A, A @ I) and np.array_equal(A, I @ A)}")

print("\n=== メモリ量の計算 ===")
data = np.random.randn(32, 128, 768).astype(np.float32)
print(f"形: {data.shape}")
print(f"要素数: {data.size:,}")
print(f"1要素あたりのバイト数: {data.itemsize}")
print(f"合計メモリ: {data.nbytes:,} バイト = {data.nbytes / 1024 / 1024:.2f} MB")

# float16にすると
data16 = data.astype(np.float16)
print(f"float16の場合: {data16.nbytes / 1024 / 1024:.2f} MB")

print("\n=== バッチサイズとメモリ量の関係 ===")
print(f"{'バッチサイズ':>12} | {'要素数':>14} | {'メモリ(MB)':>12}")
print("-" * 44)
for bs in [8, 16, 32, 64, 128]:
    arr = np.zeros((bs, 128, 768), dtype=np.float32)
    print(f"{bs:>12} | {arr.size:>14,} | {arr.nbytes/1024/1024:>12.2f}")

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

つまずき1:行と列を取り違える

2行3列と3行2列は、まったく別の行列です。

\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix} は2行3列です。

\begin{pmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{pmatrix} は3行2列です。

第8章で行列の掛け算を扱う際、この区別ができていないと、まったく計算できません。

対処法として、行列を書くときには、必ず何行何列かを口に出して確認する習慣をつけてください。

つまずき2:ライブラリによる軸の順序の違い

画像データの軸の順序が、PyTorchとTensorFlowで異なることは、先に述べたとおりです。

この違いは、学習済みモデルを別のライブラリで使おうとしたときに、必ず問題になります。

対処法として、データを読み込んだ直後に、必ず shape を確認してください。想定と違えば、その時点で気づけます。

つまずき3:テンソルという言葉に身構える

テンソルという言葉は、物理学や数学では、より厳密な定義を持ちます。座標変換に対する変換則を満たす量、といった定義です。

ディープラーニングの文脈では、そこまで厳密な意味では使われていません。単に多次元の配列という程度の意味です。

数学的に厳密なテンソルの定義を学ぼうとして、深みにはまる必要はありません。この連載では、軸が複数ある数値の並び、という理解で十分です。

演習

演習1:形を読み取る

次の shape が表示されたとき、それぞれの軸が何を表していると考えられるか、答えてください。文脈も併記しています。

(64, 10) は、顧客データを扱うモデルの入力です。

(16, 3, 224, 224) は、PyTorchの画像分類モデルの入力です。

(8, 256, 512) は、言語モデルの中間出力です。

(768, 3072) は、全結合層の重みです。

演習2:要素数とメモリ量を計算する

次のテンソルについて、要素の総数と、float32で保持した場合のメモリ量をメガバイト単位で計算してください。

形が (128, 512, 768) のテンソル。

また、バッチサイズを128から32に減らすと、メモリ量はどう変わるでしょうか。

演習3:要素を指定する

次の行列について、a_{21}a_{13} の値をそれぞれ答えてください。

A = \begin{pmatrix} 7 & 2 & 9 \\ 4 & 8 & 1 \end{pmatrix}

また、それぞれPythonではどのように書くかも答えてください。

演習4:テンソルの操作を確認する

import numpy as np

# 3次元テンソルを作成
t = np.arange(24).reshape(2, 3, 4)

print("=== 元のテンソル ===")
print(f"形: {t.shape}")
print(t)

print("\n=== 軸ごとの取り出し ===")
print(f"0番目の要素(形{t[0].shape}):\n{t[0]}")
print(f"\n1番目の要素(形{t[1].shape}):\n{t[1]}")

print("\n=== 特定の要素 ===")
print(f"t[0, 1, 2] = {t[0, 1, 2]}")
print(f"t[1, 2, 3] = {t[1, 2, 3]}")

print("\n=== 軸ごとの合計 ===")
print(f"全体の合計: {t.sum()}")
print(f"軸0で合計(形{t.sum(axis=0).shape}):\n{t.sum(axis=0)}")
print(f"\n軸2で合計(形{t.sum(axis=2).shape}):\n{t.sum(axis=2)}")

print("\n=== 形の確認 ===")
for name, arr in [("元", t),
                  ("軸0合計", t.sum(axis=0)),
                  ("軸1合計", t.sum(axis=1)),
                  ("軸2合計", t.sum(axis=2))]:
    print(f"{name:>8}: 形{arr.shape}, 軸の数{arr.ndim}")

演習の解答例と解説

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

(64, 10) は、64件の顧客データ、それぞれ10個の特徴量です。

(16, 3, 224, 224) は、16枚の画像、3チャンネル(赤緑青)、高さ224画素、幅224画素です。

(8, 256, 512) は、8件の文章、それぞれ256トークン、各トークンが512次元の埋め込みです。

(768, 3072) は、入力768次元、出力3072次元の全結合層の重みです。ただし、実装によっては (3072, 768) の順序で保持されることもあります。

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

要素の総数は、次のように計算されます。

128 \times 512 \times 768 = 50331648

約5033万個です。

float32では1要素あたり4バイトですから、次のようになります。

50331648 \times 4 = 201326592

約201.3メガバイトです。

バッチサイズを32に減らすと、要素数は4分の1になります。

32 \times 512 \times 768 = 12582912

メモリ量も4分の1の、約50.3メガバイトになります。

この計算から、バッチサイズがメモリ消費に直接比例することが分かります。メモリ不足に陥ったとき、バッチサイズを半分にすればメモリも半分になる、という見積もりができます。

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

a_{21} は、2行目1列目の要素です。値は4です。Pythonでは A[1, 0] と書きます。

a_{13} は、1行目3列目の要素です。値は9です。Pythonでは A[0, 2] と書きます。

数式の添え字とPythonの添え字が、1つずれる点に注意してください。

演習4では、軸ごとに合計を取ると、その軸が消えることが確認できます。形が (2, 3, 4) のテンソルについて、軸0で合計を取ると (3, 4) になります。

この性質は、後の章で頻繁に登場します。どの軸に対して操作を行うかによって、結果の形が変わる、という点を意識してください。

講師向けの補足

この章では、shape を読み取る練習に、最も時間を割いてください。

実務でのつまずきの大半は、形の理解不足に起因します。理論的な理解より、(32, 128, 768) を見てそれぞれの軸が何かを即座に答えられる、という実務能力のほうが、直接的に役立ちます。

効果的な方法は、受講者に実際のモデルのコードを渡し、各層の入出力の shape を表示させることです。想像していた形と違っていた、という体験が、注意深さを育てます。

また、メモリ量の計算は、必ず演習に含めてください。バッチサイズを大きくしたらメモリ不足になった、という状況は、ほぼ全員が経験します。そのときに自分で見積もれるかどうかで、対処の速さが変わります。

理解度を確認する問い

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

2行3列の行列と、3行2列の行列は、どう違うでしょうか。

スカラー、ベクトル、行列、テンソルの関係を、軸の数という観点から説明してみてください。

形が (16, 512, 768) のテンソルについて、要素の総数と、float32でのメモリ量を計算してください。

同じ画像データでも、PyTorchとTensorFlowで軸の順序が異なります。この違いは、どのような場面で問題になるでしょうか。

まとめ

行列とは、数値を縦横の格子状に並べたものです。横の並びを行、縦の並びを列と呼び、大きさは何行何列という順序で表します。要素を指定する際も、a_{ij} のように、行が先、列が後です。

特別な形として、行数と列数が等しい正方行列、対角線上が1でそれ以外が0の単位行列、対角線上以外が0の対角行列があります。単位行列は、掛けても相手が変化しないという性質を持ち、数値の1と同じ役割を果たします。

テンソルとは、数値の並びをより一般的に扱う言葉です。軸が0本ならスカラー、1本ならベクトル、2本なら行列、3本以上なら一般にテンソルと呼びます。ただし実務では、すべてまとめてテンソルと呼ぶことが一般的です。

ディープラーニングで頻出する形として、表形式データは (batch_size, num_features) 、画像データはPyTorchでは (batch_size, channels, height, width) 、言語データは (batch_size, sequence_length, embedding_dim) という構造を持ちます。ライブラリによって軸の順序が異なる場合があるため、確認が必要です。

形が分かれば、すべての軸の大きさを掛け合わせることで要素の総数が求められ、データ型に応じたバイト数を掛けることでメモリ量が計算できます。この計算は、メモリ不足への対処や、モデルの規模を見積もる際に、実務で頻繁に使われます。

テンソルを扱ううえで最も重要なのは、各軸が何を表しているかを常に意識することです。軸の順序を取り違えると、エラーにならないまま誤った結果が出ることがあります。

次の第4章では、画像も文章も数値の並びになるという話を扱います。まったく異なる種類のデータが、どのようにして同じ形式に変換されるのか。その具体的な手順を確認します。ここまでで学んだ行列とテンソルという入れ物に、実際のデータがどう収まるのかが見えてきます。

第4章に進む前に、演習4のコードを実行し、軸ごとの合計を取ると形がどう変わるかを、自分の目で確認しておいてください。どの軸に操作を行うかで結果が変わるという感覚は、以降の章で繰り返し必要になります。

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

投稿者プロフィール

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

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