NumPy 행렬 곱셈: np.dot, matmul, @ 완전 가이드
Updated on
행렬 곱셈은 머신러닝, 컴퓨터 그래픽스, 신호 처리, 과학 컴퓨팅의 기초입니다. 하지만 NumPy는 행렬을 곱하는 세 가지 방법 -- 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)) # ValueErrornp.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의 끝에서 두 번째에 대해 합산
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 내적만 | 아다마르용 |
내적 (벡터)
두 벡터의 내적은 요소별 곱의 합입니다:
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 # 첫 번째 층 가중치
b1 = np.zeros(4)
W2 = np.random.randn(2, 4) * 0.5 # 두 번째 층 가중치
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 @ B는 C[i,j] = sum(A[i,k] * B[k,j]) 규칙에 따라 행렬 곱셈을 수행합니다. 이들은 근본적으로 다른 연산입니다.
NumPy에서 두 벡터의 내적을 어떻게 계산하나요?
1D 배열(벡터)의 경우 np.dot(a, b), a @ b, 또는 np.sum(a * b)를 사용합니다. 세 가지 모두 동일한 스칼라 결과를 반환합니다: 요소별 곱의 합.
행렬 곱셈에 필요한 shape는 무엇인가요?
A @ B에서 A의 마지막 차원은 B의 끝에서 두 번째 차원과 같아야 합니다. 2D의 경우: (m, n) @ (n, p) = (m, p). A의 열 수가 B의 행 수와 같아야 합니다.
결론
NumPy에서 행렬 곱셈에는 @ 연산자를 기본으로 사용하세요 -- 가장 읽기 쉽고 배치 연산을 올바르게 처리합니다. np.dot()은 명시적인 1D 벡터 내적에만 사용하세요. *(요소별)와 @(행렬 곱셈)를 절대 혼동하지 마세요. shape 규칙을 기억하세요: (m, n) @ (n, p) = (m, p), 그러면 가장 일반적인 행렬 곱셈 오류를 피할 수 있습니다.