Skip to content

NumPy行列乗算:np.dot、matmul、@の完全ガイド

Updated on

行列乗算は機械学習、コンピュータグラフィックス、信号処理、科学計算の基礎です。しかしNumPyは行列を乗算する3つの方法 -- np.dot()np.matmul()@演算子 -- を提供しており、それらの違いは混乱を招きます。間違ったものを使用すると、特に異なる次元の配列で作業する場合や、要素ごとの乗算(*)と行列乗算を混同する場合に、暗黙のうちに誤った結果を生成する可能性があります。

このガイドでは各メソッドを明確にし、いつどれを使うべきかを示し、一般的な線形代数パターンの実用的な例を提供します。

📚

要素ごとの乗算 vs 行列乗算

まず、重要な区別です。*演算子は要素ごとの乗算を行い、行列乗算ではありません:

import numpy as np
 
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
 
# 要素ごとの乗算(アダマール積)
print(A * B)
# [[ 5 12]
#  [21 32]]
 
# 行列乗算
print(A @ B)
# [[19 22]
#  [43 50]]

行列乗算はルール C[i,j] = sum(A[i,k] * B[k,j]) に従います(すべてのkについて)。

@演算子(推奨)

@演算子(Python 3.5で導入)は行列乗算の最もクリーンで読みやすい方法です:

import numpy as np
 
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
 
C = A @ B
print(C)
# [[19 22]
#  [43 50]]

Shape規則

行列乗算 A @ B が機能するには、Aの列数がBの行数と等しくなければなりません:

import numpy as np
 
# (2, 3) @ (3, 4) = (2, 4)
A = np.ones((2, 3))
B = np.ones((3, 4))
C = A @ B
print(C.shape)  # (2, 4)
 
# (5, 3) @ (3, 2) = (5, 2)
A = np.random.randn(5, 3)
B = np.random.randn(3, 2)
C = A @ B
print(C.shape)  # (5, 2)
 
# 不一致はValueErrorを発生
# np.ones((2, 3)) @ np.ones((4, 2))  # ValueError

np.matmul()

np.matmul()@と全く同じことをします。すべての実用的な目的で同等です:

import numpy as np
 
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
 
# これらは同一の結果を生成
result1 = A @ B
result2 = np.matmul(A, B)
 
print(np.array_equal(result1, result2))  # True

よりクリーンなコードには@を使用してください。関数の引数として渡す必要がある場合(例:reduce)はnp.matmul()を使用してください。

np.dot()

np.dot()は古く、高次元配列では@/matmulとは異なる動作をします:

import numpy as np
 
# 1D配列の場合:内積(スカラー結果)
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(np.dot(a, b))  # 32 (1*4 + 2*5 + 3*6)
 
# 2D配列の場合:行列乗算(@と同じ)
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(np.dot(A, B))
# [[19 22]
#  [43 50]]

主な違い:高次元

2次元を超える配列では、np.dot()@は異なる動作をします:

import numpy as np
 
A = np.random.randn(2, 3, 4)
B = np.random.randn(2, 4, 5)
 
# @は行列乗算のバッチとして扱う:(2, 3, 4) @ (2, 4, 5) = (2, 3, 5)
result_matmul = A @ B
print(result_matmul.shape)  # (2, 3, 5)
 
# np.dotは異なるブロードキャスティングを使用:Aの最後の軸とBの最後から2番目で合計
result_dot = np.dot(A, B)
print(result_dot.shape)  # (2, 3, 2, 5) -- 異なる形状!

推奨: 行列乗算には@またはnp.matmul()を使用してください。np.dot()は1Dベクトルの明示的な内積にのみ使用してください。

メソッド比較

特徴@演算子np.matmul()np.dot()*演算子
操作行列乗算行列乗算内積要素ごと
1D x 1D内積(スカラー)内積(スカラー)内積(スカラー)要素ごと
2D x 2D行列乗算行列乗算行列乗算要素ごと
N-Dバッチバッチmatmulバッチmatmul異なるセマンティクス要素ごと
可読性最良良好まあまあN/A
スカラー許可いいえいいえはいはい
推奨はいはい(関数的)1D内積のみアダマール用

内積(ベクトル)

2つのベクトルの内積は、要素ごとの積の合計です:

import numpy as np
 
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
 
# 1Dベクトルではすべて同等
print(np.dot(a, b))    # 32
print(a @ b)            # 32
print(np.sum(a * b))    # 32

アプリケーション

import numpy as np
 
# コサイン類似度
def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
 
vec1 = np.array([1, 2, 3])
vec2 = np.array([4, 5, 6])
print(f"Cosine similarity: {cosine_similarity(vec1, vec2):.4f}")
# 0.9746
 
# aのb上への射影
def project(a, b):
    return (np.dot(a, b) / np.dot(b, b)) * b
 
print(project(np.array([3, 4]), np.array([1, 0])))
# [3. 0.]

行列-ベクトル乗算

import numpy as np
 
# 線形変換
A = np.array([[2, 0], [0, 3]])  # スケーリング行列
v = np.array([1, 1])
 
result = A @ v
print(result)  # [2 3]
 
# ニューラルネットワーク層で重みを適用
weights = np.random.randn(10, 5)  # 10出力、5入力
inputs = np.random.randn(5)
output = weights @ inputs
print(output.shape)  # (10,)

バッチ行列乗算

ディープラーニングとバッチ処理では、@がバッチを自動的に処理します:

import numpy as np
 
# 32個の行列のバッチ、各4x3、掛ける32個の行列のバッチ、各3x5
batch_A = np.random.randn(32, 4, 3)
batch_B = np.random.randn(32, 3, 5)
 
result = batch_A @ batch_B
print(result.shape)  # (32, 4, 5)

実用的な例

連立一次方程式

import numpy as np
 
# Ax = b を解く
# 2x + 3y = 8
# 4x + y = 10
A = np.array([[2, 3], [4, 1]])
b = np.array([8, 10])
 
x = np.linalg.solve(A, b)
print(f"x = {x[0]:.2f}, y = {x[1]:.2f}")
# x = 2.20, y = 1.20
 
# 検証:A @ x は b に等しいはず
print(A @ x)  # [8. 10.]

シンプルなニューラルネットワークの順伝播

import numpy as np
 
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
 
# ネットワーク:3入力 -> 4隠れ -> 2出力
np.random.seed(42)
W1 = np.random.randn(4, 3) * 0.5  # 第1層の重み
b1 = np.zeros(4)
W2 = np.random.randn(2, 4) * 0.5  # 第2層の重み
b2 = np.zeros(2)
 
# 順伝播
X = np.array([1.0, 0.5, -1.0])  # 入力
h = sigmoid(W1 @ X + b1)         # 隠れ層
y = sigmoid(W2 @ h + b2)         # 出力層
 
print(f"Input:  {X}")
print(f"Hidden: {h}")
print(f"Output: {y}")

PCA射影

import numpy as np
 
# サンプルデータを生成
np.random.seed(42)
data = np.random.randn(100, 5)  # 100サンプル、5特徴量
 
# データを中心化
data_centered = data - data.mean(axis=0)
 
# 行列乗算で共分散行列を計算
cov = (data_centered.T @ data_centered) / (len(data) - 1)
 
# 固有値分解
eigenvalues, eigenvectors = np.linalg.eigh(cov)
 
# 上位2成分に射影
top_2 = eigenvectors[:, -2:]  # 最後の2つ(最大固有値)
projected = data_centered @ top_2
print(f"Original shape: {data.shape}")
print(f"Projected shape: {projected.shape}")  # (100, 2)

行列演算の可視化

行列変換がデータに与える影響を探索するために、PyGWalker (opens in a new tab)を使用するとJupyterで射影データをインタラクティブに可視化できます:

import pandas as pd
import pygwalker as pyg
 
df = pd.DataFrame(projected, columns=['PC1', 'PC2'])
walker = pyg.walk(df)

FAQ

np.dotとnp.matmulの違いは何ですか?

1Dと2D配列では同じ結果を生成します。主な違いは高次元配列にあります:np.matmul()(と@)はそれらを行列のバッチとして扱いますが、np.dot()は異なるブロードキャスティングルールを使用します。新しいコードでは@またはnp.matmul()を優先してください。

NumPyの@演算子は何をしますか?

@演算子はnp.matmul()と同等の行列乗算を実行します。Python 3.5で導入されました。2D配列の場合、A @ Bは標準的な行列積を計算します。高次元配列の場合、バッチ行列乗算を実行します。

A * BとA @ Bで異なる結果が出るのはなぜですか?

A * Bは要素ごとの乗算(アダマール積)を実行し、対応する要素を乗算します。A @ BC[i,j] = sum(A[i,k] * B[k,j])のルールに従い行列乗算を実行します。これらは根本的に異なる演算です。

NumPyで2つのベクトルの内積を計算するにはどうすればよいですか?

1D配列(ベクトル)の場合、np.dot(a, b)a @ b、またはnp.sum(a * b)を使用します。3つとも同じスカラー結果を返します:要素ごとの積の合計。

行列乗算にはどのようなshapeが必要ですか?

A @ Bの場合、Aの最後の次元がBの最後から2番目の次元と等しくなければなりません。2Dの場合:(m, n) @ (n, p) = (m, p)。Aの列数がBの行数と等しくなければなりません。

まとめ

NumPyでの行列乗算には、デフォルトとして@演算子を使用してください -- 最も読みやすく、バッチ操作を正しく処理します。np.dot()は明示的な1Dベクトルの内積にのみ使用してください。*(要素ごと)と@(行列乗算)を決して混同しないでください。shape規則を覚えてください:(m, n) @ (n, p) = (m, p)。これで最も一般的な行列乗算のエラーを避けることができます。

📚