はい、承知いたしました。
初心者向けにPython/Numpyのdot
関数で行列・ベクトルの計算をマスターするための詳細な解説記事を作成します。以下、記事本文です。
【初心者向け】Python/Numpyのdotで行列・ベクトルの計算をマスター
はじめに
データサイエンス、機械学習、画像処理、シミュレーション…。現代のテクノロジーを支えるこれらの分野では、膨大な量のデータを効率的に処理する必要があります。その心臓部とも言える計算が「行列・ベクトルの計算」です。
Pythonでこうした科学技術計算を行う際に、デファクトスタンダードとなっているライブラリが Numpy です。そして、Numpyの中でも特に重要で、あらゆる計算の基礎となるのが、今回徹底解説する np.dot()
関数です。
「行列とかベクトルって、なんだか難しそう…」
「数学は苦手だから、記事を読んでも分からないかも…」
そう感じている方もご安心ください。この記事は、まさにそうした初心者の方々を対象としています。
この記事の目的:
* ベクトルと行列が何なのか、なぜ重要なのかを直感的に理解する。
* Numpyのdot
関数を使った基本的な計算(内積、行列とベクトルの積、行列同士の積)をマスターする。
* 計算の裏側で「何が起きているのか」というイメージを掴む。
* dot
と似た機能(@
演算子、matmul
)との違いを理解し、適切に使い分ける。
* 実践的な例題を通して、dot
がどのように活用されているかを体験する。
約5000語にわたるこの記事を読み終える頃には、あなたはnp.dot
を自在に操り、データ分析や機械学習のコードをより深く理解できるようになっているはずです。数学的な厳密さよりも、まずは「そういうことか!」と納得できる直感的な理解を大切にしながら、一歩ずつ進んでいきましょう。
第1章: 準備 – なぜ行列とベクトルなのか?
dot
関数の話に入る前に、まずはその計算対象である「ベクトル」と「行列」について、肩の力を抜いて学んでいきましょう。これらは単なる数字の集まりではなく、データを表現するための強力なツールなのです。
1-1. ベクトルとは何か? – 矢印で考えるデータ
ベクトルと聞くと、高校物理で習った「向きと大きさを持つ矢印」を思い出すかもしれません。それは正しいイメージです。
- 位置ベクトル: 「原点から見て、東に3km、北に4kmの地点」は
(3, 4)
というベクトルで表現できます。 - 速度ベクトル: 「時速100kmで北東に進む車」の速度もベクトルです。
コンピュータの世界では、この考え方を拡張して「複数の数字を順番に並べたもの」を広くベクトルと呼びます。
例えば、あるユーザーの情報を考えてみましょう。
* 年齢: 25歳
* 身長: 170cm
* 体重: 65kg
この3つの情報は、 (25, 170, 65)
というベクトルでひとまとめにできます。このように、関連する複数の数値を一つの塊として扱うのがベクトルの本質です。
Numpyでの表現方法
Numpyでは、ベクトルは1次元配列(1D-Array)として表現します。早速作ってみましょう。
“`python
Numpyライブラリをインポートする(npという別名で使うのが慣習)
import numpy as np
ベクトルを作成する
(3, 4) というベクトル
vec_a = np.array([3, 4])
print(f”ベクトルa: {vec_a}”)
print(f”ベクトルの型: {type(vec_a)}”)
print(f”ベクトルの次元数: {vec_a.ndim}”)
print(f”ベクトルの形状 (shape): {vec_a.shape}”) # (2,) は、要素が2つの1次元配列を意味する
print(“-” * 20)
ユーザー情報のベクトル
user_vec = np.array([25, 170, 65])
print(f”ユーザーベクトル: {user_vec}”)
print(f”形状 (shape): {user_vec.shape}”) # (3,) は、要素が3つの1次元配列を意味する
“`
実行結果:
“`
ベクトルa: [3 4]
ベクトルの型:
ベクトルの次元数: 1
ベクトルの形状 (shape): (2,)
ユーザーベクトル: [ 25 170 65]
形状 (shape): (3,)
``
np.array()を使ってリストを渡すだけで、簡単にNumpy配列(ここではベクトル)が作成できました。
shape`という属性が、その配列の形を表している点に注目してください。
1-2. 行列とは何か? – 数字のテーブル
行列は、ベクトルをさらに拡張したもので、「数字を縦横の格子状(テーブル形式)に並べたもの」です。
身近な例で考えてみましょう。
* 画像データ: モノクロ画像は、各ピクセルの明るさを0(黒)から255(白)の数値で表したものです。これはまさに行列そのものです。例えば、4×4ピクセルの小さな画像は4行4列の行列になります。
* 連立方程式:
2x + 3y = 8
4x + 1y = 6
この方程式の x
と y
の係数部分だけを取り出すと、
[[2, 3],
[4, 1]]
という行列になります。
このように、行列は複数のベクトルを束ねたもの(例えば、複数のユーザーデータを縦に並べたもの)や、システムや変換のルール(連立方程式の係数や画像のフィルターなど)を表現するのに非常に便利です。
Numpyでの表現方法
Numpyでは、行列は2次元配列(2D-Array)として表現します。
“`python
import numpy as np
行列を作成する
[[1, 2, 3], [4, 5, 6]] という行列
mat_A = np.array([[1, 2, 3],
[4, 5, 6]])
print(f”行列A:\n{mat_A}”)
print(f”行列の型: {type(mat_A)}”)
print(f”行列の次元数: {mat_A.ndim}”)
print(f”行列の形状 (shape): {mat_A.shape}”) # (2, 3) は、2行3列を意味する
print(“-” * 20)
連立方程式の係数行列
coeff_mat = np.array([[2, 3],
[4, 1]])
print(f”係数行列:\n{coeff_mat}”)
print(f”形状 (shape): {coeff_mat.shape}”) # (2, 2) は、2行2列を意味する
“`
実行結果:
“`
行列A:
[[1 2 3]
[4 5 6]]
行列の型:
行列の次元数: 2
行列の形状 (shape): (2, 3)
係数行列:
[[2 3]
[4 1]]
形状 (shape): (2, 2)
``
np.array()
リストのリスト(入れ子になったリスト)をに渡すことで、2次元配列(行列)が作成できます。
shapeが
(行数, 列数)` を表していることを確認してください。
1-3. なぜ計算が必要なのか?
ベクトルや行列がデータを表現する方法であることは分かりました。では、なぜそれらを「計算」する必要があるのでしょうか?
それは、データに何らかの操作を加え、新しい情報や意味を引き出すためです。
- 関係性の抽出: 2つのベクトルがどれくらい「似ている」か?(例:ユーザーAとユーザーBの購買傾向の類似度)
- データの変換: あるベクトルを、別のルール(行列)に基づいて新しいベクトルに変える。(例:画像を回転させる、3Dオブジェクトをスクリーンに投影する)
- システムのモデリング: 連立方程式を行列で表現し、解を求める。
- 予測: 過去のデータ(行列)から、未来の値を予測するモデル(これも行列で表現される)を作る。
こうした操作のほとんどが、これから学ぶ dot
関数(ドット積、行列積)によって実現されます。dot
は、ベクトルと行列の世界における「基本的な動詞」のようなものなのです。
第2章: 基本の「き」 – ベクトル同士の計算(内積)
まずは最もシンプルなケース、ベクトルとベクトルのdot
計算から始めましょう。これは数学では内積(inner product)またはドット積(dot product)と呼ばれます。
2-1. 内積(ドット積)とは何か?
ベクトルの内積は、2つのベクトルから1つの数値(スカラー)を計算する操作です。この計算結果は、2つのベクトルが「どれだけ同じ方向を向いているか」という度合いを表します。
計算ルール
計算は非常にシンプルです。対応する要素同士を掛け合わせ、それらをすべて足し合わせます。
ベクトル a = (a1, a2, ..., an)
と b = (b1, b2, ..., bn)
の内積 a・b
は、
a・b = a1*b1 + a2*b2 + ... + an*bn
となります。
例:
a = (3, 4)
と b = (5, 2)
の内積は、
3*5 + 4*2 = 15 + 8 = 23
となります。
内積が教えてくれること
* 結果が正で大きい: 2つのベクトルは似た方向を向いている。
* 結果が0: 2つのベクトルは直角に交わっている(直交している)。全く関係ない方向を向いている。
* 結果が負で大きい: 2つのベクトルはほぼ真逆の方向を向いている。
この性質は、例えば「推薦システム」で応用されます。ユーザーAの映画評価ベクトルとユーザーBの映画評価ベクトルの内積を取ることで、2人の好みがどれだけ似ているかを数値化できるのです。
2-2. Numpy dot
を使ったベクトルの内積
では、この計算をNumpyで実行してみましょう。np.dot(a, b)
という形で使います。
“`python
import numpy as np
2次元ベクトル
vec_a = np.array([3, 4])
vec_b = np.array([5, 2])
内積を計算
dot_ab = np.dot(vec_a, vec_b)
print(f”ベクトルa: {vec_a}”)
print(f”ベクトルb: {vec_b}”)
print(f”aとbの内積: {dot_ab}”)
print(f”計算過程: (35) + (42) = {35 + 42}”)
print(“-” * 20)
3次元ベクトル
vec_c = np.array([1, 2, 3])
vec_d = np.array([4, -5, 6]) # 負の数もOK
dot_cd = np.dot(vec_c, vec_d)
print(f”ベクトルc: {vec_c}”)
print(f”ベクトルd: {vec_d}”)
print(f”cとdの内積: {dot_cd}”)
print(f”計算過程: (14) + (2-5) + (36) = {14 + 2(-5) + 36}”)
“`
実行結果:
“`
ベクトルa: [3 4]
ベクトルb: [5 2]
aとbの内積: 23
計算過程: (35) + (42) = 23
ベクトルc: [1 2 3]
ベクトルd: [ 4 -5 6]
cとdの内積: 12
計算過程: (14) + (2-5) + (3*6) = 12
“`
np.dot()
を使うことで、手計算と同じ結果が簡単得られました。結果がベクトルや行列ではなく、ただの数値(スカラー)になっている点に注意してください。
2-3. @
演算子との関係
Python 3.5以降では、行列やベクトルの積をより直感的に書くための@
演算子(アットマーク演算子)が導入されました。
ベクトル同士の場合、a @ b
は np.dot(a, b)
と全く同じ意味になります。
“`python
import numpy as np
vec_a = np.array([3, 4])
vec_b = np.array([5, 2])
np.dot() を使う方法
dot_by_func = np.dot(vec_a, vec_b)
print(f”np.dot(a, b) の結果: {dot_by_func}”)
@演算子を使う方法
dot_by_op = vec_a @ vec_b
print(f”a @ b の結果: {dot_by_op}”)
結果が同じであることを確認
print(f”結果は同じ? -> {dot_by_func == dot_by_op}”)
“`
実行結果:
np.dot(a, b) の結果: 23
a @ b の結果: 23
結果は同じ? -> True
どちらを使うべき?
a * b
が要素ごとの積、a @ b
が内積(や後述の行列積)というように、記号で計算の種類が明確に区別できるため、コードの可読性が上がります。そのため、最近のコードでは @
演算子が好まれる傾向にあります。
この記事でも、今後はdot
関数と@
演算子を併用して解説していきます。まずは「ベクトル同士の計算では np.dot(a, b)
と a @ b
は同じ」と覚えておきましょう。
第3章: 行列とベクトルの積 – データを変換する魔法
次に、行列とベクトルの積を見ていきましょう。これは、行列が持つ「変換ルール」をベクトルに適用する操作と考えることができます。結果として、元のベクトルが新しいベクトルに変換されます。
3-1. 行列とベクトルの積の計算ルール
ここが最初の山場です。落ち着いてルールを理解しましょう。
行列 A
とベクトル v
の積 A @ v
を計算するには、行列 A
の各「行」とベクトル v
の「内積」を計算していきます。
例:
行列 A
を
[[1, 2, 3],
[4, 5, 6]]
ベクトル v
を (7, 8, 9)
とします。
積の結果は新しいベクトル w
になります。
w
の1番目の要素 =A
の1行目(1, 2, 3)
とv
(7, 8, 9)
の内積1*7 + 2*8 + 3*9 = 7 + 16 + 27 = 50
w
の2番目の要素 =A
の2行目(4, 5, 6)
とv
(7, 8, 9)
の内積4*7 + 5*8 + 6*9 = 28 + 40 + 54 = 122
したがって、計算結果のベクトル w
は (50, 122)
となります。
重要な計算条件
この計算が成り立つためには、行列の「列数」とベクトルの「要素数」が一致している必要があります。
- 行列
A
の shape:(m, n)
(m行, n列) - ベクトル
v
の shape:(n,)
(要素数n)
このとき、計算結果のベクトルの shape は (m,)
になります。
今回の例では、A
が(2, 3)
、v
が(3,)
だったので、列数と要素数が3
で一致しており、計算可能でした。結果のベクトルのshapeは(2,)
となり、実際にそうなっています。
もし行列の列数とベクトルの要素数が違うと、内積が計算できないためエラーになります。これはNumpyを使う上で非常に重要なルールです。
3-2. np.dot
を使った計算
この計算をNumpyで実行してみましょう。
“`python
import numpy as np
行列A (2行3列)
mat_A = np.array([[1, 2, 3],
[4, 5, 6]])
ベクトルv (要素数3)
vec_v = np.array([7, 8, 9])
print(f”行列Aのshape: {mat_A.shape}”)
print(f”ベクトルvのshape: {vec_v.shape}”)
print(“行列の列数(3)とベクトルの要素数(3)が一致しているので計算可能”)
行列とベクトルの積を計算
方法1: np.dot()
result_vec_dot = np.dot(mat_A, vec_v)
print(f”\nnp.dot(A, v) の結果: {result_vec_dot}”)
print(f”結果のshape: {result_vec_dot.shape}”)
方法2: @演算子
result_vec_at = mat_A @ vec_v
print(f”A @ v の結果: {result_vec_at}”)
print(f”結果のshape: {result_vec_at.shape}”)
“`
実行結果:
“`
行列Aのshape: (2, 3)
ベクトルvのshape: (3,)
行列の列数(3)とベクトルの要素数(3)が一致しているので計算可能
np.dot(A, v) の結果: [ 50 122]
結果のshape: (2,)
A @ v の結果: [ 50 122]
結果のshape: (2,)
``
[ 50 122]
手計算した通りのという結果が得られました。
shapeも
(2,)` となり、ルール通りです。
エラーになる例
もし、ベクトルの要素数が違っていたらどうなるでしょうか?
“`python
わざとエラーを起こしてみる
vec_error = np.array([1, 2]) # 要素数が2
print(f”行列Aのshape: {mat_A.shape}”) # (2, 3)
print(f”ベクトルerrorのshape: {vec_error.shape}”) # (2,)
mat_A @ vec_error # この行を実行するとエラーになる
エラーメッセージ (抜粋):
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0,
with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 3)
``
ValueError
上記コードのコメントアウトを外して実行すると、が発生します。メッセージは「サイズが違うよ(2は3と違う)」と教えてくれています。Numpyの計算でエラーが出たら、まずは各配列の
shape`を確認するのがデバッグの第一歩です。
3-3. 何が起きているのか? – 行列積の幾何学的な意味
行列とベクトルの積は、単なる計算ではありません。これには「空間の線形変換」という美しい幾何学的な意味が隠されています。
難しく聞こえるかもしれませんが、要は「行列は、ベクトルを別の場所に移動させる(=変換する)ための指示書」と考えることができます。
例えば、2次元空間上の点(ベクトル)を、原点を中心に回転させることを考えてみましょう。
ある点 (x, y)
を反時計回りに θ
度回転させるには、回転行列と呼ばれる特別な行列を掛ければよいことが知られています。
θ
度回転させるための回転行列 R
は、
[[cos(θ), -sin(θ)],
[sin(θ), cos(θ)]]
です。
では、点 (1, 0)
(x軸上の点)を90度回転させてみましょう。θ=90°
なので cos(90°)=0
, sin(90°)=1
です。
回転行列 R_90
は [[0, -1], [1, 0]]
となります。
これを使って計算してみます。
“`python
import numpy as np
90度回転行列
theta = np.pi / 2 # 90度をラジアンで表現
R_90 = np.array([[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]])
小数点以下を丸めて見やすくする
R_90 = np.round(R_90)
print(f”90度回転行列 R_90:\n{R_90}”)
回転させたいベクトル (点)
p = np.array([1, 0])
print(f”\n元のベクトル p: {p}”)
行列を掛けてベクトルを変換する
p_rotated = R_90 @ p
print(f”90度回転後のベクトル p’: {p_rotated}”)
“`
実行結果:
“`
90度回転行列 R_90:
[[ 0. -1.]
[ 1. 0.]]
元のベクトル p: [1 0]
90度回転後のベクトル p’: [0. 1.]
``
[1, 0]というベクトル(x軸上の点)が、
[0, 1]` というベクトル(y軸上の点)に変換されました。これは、まさしく90度回転した結果です!
このように、行列は「操作」や「変換」そのものを表しており、dot
(または@
)は、その操作を実際にベクトルに「適用する」行為なのです。この考え方は、画像処理(回転、拡大、縮小)や3Dグラフィックスの世界で中心的な役割を果たしています。
第4章: 行列と行列の積 – 変換の合成
いよいよラスボス、行列と行列の積です。しかし、第3章の「行列の行とベクトルの内積」という考え方を応用すれば、決して難しくありません。
行列同士の積は、「変換を連続して行うこと(変換の合成)」を意味します。例えば、「拡大してから、回転する」という2つの操作は、1つの行列積で表現できます。
4-1. 行列同士の積の計算ルール
行列 A
と行列 B
の積 C = A @ B
を計算するルールは以下の通りです。
結果の行列 C
の i
行 j
列目の要素 C_ij
は、A
の i
行目のベクトルと B
の j
列目のベクトルの内積によって計算されます。
言葉だけだと分かりにくいので、図で見てみましょう。
A
が (2, 3)
行列、B
が (3, 2)
行列だとします。
A = [[a11, a12, a13], B = [[b11, b12],
[a21, a22, a23]] [b21, b22],
[b31, b32]]
結果の行列 C
の (1, 1)
成分(1行1列目)を求めるには…
A
の 1行目 (a11, a12, a13)
と B
の 1列目 (b11, b21, b31)
の内積を取ります。
C_11 = a11*b11 + a12*b21 + a13*b31
C
の (1, 2)
成分(1行2列目)を求めるには…
A
の 1行目 と B
の 2列目 の内積を取ります。
C_12 = a11*b12 + a12*b22 + a13*b32
というように、左の行列からは「行」を、右の行列からは「列」を取り出して、順番に内積を計算し、結果の行列の対応する位置に埋めていくのです。
重要な計算条件
この計算が成り立つためには、左の行列 A
の「列数」と、右の行列 B
の「行数」が一致している必要があります。
- 行列
A
の shape:(m, k)
- 行列
B
の shape:(k, n)
このとき、積 C = A @ B
の shape は (m, n)
となります。真ん中の k
が消えて、外側の m
と n
が残るイメージです。
積の順序は重要!
普通の掛け算では 3 * 5
と 5 * 3
は同じですが、行列の積では A @ B
と B @ A
は一般的に異なる結果になります。場合によっては、片方は計算できても、もう片方はshapeが合わずに計算できないこともあります。これは非常に重要な性質なので、必ず覚えておいてください。
4-2. np.dot
を使った行列積
では、具体的な数値でNumpyで計算してみましょう。
“`python
import numpy as np
行列A (2行3列)
mat_A = np.array([[1, 2, 3],
[4, 5, 6]])
行列B (3行2列)
mat_B = np.array([[7, 8],
[9, 10],
[11, 12]])
print(f”行列Aのshape: {mat_A.shape}”)
print(f”行列Bのshape: {mat_B.shape}”)
print(“Aの列数(3)とBの行数(3)が一致。計算可能。”)
print(f”結果の行列のshapeは (2, 2) になるはず。”)
行列積を計算
方法1: np.dot()
result_mat_dot = np.dot(mat_A, mat_B)
print(f”\nnp.dot(A, B) の結果:\n{result_mat_dot}”)
方法2: @演算子
result_mat_at = mat_A @ mat_B
print(f”\nA @ B の結果:\n{result_mat_at}”)
“`
実行結果:
“`
行列Aのshape: (2, 3)
行列Bのshape: (3, 2)
Aの列数(3)とBの行数(3)が一致。計算可能。
結果の行列のshapeは (2, 2) になるはず。
np.dot(A, B) の結果:
[[ 58 64]
[139 154]]
A @ B の結果:
[[ 58 64]
[139 154]]
“`
手計算で確認してみましょう。
結果の (1, 1)
成分: A
の1行目(1,2,3)
と B
の1列目(7,9,11)
の内積
1*7 + 2*9 + 3*11 = 7 + 18 + 33 = 58
結果の (2, 2)
成分: A
の2行目(4,5,6)
と B
の2列目(8,10,12)
の内積
4*8 + 5*10 + 6*12 = 32 + 50 + 72 = 154
確かに計算が合っていますね。
順序を入れ替えてみる (B @ A
)
今度は B @ A
を計算してみましょう。
B
のshapeは(3, 2)
、A
のshapeは(2, 3)
です。B
の列数(2)とA
の行数(2)が一致しているので、計算は可能です。結果のshapeは(3, 3)
になるはずです。
“`python
B @ A を計算
result_BA = mat_B @ mat_A
print(f”Bのshape: {mat_B.shape}”)
print(f”Aのshape: {mat_A.shape}”)
print(f”B @ A の結果:\n{result_BA}”)
print(f”結果のshape: {result_BA.shape}”)
“`
実行結果:
Bのshape: (3, 2)
Aのshape: (2, 3)
B @ A の結果:
[[ 39 54 69]
[ 49 68 87]
[ 59 82 105]]
結果のshape: (3, 3)
A @ B
の結果 [[58, 64], [139, 154]]
とは全く異なる行列になりました。このように、行列積では計算の順序が結果を大きく変えることを、コードでしっかり確認できました。
4-3. 何が起きているのか? – 変換の連続適用
行列積が「変換の合成」であることを、先ほどの回転行列の例で見てみましょう。
操作1: ベクトルをx軸方向に2倍に拡大する。
操作2: その後、90度回転させる。
この2つの操作を考えます。
x軸方向に2倍に拡大する行列 S
は [[2, 0], [0, 1]]
です。
90度回転させる行列 R_90
は [[0, -1], [1, 0]]
でした。
ベクトル p = (1, 1)
にこの操作を順に適用してみます。
“`python
import numpy as np
元のベクトル
p = np.array([1, 1])
拡大行列
S = np.array([[2, 0], [0, 1]])
回転行列
R = np.array([[0, -1], [1, 0]])
ステップ1: pを拡大する
p_scaled = S @ p
print(f”元のベクトル p: {p}”)
print(f”拡大後のベクトル p_scaled: {p_scaled}”)
ステップ2: 拡大後のベクトルを回転する
p_final_stepwise = R @ p_scaled
print(f”最終的なベクトル (段階的計算): {p_final_stepwise}”)
print(“-” * 30)
次に、変換行列を先に合成する
注意: 操作の順序は行列を「左から」掛けていくことに対応する
「拡大(S)して、回転(R)する」は R @ S と書く
M = R @ S
print(f”合成された変換行列 M = R @ S:\n{M}”)
合成行列Mを元のベクトルpに一度に適用する
p_final_onestep = M @ p
print(f”最終的なベクトル (一括計算): {p_final_onestep}”)
結果が同じことを確認
print(f”\n段階的計算と一括計算の結果は同じ? -> {np.allclose(p_final_stepwise, p_final_onestep)}”)
“`
実行結果:
“`
元のベクトル p: [1 1]
拡大後のベクトル p_scaled: [2 1]
最終的なベクトル (段階的計算): [-1 2]
合成された変換行列 M = R @ S:
[[ 0 -1]
[ 2 0]]
最終的なベクトル (一括計算): [-1 2]
段階的計算と一括計算の結果は同じ? -> True
“`
素晴らしい結果が出ました!
1. ベクトル p
を S
で拡大し、その結果を R
で回転させる(段階的計算)
2. 変換行列 R
と S
を先に掛け合わせて一つの合成行列 M = R @ S
を作り、それを p
に適用する(一括計算)
この2つの方法が、全く同じ結果 [-1, 2]
を生み出しました。
これは、行列積がまさに変換の合成操作そのものであることを示しています。複数の変換をあらかじめ一つの行列にまとめておけば、多数のベクトルを効率的に一括変換できるため、非常に強力です。
第5章: np.dot
の多様な顔 – 2次元配列以外の場合
これまで、ベクトル(1D)と行列(2D)を中心に見てきましたが、np.dot
はもっと広い範囲の計算を扱えます。しかし、その挙動は少し複雑なため、初心者にとっては混乱の元にもなり得ます。ここでは、その挙動と、より現代的なnp.matmul
や@
演算子との違いを整理します。
5-1. スカラーとの積
np.dot
はスカラー(ただの数字)と配列の計算もできますが、この場合の挙動は、単純な掛け算 *
(要素ごとの積)と同じになります。
“`python
import numpy as np
scalar = 10
arr = np.array([1, 2, 3])
np.dot を使った場合
result_dot = np.dot(scalar, arr)
print(f”np.dot(10, [1, 2, 3]) = {result_dot}”)
* 演算子を使った場合
result_mul = scalar * arr
print(f”10 * [1, 2, 3] = {result_mul}”)
**実行結果:**
np.dot(10, [1, 2, 3]) = [10 20 30]
10 * [1, 2, 3] = [10 20 30]
``
*
結果は同じです。一般的に、スカラーと配列の積には可読性の高い演算子を使うのが普通です。
np.dot`をこの目的で使うことは稀です。
5-2. 多次元配列(N-D Array)の場合
np.dot
の挙動が最もトリッキーになるのが、3次元以上の配列を扱う場合です。
np.dot(a, b)
の仕様は、厳密には以下のようになっています。
- もし
a
とb
が両方とも2次元配列なら、それは行列積。 - もし
a
とb
が両方とも1次元配列なら、それは内積。 - もし
a
がN次元でb
がM次元の場合、dot
はa
の最後の軸 とb
の後ろから2番目の軸 に対して積和(sum-product)を取ります。(b
が1次元の場合は、その唯一の軸)
このルールは非常に汎用的ですが、直感的ではありません。
例えば、a
のshapeが(2, 3, 4)
、b
のshapeが(5, 4, 6)
だったとします。
* a
の最後の軸のサイズは 4
* b
の後ろから2番目の軸のサイズは 4
これらが一致しているので計算は可能です。結果のshapeは、それぞれの軸を除いた部分を連結した (2, 3, 5, 6)
になります。
これはテンソル積と呼ばれる、より一般化された計算です。初心者のうちは、この挙動を無理に覚える必要はありません。「dot
は高次元配列だと複雑なことをする」とだけ認識しておけば十分です。
5-3. np.dot
, np.matmul
, @
の違いまとめ
ここで、dot
と非常によく似た np.matmul
関数、そして @
演算子の違いを整理しておきましょう。これが分かれば、あなたはNumpy中級者です。
計算の種類 | np.dot(a, b) |
np.matmul(a, b) |
a @ b |
---|---|---|---|
ベクトル内積 (1D, 1D) |
OK (スカラーを返す) | OK (スカラーを返す) | OK (スカラーを返す) |
行列×ベクトル (2D, 1D) |
OK | OK | OK |
行列×行列 (2D, 2D) |
OK | OK | OK |
スカラー×配列 | OK (* と同じ) |
エラー | エラー |
多次元配列 (stack of matrices) |
テンソル積 (直感的でない) |
行列積として振る舞う (最後の2次元を複数行列と見なす) |
np.matmul と同じ |
解説:
np.matmul
と @
は、np.dot
よりも「行列積」という目的に特化した設計になっています。
- スカラーを扱えない: これらは行列積のための関数/演算子なので、スカラーとの積はエラーになります。これは意図しない計算を防ぐ上で安全な仕様です。
- 多次元配列の扱いが直感的:
matmul
と@
は、3次元以上の配列を「行列の束(a stack of matrices)」として扱います。例えば、shapeが(10, 3, 4)
の配列は、「3×4行列が10個ある」と解釈します。(10, 3, 4) @ (10, 4, 5)
という計算は、10個の行列積をそれぞれ独立に行い、結果として(10, 3, 5)
の配列を返します。これは、バッチ処理が基本となる機械学習などで非常に便利な挙動です。
結論として、現代のNumpyプログラミングでは、以下の使い分けが推奨されます。
- 行列積(行列×行列, 行列×ベクトル)をしたい場合: 迷わず
@
演算子 を使いましょう。np.matmul
と同じですが、より簡潔で可読性が高いです。 - ベクトルの内積を計算したい場合:
np.dot(a, b)
またはa @ b
のどちらでもOKです。@
の方が一貫性があるかもしれません。 np.dot
を使う場面: 古いコードをメンテナンスする場合や、テンソル積のようなより汎用的な計算が必要な特殊なケースに限られます。
初心者のうちは、「行列の計算には@
を使う」と覚えておけば、ほとんどの場面で困ることはないでしょう。
第6章: 実践!dot
(@
)を使ってみよう
理論を学んだら、次は実践です。dot
(ここでは主に@
を使います)が、実際の問題解決にどう役立つのかを3つの例題で見ていきましょう。
6-1. 例題1: 連立一次方程式を解く
第1章で登場した連立方程式を、行列の力で解いてみましょう。
2x + 3y = 8
4x + 1y = 6
これは、行列とベクトルを使って A @ x = b
という形で表現できます。
A = [[2, 3], [4, 1]]
(係数行列)
x = [x, y]
(未知数のベクトル)
b = [8, 6]
(定数項のベクトル)
この方程式を x
について解くには、両辺に左から A
の逆行列 A⁻¹
を掛けます。
A⁻¹ @ A @ x = A⁻¹ @ b
I @ x = A⁻¹ @ b
(ここで I
は単位行列で、掛けても何も変わらない)
x = A⁻¹ @ b
つまり、係数行列A
の逆行列を求めて、定数項ベクトルb
に掛ければ、解 x
が得られるのです。
“`python
import numpy as np
係数行列 A
A = np.array([[2, 3],
[4, 1]])
定数項ベクトル b
b = np.array([8, 6])
Aの逆行列を計算
np.linalg は線形代数(Linear Algebra)関連の機能が集まっているモジュール
A_inv = np.linalg.inv(A)
print(f”行列A:\n{A}”)
print(f”\n逆行列 A_inv:\n{A_inv}”)
解 x を計算
x = A_inv @ b
print(f”\n解 x = [x, y]: {x}”)
print(f”つまり、x = {x[0]}, y = {x[1]}”)
検算: A @ x が b に戻るか確認
print(f”\n検算 (A @ x): {A @ x}”)
print(f”元のb: {b}”)
“`
実行結果:
“`
行列A:
[[2 3]
[4 1]]
逆行列 A_inv:
[[-0.1 0.3]
[ 0.4 -0.2]]
解 x = [x, y]: [1. 2.]
つまり、x = 1.0, y = 2.0
検算 (A @ x): [8. 6.]
元のb: [8 6]
``
x=1, y=2という解が見事に求まりました!検算結果も元の
bと一致しています。行列積が、方程式を解くための強力なツールであることがわかります。
np.linalg.solve(A, b)
(補足: 実は、Numpyにはより数値的に安定して高速なという専用関数があります。逆行列を陽に計算するのはコストが高いので、実用上は
solve` を使うのがベストプラクティスです。)
6-2. 例題2: データの共分散行列を計算する
データサイエンスでは、複数の特徴量(身長と体重など)がどのように関係しているかを調べるために共分散行列を計算することがよくあります。
ここでは、5人分の「勉強時間」と「テストの点数」のデータがあるとします。
“`python
import numpy as np
データ: 各行が一人分のデータ [勉強時間, テストの点数]
5人分なので (5, 2) の行列
X = np.array([
[2, 60],
[3, 75],
[5, 85],
[1, 50],
[4, 80]
])
1. 各特徴量の平均値を計算
mean = np.mean(X, axis=0) # axis=0 は列ごとの平均
print(f”平均値 [勉強時間, 点数]: {mean}”)
2. 各データから平均値を引く (センタリング)
X_centered = X – mean
print(f”\nセンタリング後のデータ:\n{X_centered}”)
3. 共分散行列を計算
式: C = (1/N) * X_centered^T @ X_centered
Nはデータ数
N = X.shape[0]
X_centered.T は転置行列 (行と列を入れ替えたもの)
cov_matrix = (1 / (N-1)) * (X_centered.T @ X_centered) # 普遍共分散はN-1で割る
print(f”\n共分散行列:\n{cov_matrix}”)
“`
実行結果:
“`
平均値 [勉強時間, 点数]: [3. 70.]
センタリング後のデータ:
[[-1. -10.]
[ 0. 5.]
[ 2. 15.]
[-2. -20.]
[ 1. 10.]]
共分散行列:
[[ 2.5 27.5 ]
[ 27.5 325. ]]
``
X_centered.Tは
(2, 5)行列、
X_centeredは
(5, 2)行列なので、
@で計算した結果は
(2, 2)` の共分散行列になります。
* 対角成分(2.5, 325)は各特徴量の分散(ばらつき具合)を表します。
* 非対角成分(27.5)は2つの特徴量間の共分散を表します。正の値なので、「勉強時間が増えるほど、テストの点数も上がる」という正の相関があることが示唆されます。
ここでも、中心的な計算は T
(転置) と @
(行列積) であり、データの特徴を要約する上で dot
がいかに重要かが分かります。
6-3. 例題3: ニューラルネットワークの順伝播(超入門)
機械学習、特にディープラーニングの根幹をなすニューラルネットワークの計算は、まさに行列積のオンパレードです。
入力層から隠れ層、隠れ層から出力層への信号の伝播は、(重み行列 @ 入力) + バイアス
という計算の繰り返しです。
非常にシンプルなネットワークで、その計算を体験してみましょう。
* 入力層: 2つのニューロン(例: 部屋の広さ、駅からの距離)
* 隠れ層: 3つのニューロン
* 出力層: 1つのニューロン(例: 家賃)
“`python
import numpy as np
シグモイド関数 (活性化関数の一種)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
ネットワークのパラメータ (通常は学習によって決まるが、ここでは固定値)
入力層(2) -> 隠れ層(3) の重み行列 W1
W1 = np.array([[0.1, 0.3],
[0.5, 0.2],
[0.4, 0.6]]) # shape: (3, 2)
隠れ層のバイアス b1
b1 = np.array([0.1, 0.2, 0.3]) # shape: (3,)
隠れ層(3) -> 出力層(1) の重み行列 W2
W2 = np.array([[0.7, 0.8, 0.9]]) # shape: (1, 3)
出力層のバイアス b2
b2 = np.array([0.4]) # shape: (1,)
入力データ
input_data = np.array([1.5, 2.0]) # [広さ=1.5, 距離=2.0]
— 順伝播計算 —
1. 入力層 -> 隠れ層
(3, 2) @ (2,) -> (3,)
hidden_layer_input = W1 @ input_data + b1
print(f”隠れ層への入力: {hidden_layer_input}”)
活性化関数を適用
hidden_layer_output = sigmoid(hidden_layer_input)
print(f”隠れ層からの出力: {hidden_layer_output}”)
2. 隠れ層 -> 出力層
(1, 3) @ (3,) -> (1,)
output_layer_input = W2 @ hidden_layer_output + b2
print(f”\n出力層への入力: {output_layer_input}”)
最終的な予測値 (この例では活性化関数は省略)
predicted_price = output_layer_input
print(f”予測された家賃: {predicted_price}”)
**実行結果:**
隠れ層への入力: [0.85 1.35 1.9 ]
隠れ層からの出力: [0.69997368 0.79413233 0.86989154]
出力層への入力: [2.01280335]
予測された家賃: [2.01280335]
``
[1.5, 2.0]
入力データが、
W1 @ … + b1、
W2 @ … + b2という一連の行列・ベクトル演算を経て、最終的な予測値
[2.0128…]` に変換されました。
実際のニューラルネットワークはもっと巨大で複雑ですが、その基本計算は今あなたが行った行列積そのものです。Numpy(とその背後にある高速なライブラリ)がなければ、現代のAIの発展はあり得ませんでした。
まとめ
この記事では、PythonのNumpyライブラリにおける最重要関数の一つ、np.dot
について、その基礎から応用までを徹底的に解説してきました。
あなたがこの記事で学んだこと:
* ベクトルと行列: これらが単なる数字の集まりではなく、データを表現し、変換するための強力なツールであること。
* dot
の3つの主要な計算:
1. ベクトル内積 (a @ b
): 2つのベクトルの関係性を一つの数値にする。
2. 行列ベクトル積 (A @ v
): 行列という「変換ルール」をベクトルに適用し、新しいベクトルを作る。
3. 行列積 (A @ B
): 複数の変換を一つに合成する。
* @
演算子: np.dot
よりも行列積の意図が明確になり、現代のコードでは主流であること。
* dot
vs matmul
vs @
: それぞれの挙動の違い、特に多次元配列の扱いを理解し、目的に応じて@
演算子を第一候補として選ぶべきであること。
* 実践的な応用: 連立方程式の解法、共分散行列の計算、そしてニューラルネットワークの順伝播計算など、dot
(@
)が科学技術計算のあらゆる場面で心臓部として機能していること。
np.dot
は、一見するとただの数学関数ですが、その背後には「空間をねじ曲げたり、回転させたりする」「データ間の隠れた関係をあぶり出す」「複雑なシステムの挙動をシミュレートする」といった、ダイナミックでパワフルな概念が広がっています。
この記事が、あなたのNumpy学習の確かな一歩となり、データサイエンスや機械学習の世界への扉を開くきっかけとなれば幸いです。数学的な背景に苦手意識があった方も、コードを通じてその動きを体感することで、新しい理解が得られたのではないでしょうか。
さあ、恐れることはありません。Jupyter NotebookやGoogle Colaboratoryを開いて、あなた自身の手で様々なベクトルや行列を作り、@
で計算してみてください。shape
を確認し、エラーと格闘し、そして計算結果が何を意味するのかを考える。その試行錯誤こそが、あなたを真のNumpyマスターへと導く最短の道です。