2.3. 線形代数

ここまでで、データセットをテンソルに読み込み、 基本的な数学演算でこれらのテンソルを操作できるようになった。 より洗練されたモデルを構築し始めるには、 線形代数のいくつかの道具も必要になる。 この節では、スカラー演算から始めて行列積へと進みながら、 最も重要な概念をやさしく導入する。

import torch
from mxnet import np, npx
npx.set_np()
from jax import numpy as jnp
import tensorflow as tf

2.3.1. スカラー

数学における基本的な演算の多くは、 数値を一つずつ操作することから成り立っている。 形式的には、これらの値を スカラー と呼ぶ。 たとえば、パロアルトの気温が 華氏 \(72\) 度であるとする。 気温を摂氏に変換したければ、 \(f\)\(72\) に設定して式 \(c = \frac{5}{9}(f - 32)\) を評価する。 この式では、\(5\)\(9\)\(32\) は定数スカラーである。 変数 \(c\)\(f\) は一般に未知のスカラーを表す。

スカラーは通常の小文字 (たとえば \(x\)\(y\)\(z\)) で表し、すべての(連続な) 実数値 スカラーの空間を \(\mathbb{R}\) で表す。 手短にするため、空間 の厳密な定義は省略する。 ここでは、式 \(x \in \mathbb{R}\)\(x\) が実数値のスカラーであることを示す形式的な記法だと考えよ。 記号 \(\in\)(「〜に属する」と読みます)は、 集合への所属を表す。 たとえば、\(x, y \in \{0, 1\}\) は、 \(x\)\(y\)\(0\)\(1\) しか取れない変数であることを示す。

スカラーは、1つの要素だけを含むテンソルとして実装される。 以下では、2つのスカラーを代入し、 おなじみの加算、乗算、除算、べき乗を行う。

x = torch.tensor(3.0)
y = torch.tensor(2.0)

x + y, x * y, x / y, x**y
(tensor(5.), tensor(6.), tensor(1.5000), tensor(9.))
x = np.array(3.0)
y = np.array(2.0)

x + y, x * y, x / y, x ** y
[07:04:15] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
(array(5.), array(6.), array(1.5), array(9.))
x = jnp.array(3.0)
y = jnp.array(2.0)

x + y, x * y, x / y, x**y
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
(Array(5., dtype=float32, weak_type=True),
 Array(6., dtype=float32, weak_type=True),
 Array(1.5, dtype=float32, weak_type=True),
 Array(9., dtype=float32, weak_type=True))
x = tf.constant(3.0)
y = tf.constant(2.0)

x + y, x * y, x / y, x**y
(<tf.Tensor: shape=(), dtype=float32, numpy=5.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=6.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=1.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=9.0>)

2.3.2. ベクトル

今のところ、ベクトルはスカラーの固定長配列だと考えて差し支えない。 通常の配列と同様に、 これらのスカラーをベクトルの 要素 と呼ぶ (エントリ成分 とも呼ばれます)。 ベクトルが実世界のデータセットの例を表すとき、 その値には何らかの現実世界での意味がある。 たとえば、ローンのデフォルトリスクを予測するモデルを学習しているなら、 各申請者をベクトルに対応付け、 その成分は収入、勤務年数、 過去のデフォルト回数などの量に対応するかもしれない。 心臓発作のリスクを研究しているなら、 各ベクトルは患者を表し、 その成分は直近のバイタルサイン、コレステロール値、 1日あたりの運動時間などに対応するかもしれない。 ベクトルは太字の小文字 (たとえば \(\mathbf{x}\)\(\mathbf{y}\)\(\mathbf{z}\)) で表す。

ベクトルは \(1^{\textrm{st}}\)-order テンソルとして実装される。 一般に、このようなテンソルはメモリ制約の範囲で任意の長さを持てる。注意: Python では、ほとんどのプログラミング言語と同様に、ベクトルの添字は \(0\) から始まりる。これは ゼロ始まりのインデックス付け とも呼ばれる。一方、線形代数では添字は \(1\) から始まります(1始まりのインデックス付け)。

x = torch.arange(3)
x
tensor([0, 1, 2])
x = np.arange(3)
x
array([0., 1., 2.])
x = jnp.arange(3)
x
Array([0, 1, 2], dtype=int32)
x = tf.range(3)
x
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([0, 1, 2], dtype=int32)>

添字を使ってベクトルの要素を参照できる。 たとえば、\(x_2\)\(\mathbf{x}\) の2番目の要素を表す。 \(x_2\) はスカラーなので、太字にはしない。 デフォルトでは、ベクトルは要素を縦に並べて可視化する。

(2.3.1)\[\begin{split}\mathbf{x} =\begin{bmatrix}x_{1} \\ \vdots \\x_{n}\end{bmatrix}.\end{split}\]

ここで \(x_1, \ldots, x_n\) はベクトルの要素である。 後で、こうした 列ベクトル と、要素を横に並べた 行ベクトル を区別する。 テンソルの要素にはインデックスでアクセスする ことを思い出しよ。

x[2]
tensor(2)

ベクトルが \(n\) 個の要素を含むことを示すには、 \(\mathbf{x} \in \mathbb{R}^n\) と書きる。 形式的には、\(n\) をベクトルの 次元数 と呼ぶ。 コードでは、これはテンソルの長さに対応する。 Python の組み込み関数 len で取得できる。

len(x)
3

長さは shape 属性でも取得できる。 shape は、各軸に沿ったテンソルの長さを示すタプルである。 1つの軸しか持たないテンソルの shape は、1つの要素だけを持つ。

x.shape
torch.Size([3])

しばしば「次元」という語は、 軸の数と特定の軸に沿った長さの両方を指すために 曖昧に使われる。 この混乱を避けるため、 軸の数を指すときには 階数 を用い、 要素数を指すときには 次元数 を専ら用いる。

2.3.3. 行列

スカラーが \(0^{\textrm{th}}\)-order テンソルであり、 ベクトルが \(1^{\textrm{st}}\)-order テンソルであるのと同様に、 行列は \(2^{\textrm{nd}}\)-order テンソルである。 行列は太字の大文字 (たとえば \(\mathbf{X}\)\(\mathbf{Y}\)\(\mathbf{Z}\)) で表し、コードでは2つの軸を持つテンソルとして表現する。 式 \(\mathbf{A} \in \mathbb{R}^{m \times n}\) は、 行列 \(\mathbf{A}\)\(m \times n\) 個の実数値スカラーを含み、 \(m\)\(n\) 列に並んでいることを示す。 \(m = n\) のとき、その行列は 正方 行列と呼ぶ。 視覚的には、任意の行列を表として示せる。 個々の要素を参照するには、 行と列の両方の添字を付ける。たとえば、 \(a_{ij}\)\(\mathbf{A}\)\(i^{\textrm{th}}\)\(j^{\textrm{th}}\) 列に属する値である。

(2.3.2)\[\begin{split}\mathbf{A}=\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \cdots & a_{mn} \\ \end{bmatrix}.\end{split}\]

コードでは、行列 \(\mathbf{A} \in \mathbb{R}^{m \times n}\) を shape が (\(m\), \(n\)) の \(2^{\textrm{nd}}\)-order テンソルとして表す。 任意の適切なサイズの \(m \times n\) テンソルを \(m \times n\) 行列に変換できる reshape に希望の shape を渡すことで実現できる。

A = torch.arange(6).reshape(3, 2)
A
tensor([[0, 1],
        [2, 3],
        [4, 5]])
A = np.arange(6).reshape(3, 2)
A
array([[0., 1.],
       [2., 3.],
       [4., 5.]])
A = jnp.arange(6).reshape(3, 2)
A
Array([[0, 1],
       [2, 3],
       [4, 5]], dtype=int32)
A = tf.reshape(tf.range(6), (3, 2))
A
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[0, 1],
       [2, 3],
       [4, 5]], dtype=int32)>

ときには軸を入れ替えたいことがある。 行列の行と列を交換した結果を、その 転置 と呼ぶ。 形式的には、行列 \(\mathbf{A}\) の転置を \(\mathbf{A}^\top\) で表し、 \(\mathbf{B} = \mathbf{A}^\top\) ならば、すべての \(i\)\(j\) について \(b_{ij} = a_{ji}\) である。 したがって、\(m \times n\) 行列の転置は \(n \times m\) 行列になる。

(2.3.3)\[\begin{split}\mathbf{A}^\top = \begin{bmatrix} a_{11} & a_{21} & \dots & a_{m1} \\ a_{12} & a_{22} & \dots & a_{m2} \\ \vdots & \vdots & \ddots & \vdots \\ a_{1n} & a_{2n} & \dots & a_{mn} \end{bmatrix}.\end{split}\]

コードでは、任意の行列の転置を次のように取得できる。

A.T
tensor([[0, 2, 4],
        [1, 3, 5]])
A.T
array([[0., 2., 4.],
       [1., 3., 5.]])
A.T
Array([[0, 2, 4],
       [1, 3, 5]], dtype=int32)
tf.transpose(A)
<tf.Tensor: shape=(2, 3), dtype=int32, numpy=
array([[0, 2, 4],
       [1, 3, 5]], dtype=int32)>

対称行列は、正方行列のうち自分自身の転置に等しいものです: \(\mathbf{A} = \mathbf{A}^\top\). 次の行列は対称である。

A = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
A == A.T
tensor([[True, True, True],
        [True, True, True],
        [True, True, True]])
A = np.array([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
A == A.T
array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]])
A = jnp.array([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
A == A.T
Array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]], dtype=bool)
A = tf.constant([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
A == tf.transpose(A)
<tf.Tensor: shape=(3, 3), dtype=bool, numpy=
array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]])>

行列はデータセットを表現するのに便利である。 通常、行は個々の記録に対応し、 列は異なる属性に対応する。

2.3.4. テンソル

スカラー、ベクトル、行列だけでも 機械学習の道のりをかなり進めるが、 やがてはより高階の テンソル を扱う必要が出てきる。 テンソルは\(n^{\textrm{th}}\)-order 配列への拡張を 一般的に記述する方法を与えてくれる。 テンソルクラス のソフトウェアオブジェクトを「テンソル」と呼ぶのは、 それらも任意の数の軸を持てるからである。 数学的対象としての テンソル と、 コード上での実装を同じ語で呼ぶのは紛らわしいかもしれないが、 通常は文脈から意味が明らかである。 一般のテンソルは特別な書体の大文字 (たとえば \(\mathsf{X}\)\(\mathsf{Y}\)\(\mathsf{Z}\)) で表し、そのインデックス付けの仕組み (たとえば \(x_{ijk}\)\([\mathsf{X}]_{1, 2i-1, 3}\)) は行列のそれから自然に拡張される。

画像を扱い始めると、テンソルはさらに重要になる。 各画像は、高さ、幅、チャネル に対応する軸を持つ \(3^{\textrm{rd}}\)-order テンソルとして与えられる。 各空間位置では、各色(赤、緑、青)の強度が チャネル方向に並べられる。 さらに、画像の集合はコード上では \(4^{\textrm{th}}\)-order テンソルとして表され、 個々の画像は第1軸に沿ってインデックス付けされる。 高階テンソルは、ベクトルや行列と同様に、 shape の成分数を増やすことで構成される。

torch.arange(24).reshape(2, 3, 4)
tensor([[[ 0,  1,  2,  3],
         [ 4,  5,  6,  7],
         [ 8,  9, 10, 11]],

        [[12, 13, 14, 15],
         [16, 17, 18, 19],
         [20, 21, 22, 23]]])
np.arange(24).reshape(2, 3, 4)
array([[[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.]],

       [[12., 13., 14., 15.],
        [16., 17., 18., 19.],
        [20., 21., 22., 23.]]])
jnp.arange(24).reshape(2, 3, 4)
Array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]],

       [[12, 13, 14, 15],
        [16, 17, 18, 19],
        [20, 21, 22, 23]]], dtype=int32)
tf.reshape(tf.range(24), (2, 3, 4))
<tf.Tensor: shape=(2, 3, 4), dtype=int32, numpy=
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]],

       [[12, 13, 14, 15],
        [16, 17, 18, 19],
        [20, 21, 22, 23]]], dtype=int32)>

2.3.5. テンソル演算の基本的性質

スカラー、ベクトル、行列、 および高階テンソルには、 便利な性質がいくつかある。 たとえば、要素ごとの演算は、 入力と同じ shape を持つ出力を生成する。

A = torch.arange(6, dtype=torch.float32).reshape(2, 3)
B = A.clone()  # Assign a copy of A to B by allocating new memory
A, A + B
(tensor([[0., 1., 2.],
         [3., 4., 5.]]),
 tensor([[ 0.,  2.,  4.],
         [ 6.,  8., 10.]]))
A = np.arange(6).reshape(2, 3)
B = A.copy()  # Assign a copy of A to B by allocating new memory
A, A + B
(array([[0., 1., 2.],
        [3., 4., 5.]]),
 array([[ 0.,  2.,  4.],
        [ 6.,  8., 10.]]))
A = jnp.arange(6, dtype=jnp.float32).reshape(2, 3)
B = A
A, A + B
(Array([[0., 1., 2.],
        [3., 4., 5.]], dtype=float32),
 Array([[ 0.,  2.,  4.],
        [ 6.,  8., 10.]], dtype=float32))
A = tf.reshape(tf.range(6, dtype=tf.float32), (2, 3))
B = A  # No cloning of A to B by allocating new memory
A, A + B
(<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
 array([[0., 1., 2.],
        [3., 4., 5.]], dtype=float32)>,
 <tf.Tensor: shape=(2, 3), dtype=float32, numpy=
 array([[ 0.,  2.,  4.],
        [ 6.,  8., 10.]], dtype=float32)>)

2つの行列の要素ごとの積は Hadamard 積 と呼ばれます(\(\odot\) で表す)。 2つの行列 \(\mathbf{A}, \mathbf{B} \in \mathbb{R}^{m \times n}\) の Hadamard 積の各要素は次のように書ける。

(2.3.4)\[\begin{split}\mathbf{A} \odot \mathbf{B} = \begin{bmatrix} a_{11} b_{11} & a_{12} b_{12} & \dots & a_{1n} b_{1n} \\ a_{21} b_{21} & a_{22} b_{22} & \dots & a_{2n} b_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} b_{m1} & a_{m2} b_{m2} & \dots & a_{mn} b_{mn} \end{bmatrix}.\end{split}\]
A * B
tensor([[ 0.,  1.,  4.],
        [ 9., 16., 25.]])

スカラーとテンソルの加算や乗算 は、元のテンソルと同じ shape の結果を生成する。 ここでは、テンソルの各要素にスカラーを加算(または乗算)している。

a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
(tensor([[[ 2,  3,  4,  5],
          [ 6,  7,  8,  9],
          [10, 11, 12, 13]],

         [[14, 15, 16, 17],
          [18, 19, 20, 21],
          [22, 23, 24, 25]]]),
 torch.Size([2, 3, 4]))
a = 2
X = np.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
(array([[[ 2.,  3.,  4.,  5.],
         [ 6.,  7.,  8.,  9.],
         [10., 11., 12., 13.]],

        [[14., 15., 16., 17.],
         [18., 19., 20., 21.],
         [22., 23., 24., 25.]]]),
 (2, 3, 4))
a = 2
X = jnp.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
(Array([[[ 2,  3,  4,  5],
         [ 6,  7,  8,  9],
         [10, 11, 12, 13]],

        [[14, 15, 16, 17],
         [18, 19, 20, 21],
         [22, 23, 24, 25]]], dtype=int32),
 (2, 3, 4))
a = 2
X = tf.reshape(tf.range(24), (2, 3, 4))
a + X, (a * X).shape
(<tf.Tensor: shape=(2, 3, 4), dtype=int32, numpy=
 array([[[ 2,  3,  4,  5],
         [ 6,  7,  8,  9],
         [10, 11, 12, 13]],

        [[14, 15, 16, 17],
         [18, 19, 20, 21],
         [22, 23, 24, 25]]], dtype=int32)>,
 TensorShape([2, 3, 4]))

2.3.6. リダクション

しばしば、テンソルの要素の 総和を計算したい ことがある。 長さ \(n\) のベクトル \(\mathbf{x}\) の要素の和を表すには、 \(\sum_{i=1}^n x_i\) と書きる。これには簡単な関数がある。

x = torch.arange(3, dtype=torch.float32)
x, x.sum()
(tensor([0., 1., 2.]), tensor(3.))
x = np.arange(3)
x, x.sum()
(array([0., 1., 2.]), array(3.))
x = jnp.arange(3, dtype=jnp.float32)
x, x.sum()
(Array([0., 1., 2.], dtype=float32), Array(3., dtype=float32))
x = tf.range(3, dtype=tf.float32)
x, tf.reduce_sum(x)
(<tf.Tensor: shape=(3,), dtype=float32, numpy=array([0., 1., 2.], dtype=float32)>,
 <tf.Tensor: shape=(), dtype=float32, numpy=3.0>)

任意の shape のテンソルの要素の 和を表すには、 すべての軸にわたって単純に和を取ればよいである。 たとえば、\(m \times n\) 行列 \(\mathbf{A}\) の要素の和は \(\sum_{i=1}^{m} \sum_{j=1}^{n} a_{ij}\) と書ける。

A.shape, A.sum()
(torch.Size([2, 3]), tensor(15.))
A.shape, A.sum()
((2, 3), array(15.))
A.shape, A.sum()
((2, 3), Array(15., dtype=float32))
A.shape, tf.reduce_sum(A)
(TensorShape([2, 3]), <tf.Tensor: shape=(), dtype=float32, numpy=15.0>)

デフォルトでは、sum 関数を呼ぶと テンソルはすべての軸に沿って リダクション され、 最終的にスカラーが得られる。 ライブラリでは、テンソルを どの軸に沿ってリダクションするかを 指定することもできる。 行(軸0)に沿ってすべての要素を足し合わせるには、 sumaxis=0 を指定する。 入力行列は軸0に沿ってリダクションされて出力ベクトルを生成するため、 この軸は出力の shape から消える。

A.shape, A.sum(axis=0).shape
(torch.Size([2, 3]), torch.Size([3]))
A.shape, A.sum(axis=0).shape
((2, 3), (3,))
A.shape, A.sum(axis=0).shape
((2, 3), (3,))
A.shape, tf.reduce_sum(A, axis=0).shape
(TensorShape([2, 3]), TensorShape([3]))

axis=1 を指定すると、列方向(軸1)が、すべての列の要素を足し合わせることでリダクションされる。

A.shape, A.sum(axis=1).shape
(torch.Size([2, 3]), torch.Size([2]))
A.shape, A.sum(axis=1).shape
((2, 3), (2,))
A.shape, A.sum(axis=1).shape
((2, 3), (2,))
A.shape, tf.reduce_sum(A, axis=1).shape
(TensorShape([2, 3]), TensorShape([2]))

行と列の両方に沿って和を取って行列をリダクションすることは、 行列のすべての要素を足し合わせることと同じである。

A.sum(axis=[0, 1]) == A.sum()  # Same as A.sum()
tensor(True)
A.sum(axis=[0, 1]) == A.sum()  # Same as A.sum()
array(True)
A.sum(axis=[0, 1]) == A.sum()  # Same as A.sum()
Array(True, dtype=bool)
tf.reduce_sum(A, axis=[0, 1]), tf.reduce_sum(A)  # Same as tf.reduce_sum(A)
(<tf.Tensor: shape=(), dtype=float32, numpy=15.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=15.0>)

関連する量として 平均、別名 アベレージ がある。 平均は、和を要素数の総数で割ることで求める。 平均の計算は非常に頻繁に行われるため、 sum と同様に使える専用のライブラリ関数がある。

A.mean(), A.sum() / A.numel()
(tensor(2.5000), tensor(2.5000))
A.mean(), A.sum() / A.size
(array(2.5), array(2.5))
A.mean(), A.sum() / A.size
(Array(2.5, dtype=float32), Array(2.5, dtype=float32))
tf.reduce_mean(A), tf.reduce_sum(A) / tf.size(A).numpy()
(<tf.Tensor: shape=(), dtype=float32, numpy=2.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=2.5>)

同様に、平均を計算する関数も 特定の軸に沿ってテンソルをリダクションできる。

A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(tensor([1.5000, 2.5000, 3.5000]), tensor([1.5000, 2.5000, 3.5000]))
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(array([1.5, 2.5, 3.5]), array([1.5, 2.5, 3.5]))
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(Array([1.5, 2.5, 3.5], dtype=float32), Array([1.5, 2.5, 3.5], dtype=float32))
tf.reduce_mean(A, axis=0), tf.reduce_sum(A, axis=0) / A.shape[0]
(<tf.Tensor: shape=(3,), dtype=float32, numpy=array([1.5, 2.5, 3.5], dtype=float32)>,
 <tf.Tensor: shape=(3,), dtype=float32, numpy=array([1.5, 2.5, 3.5], dtype=float32)>)

2.3.7. 非リダクション和

和や平均を計算する関数を呼ぶときに、 軸の数を変えずに保つ と便利な場合がある。 これは、ブロードキャスト機構を使いたいときに重要である。

sum_A = A.sum(axis=1, keepdims=True)
sum_A, sum_A.shape
(tensor([[ 3.],
         [12.]]),
 torch.Size([2, 1]))
sum_A = A.sum(axis=1, keepdims=True)
sum_A, sum_A.shape
(array([[ 3.],
        [12.]]),
 (2, 1))
sum_A = A.sum(axis=1, keepdims=True)
sum_A, sum_A.shape
(Array([[ 3.],
        [12.]], dtype=float32),
 (2, 1))
sum_A = tf.reduce_sum(A, axis=1, keepdims=True)
sum_A, sum_A.shape
(<tf.Tensor: shape=(2, 1), dtype=float32, numpy=
 array([[ 3.],
        [12.]], dtype=float32)>,
 TensorShape([2, 1]))

たとえば、sum_A は各行を足し合わせた後も2つの軸を保つので、 ブロードキャストを使って Asum_A で割る ことができ、 各行の和が \(1\) になる行列を作れる。

A / sum_A
tensor([[0.0000, 0.3333, 0.6667],
        [0.2500, 0.3333, 0.4167]])

A の要素の累積和をある軸に沿って、 たとえば axis=0(行ごと)で計算したければ、 cumsum 関数を呼べる。 設計上、この関数は入力テンソルをどの軸に沿ってもリダクションしない。

A.cumsum(axis=0)
tensor([[0., 1., 2.],
        [3., 5., 7.]])
A.cumsum(axis=0)
array([[0., 1., 2.],
       [3., 5., 7.]])
A.cumsum(axis=0)
Array([[0., 1., 2.],
       [3., 5., 7.]], dtype=float32)
tf.cumsum(A, axis=0)
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[0., 1., 2.],
       [3., 5., 7.]], dtype=float32)>

2.3.8. ドット積

ここまでで、要素ごとの演算、和、平均だけを扱ってきた。 もしこれしかできないなら、線形代数が独立した節を持つ価値はない。 幸い、ここからが面白くなる。 最も基本的な演算の1つがドット積である。 2つのベクトル \(\mathbf{x}, \mathbf{y} \in \mathbb{R}^d\) に対して、 その ドット積 \(\mathbf{x}^\top \mathbf{y}\)内積\(\langle \mathbf{x}, \mathbf{y} \rangle\) とも呼ばれます)は、 同じ位置にある要素の積の和です: \(\mathbf{x}^\top \mathbf{y} = \sum_{i=1}^{d} x_i y_i\)

y = torch.ones(3, dtype = torch.float32)
x, y, torch.dot(x, y)
(tensor([0., 1., 2.]), tensor([1., 1., 1.]), tensor(3.))
y = np.ones(3)
x, y, np.dot(x, y)
(array([0., 1., 2.]), array([1., 1., 1.]), array(3.))
y = jnp.ones(3, dtype = jnp.float32)
x, y, jnp.dot(x, y)
(Array([0., 1., 2.], dtype=float32),
 Array([1., 1., 1.], dtype=float32),
 Array(3., dtype=float32))
y = tf.ones(3, dtype=tf.float32)
x, y, tf.tensordot(x, y, axes=1)
(<tf.Tensor: shape=(3,), dtype=float32, numpy=array([0., 1., 2.], dtype=float32)>,
 <tf.Tensor: shape=(3,), dtype=float32, numpy=array([1., 1., 1.], dtype=float32)>,
 <tf.Tensor: shape=(), dtype=float32, numpy=3.0>)

同値な見方として、2つのベクトルのドット積は、 要素ごとの乗算の後に和を取ることで計算できる:

torch.sum(x * y)
tensor(3.)
np.sum(x * y)
array(3.)
jnp.sum(x * y)
Array(3., dtype=float32)
tf.reduce_sum(x * y)
<tf.Tensor: shape=(), dtype=float32, numpy=3.0>

ドット積は幅広い文脈で有用である。 たとえば、ある値の集合をベクトル \(\mathbf{x} \in \mathbb{R}^n\) で表し、 重みの集合を \(\mathbf{w} \in \mathbb{R}^n\) で表すと、 重み \(\mathbf{w}\) に従った \(\mathbf{x}\) の値の重み付き和は ドット積 \(\mathbf{x}^\top \mathbf{w}\) として表せる。 重みが非負で、かつ \(1\) に和が等しい、すなわち \(\left(\sum_{i=1}^{n} {w_i} = 1\right)\) とき、 ドット積は 重み付き平均 を表す。 2つのベクトルを単位長に正規化すると、 ドット積はそれらのなす角の余弦を表す。 この節の後半で、この 長さ の概念を正式に導入する。

2.3.9. 行列–ベクトル積

ドット積の計算方法がわかったので、 \(m \times n\) 行列 \(\mathbf{A}\)\(n\) 次元ベクトル \(\mathbf{x}\) の間の を理解し始められる。 まず、行ベクトルの観点から行列を可視化する。

(2.3.5)\[\begin{split}\mathbf{A}= \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_m \\ \end{bmatrix},\end{split}\]

ここで各 \(\mathbf{a}^\top_{i} \in \mathbb{R}^n\) は、 行列 \(\mathbf{A}\)\(i^\textrm{th}\) 行を表す行ベクトルである。

行列–ベクトル積 \(\mathbf{A}\mathbf{x}\) は、 長さ \(m\) の列ベクトルにすぎず、 その \(i^\textrm{th}\) 要素はドット積 \(\mathbf{a}^\top_i \mathbf{x}\) です:

(2.3.6)\[\begin{split}\mathbf{A}\mathbf{x} = \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_m \\ \end{bmatrix}\mathbf{x} = \begin{bmatrix} \mathbf{a}^\top_{1} \mathbf{x} \\ \mathbf{a}^\top_{2} \mathbf{x} \\ \vdots\\ \mathbf{a}^\top_{m} \mathbf{x}\\ \end{bmatrix}.\end{split}\]

行列 \(\mathbf{A}\in \mathbb{R}^{m \times n}\) による乗算は、 ベクトルを \(\mathbb{R}^{n}\) から \(\mathbb{R}^{m}\) へ写す変換だと考えられる。 このような変換は非常に有用である。 たとえば、回転は特定の正方行列との乗算として表現できる。 行列–ベクトル積は、前の層の出力から ニューラルネットワークの各層の出力を計算する際の 主要な計算も表している。

コードで行列–ベクトル積を表すには、 mv 関数を使う。 A の列方向の次元(軸1に沿った長さ)が x の次元(長さ)と同じでなければならないことに注意しよ。 Python には便利な演算子 @ があり、 行列–ベクトル積と行列–行列積の両方を (引数に応じて)実行できる。 したがって A@x と書ける。

A.shape, x.shape, torch.mv(A, x), A@x
(torch.Size([2, 3]), torch.Size([3]), tensor([ 5., 14.]), tensor([ 5., 14.]))
A.shape, x.shape, np.dot(A, x)
((2, 3), (3,), array([ 5., 14.]))
A.shape, x.shape, jnp.matmul(A, x)
((2, 3), (3,), Array([ 5., 14.], dtype=float32))
A.shape, x.shape, tf.linalg.matvec(A, x)
(TensorShape([2, 3]),
 TensorShape([3]),
 <tf.Tensor: shape=(2,), dtype=float32, numpy=array([ 5., 14.], dtype=float32)>)

2.3.10. 行列–行列積

ドット積と行列–ベクトル積に慣れれば、 行列–行列積 は簡単に理解できる。

2つの行列 \(\mathbf{A} \in \mathbb{R}^{n \times k}\)\(\mathbf{B} \in \mathbb{R}^{k \times m}\) があるとする。

(2.3.7)\[\begin{split}\mathbf{A}=\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1k} \\ a_{21} & a_{22} & \cdots & a_{2k} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nk} \\ \end{bmatrix},\quad \mathbf{B}=\begin{bmatrix} b_{11} & b_{12} & \cdots & b_{1m} \\ b_{21} & b_{22} & \cdots & b_{2m} \\ \vdots & \vdots & \ddots & \vdots \\ b_{k1} & b_{k2} & \cdots & b_{km} \\ \end{bmatrix}.\end{split}\]

\(\mathbf{A}\)\(i^\textrm{th}\) 行を表す行ベクトルを \(\mathbf{a}^\top_{i} \in \mathbb{R}^k\) とし、 \(\mathbf{B}\)\(j^\textrm{th}\) 列を表す列ベクトルを \(\mathbf{b}_{j} \in \mathbb{R}^k\) とする。

(2.3.8)\[\begin{split}\mathbf{A}= \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_n \\ \end{bmatrix}, \quad \mathbf{B}=\begin{bmatrix} \mathbf{b}_{1} & \mathbf{b}_{2} & \cdots & \mathbf{b}_{m} \\ \end{bmatrix}.\end{split}\]

行列積 \(\mathbf{C} \in \mathbb{R}^{n \times m}\) を作るには、 各要素 \(c_{ij}\)\(\mathbf{A}\)\(i^\textrm{th}\) 行と \(\mathbf{B}\)\(j^\textrm{th}\) 列のドット積、 すなわち \(\mathbf{a}^\top_i \mathbf{b}_j\) として計算するだけである。

(2.3.9)\[\begin{split}\mathbf{C} = \mathbf{AB} = \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_n \\ \end{bmatrix} \begin{bmatrix} \mathbf{b}_{1} & \mathbf{b}_{2} & \cdots & \mathbf{b}_{m} \\ \end{bmatrix} = \begin{bmatrix} \mathbf{a}^\top_{1} \mathbf{b}_1 & \mathbf{a}^\top_{1}\mathbf{b}_2& \cdots & \mathbf{a}^\top_{1} \mathbf{b}_m \\ \mathbf{a}^\top_{2}\mathbf{b}_1 & \mathbf{a}^\top_{2} \mathbf{b}_2 & \cdots & \mathbf{a}^\top_{2} \mathbf{b}_m \\ \vdots & \vdots & \ddots &\vdots\\ \mathbf{a}^\top_{n} \mathbf{b}_1 & \mathbf{a}^\top_{n}\mathbf{b}_2& \cdots& \mathbf{a}^\top_{n} \mathbf{b}_m \end{bmatrix}.\end{split}\]

行列–行列積 \(\mathbf{AB}\) は、 \(m\) 個の行列–ベクトル積 または \(m \times n\) 個のドット積を計算し、 その結果をつなぎ合わせて \(n \times m\) 行列を作るものだと考えられる。 次のコード片では、AB に対して行列積を行う。 ここで A は2行3列の行列で、 B は3行4列の行列である。 乗算後、2行4列の行列が得られる。

B = torch.ones(3, 4)
torch.mm(A, B), A@B
(tensor([[ 3.,  3.,  3.,  3.],
         [12., 12., 12., 12.]]),
 tensor([[ 3.,  3.,  3.,  3.],
         [12., 12., 12., 12.]]))
B = np.ones(shape=(3, 4))
np.dot(A, B)
array([[ 3.,  3.,  3.,  3.],
       [12., 12., 12., 12.]])
B = jnp.ones((3, 4))
jnp.matmul(A, B)
Array([[ 3.,  3.,  3.,  3.],
       [12., 12., 12., 12.]], dtype=float32)
B = tf.ones((3, 4), tf.float32)
tf.matmul(A, B)
<tf.Tensor: shape=(2, 4), dtype=float32, numpy=
array([[ 3.,  3.,  3.,  3.],
       [12., 12., 12., 12.]], dtype=float32)>

行列–行列積 という用語は、 しばしば単に 行列積 と短縮され、 Hadamard 積と混同してはならない。

2.3.11. ノルム

線形代数で最も有用な演算子のいくつかは ノルム である。 直感的には、ベクトルのノルムはそれがどれだけ 大きい かを教えてくれる。 たとえば、\(\ell_2\) ノルムはベクトルの(ユークリッド)長さを測りる。 ここで私たちは、ベクトルの成分の大きさに関する サイズ の概念 (次元数ではない)を使っている。

ノルムは、ベクトルをスカラーに写す関数 \(\| \cdot \|\) であり、 次の3つの性質を満たする。

  1. 任意のベクトル \(\mathbf{x}\) について、ベクトル(のすべての要素)を スカラー \(\alpha \in \mathbb{R}\) でスケールすると、そのノルムもそれに応じてスケールする:

    (2.3.10)\[\|\alpha \mathbf{x}\| = |\alpha| \|\mathbf{x}\|.\]
  2. 任意のベクトル \(\mathbf{x}\)\(\mathbf{y}\) について、 ノルムは三角不等式を満たす:

    (2.3.11)\[\|\mathbf{x} + \mathbf{y}\| \leq \|\mathbf{x}\| + \|\mathbf{y}\|.\]
  3. ベクトルのノルムは非負であり、ベクトルがゼロのときにのみ 0 になる:

    (2.3.12)\[\|\mathbf{x}\| > 0 \textrm{ for all } \mathbf{x} \neq 0.\]

多くの関数が有効なノルムであり、異なるノルムは 異なるサイズの概念を表す。 小学校の図形で直角三角形の斜辺を求めるときに学ぶ ユークリッドノルムは、 ベクトルの要素の二乗和の平方根である。 形式的には、これは \(\ell_2\) ノルム と呼ばれ、次のように表される。

(2.3.13)\[\|\mathbf{x}\|_2 = \sqrt{\sum_{i=1}^n x_i^2}.\]

norm メソッドは \(\ell_2\) ノルムを計算する。

u = torch.tensor([3.0, -4.0])
torch.norm(u)
tensor(5.)
u = np.array([3, -4])
np.linalg.norm(u)
array(5.)
u = jnp.array([3.0, -4.0])
jnp.linalg.norm(u)
Array(5., dtype=float32)
u = tf.constant([3.0, -4.0])
tf.norm(u)
<tf.Tensor: shape=(), dtype=float32, numpy=5.0>

\(\ell_1\) ノルム もよく使われ、 それに対応する尺度はマンハッタン距離と呼ばれる。 定義により、\(\ell_1\) ノルムは ベクトルの要素の絶対値の和である。

(2.3.14)\[\|\mathbf{x}\|_1 = \sum_{i=1}^n \left|x_i \right|.\]

\(\ell_2\) ノルムと比べると、外れ値の影響を受けにくいである。 \(\ell_1\) ノルムを計算するには、 絶対値と和の演算を組み合わせる。

torch.abs(u).sum()
tensor(7.)
np.abs(u).sum()
array(7.)
jnp.linalg.norm(u, ord=1) # same as jnp.abs(u).sum()
Array(7., dtype=float32)
tf.reduce_sum(tf.abs(u))
<tf.Tensor: shape=(), dtype=float32, numpy=7.0>

\(\ell_2\) ノルムと \(\ell_1\) ノルムはどちらも、 より一般的な \(\ell_p\) ノルム の特殊な場合である。

(2.3.15)\[\|\mathbf{x}\|_p = \left(\sum_{i=1}^n \left|x_i \right|^p \right)^{1/p}.\]

行列の場合は、事情がより複雑である。 そもそも行列は、個々の要素の集合としても、 ベクトルに作用して別のベクトルへ変換する対象としても見られる。 たとえば、行列–ベクトル積 \(\mathbf{X} \mathbf{v}\)\(\mathbf{v}\) に比べてどれだけ長くなりうるかを問うことができる。 この考え方は、スペクトル ノルムと呼ばれるものにつながりる。 ここではまず、計算がずっと簡単な フロベニウスノルム を導入する。 これは、行列の要素の二乗和の平方根として定義される。

(2.3.16)\[\|\mathbf{X}\|_\textrm{F} = \sqrt{\sum_{i=1}^m \sum_{j=1}^n x_{ij}^2}.\]

フロベニウスノルムは、行列の形をしたベクトルに対する \(\ell_2\) ノルムのように振る舞いる。 次の関数を呼ぶと、行列のフロベニウスノルムが計算される。

torch.norm(torch.ones((4, 9)))
tensor(6.)
np.linalg.norm(np.ones((4, 9)))
array(6.)
jnp.linalg.norm(jnp.ones((4, 9)))
Array(6., dtype=float32)
tf.norm(tf.ones((4, 9)))
<tf.Tensor: shape=(), dtype=float32, numpy=6.0>

あまり先走りすぎたくはないが、 これらの概念がなぜ有用なのかについての直感は すでに少し持っておける。 深層学習では、しばしば最適化問題を解こうとする。 観測データに割り当てられる確率を 最大化 すること、 推薦モデルに関連する収益を 最大化 すること、 予測と正解観測値の間の距離を 最小化 すること、 同じ人物の写真の表現同士の距離を 最小化 しつつ、 異なる人物の写真の表現同士の距離を 最大化 すること。 これらの距離は深層学習アルゴリズムの目的関数を構成し、 しばしばノルムとして表される。

2.3.12. 議論

この節では、現代の深層学習のかなりの部分を理解するのに必要な 線形代数をひととおり見てきた。 とはいえ、線形代数にはまだまだ多くの内容があり、 その多くは機械学習に有用である。 たとえば、行列は因子に分解でき、 その分解によって実世界のデータセットに潜む 低次元構造を明らかにできることがある。 機械学習には、行列分解とその高階テンソルへの一般化を用いて データセットの構造を発見し、 予測問題を解くことに焦点を当てた サブフィールドが丸ごと存在する。 しかし、この本の焦点は深層学習である。 そして、実際のデータセットに機械学習を適用して 手を動かした後のほうが、 より多くの数学を学ぶ意欲が高まると私たちは考えている。 そのため、後でさらに数学を導入する余地は残しつつも、 ここでこの節を締めくくりる。

もっと線形代数を学びたいなら、 優れた書籍やオンライン資料がたくさんある。 より発展的な速習コースとしては、 Strang (1993), Kolter (2008), Petersen and Pedersen (2008) を参照されたい。

要点をまとめると:

  • スカラー、ベクトル、行列、テンソルは 線形代数で使われる基本的な数学的対象であり、 それぞれ 0、1、2、および任意個の軸を持つ。

  • テンソルは、インデックス付けや summean などの演算によって、 指定した軸に沿ってスライスしたりリダクションしたりできる。

  • 要素ごとの積は Hadamard 積と呼ばれる。 これに対して、ドット積、行列–ベクトル積、行列–行列積は 要素ごとの演算ではなく、一般に入力とは異なる shape を持つ対象を返す。

  • Hadamard 積と比べると、行列–行列積は 計算にかなり時間がかかります(2次時間ではなく3次時間)。

  • ノルムはベクトル(または行列)の大きさに関するさまざまな概念を捉え、 2つのベクトルの差に適用して距離を測るのによく使われる。

  • よく使われるベクトルノルムには \(\ell_1\) ノルムと \(\ell_2\) ノルムがあり、 よく使われる行列ノルムには スペクトル ノルムと フロベニウス ノルムがある。

2.3.13. 演習

  1. 行列の転置の転置は元の行列そのものであることを証明せよ: \((\mathbf{A}^\top)^\top = \mathbf{A}\)

  2. 2つの行列 \(\mathbf{A}\)\(\mathbf{B}\) について、和と転置が可換であることを示せ: \(\mathbf{A}^\top + \mathbf{B}^\top = (\mathbf{A} + \mathbf{B})^\top\)

  3. 任意の正方行列 \(\mathbf{A}\) について、\(\mathbf{A} + \mathbf{A}^\top\) は常に対称か。前の2つの演習の結果だけを使って証明できるか。

  4. この節では shape が (2, 3, 4) のテンソル X を定義した。len(X) の出力は何だろうか。コードを実行せずに答えを書き、その後コードで確認しよ。

  5. 任意の shape のテンソル X について、len(X) は常に X のある軸の長さに対応するか。その軸はどれですか。

  6. A / A.sum(axis=1) を実行して何が起こるか見よ。結果を分析できるか。

  7. マンハッタンの中心部で2点間を移動するとき、座標、すなわち通りと街路の観点で、どれだけの距離を移動する必要があるか。斜めに移動できるか。

  8. shape が (2, 3, 4) のテンソルを考える。軸0、1、2 に沿った和の出力の shape はそれぞれ何ですか。

  9. 3つ以上の軸を持つテンソルを linalg.norm 関数に入力して、その出力を観察しよ。この関数は任意の shape のテンソルに対して何を計算するか。

  10. たとえば \(\mathbf{A} \in \mathbb{R}^{2^{10} \times 2^{16}}\), \(\mathbf{B} \in \mathbb{R}^{2^{16} \times 2^{5}}\), \(\mathbf{C} \in \mathbb{R}^{2^{5} \times 2^{14}}\) のような3つの大きな行列を、ガウス乱数で初期化したとする。積 \(\mathbf{A} \mathbf{B} \mathbf{C}\) を計算したいとき、\((\mathbf{A} \mathbf{B}) \mathbf{C}\)\(\mathbf{A} (\mathbf{B} \mathbf{C})\) のどちらで計算するかによって、メモリ使用量や速度に違いはあるか。なぜですか。

  11. たとえば \(\mathbf{A} \in \mathbb{R}^{2^{10} \times 2^{16}}\), \(\mathbf{B} \in \mathbb{R}^{2^{16} \times 2^{5}}\), \(\mathbf{C} \in \mathbb{R}^{2^{5} \times 2^{16}}\) のような3つの大きな行列を考える。\(\mathbf{A} \mathbf{B}\)\(\mathbf{A} \mathbf{C}^\top\) のどちらを計算するかによって速度に違いはあるか。なぜですか。もし \(\mathbf{C} = \mathbf{B}^\top\) をメモリを複製せずに初期化したら何が変わりますか。なぜですか。

  12. たとえば \(\mathbf{A}, \mathbf{B}, \mathbf{C} \in \mathbb{R}^{100 \times 200}\) の3つの行列を考える。\([\mathbf{A}, \mathbf{B}, \mathbf{C}]\) をスタックして3つの軸を持つテンソルを構成しよ。次元数はいくつですか。第3軸の第2成分を取り出して \(\mathbf{B}\) を復元しよ。答えが正しいことを確認しよ。