NumPy를 이용한 행렬–벡터곱과 행렬곱
이 자료에서 $[\boldsymbol A]$, $[\boldsymbol x]$는 행렬·벡터의 성분 배열을 뜻하며, 배열의 행렬곱은 $[\boldsymbol A][\boldsymbol x]$처럼 붙여 쓴다. 물리적 벡터·텐서의 단일수축은 $\cdot$로 표시한다.
이번 강의가 끝나면 다음을 할 수 있어야 한다.
np.linalg.solve()로 간단한 연립방정식을 풀 수 있다.$m$행 $n$열의 행렬은 $m\times n$ 행렬이라고 한다.
\[[\boldsymbol A]= \begin{bmatrix} 1&2&3\\ 4&5&6 \end{bmatrix}\]은 2행 3열 행렬이다.
import numpy as np
A = np.array([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
])
print(A)
print(A.shape)
print(A.ndim)
A.shape은 (2, 3)이고 A.ndim은 2이다. 행렬 성분 $A_{ij}$는 Python에서 A[i, j]로 선택한다. 다만 수학에서 첨자는 1부터, Python의 인덱스는 0부터 시작한다.
print(A[0, 0])
print(A[1, 2])
shape이 같은 두 행렬의 합과 차는 같은 위치의 성분끼리 계산한다.
A = np.array([
[1.0, 2.0],
[3.0, 4.0],
])
B = np.array([
[5.0, 6.0],
[7.0, 8.0],
])
print(A + B)
print(A - B)
별표 연산자 *는 행렬곱이 아니라 원소별 곱을 계산한다.
elementwise_product = A * B
print(elementwise_product)
결과는
\[\begin{bmatrix} 5&12\\ 21&32 \end{bmatrix}\]이다.
행렬
\[[\boldsymbol A]= \begin{bmatrix} 1&2\\ 3&4 \end{bmatrix}\]와 벡터
\[[\boldsymbol x]= \begin{bmatrix} 5\\6 \end{bmatrix}\]의 곱은 각 행과 벡터의 내적으로 계산한다.
\[[\boldsymbol A][\boldsymbol x] = \begin{bmatrix} 1\times5+2\times6\\ 3\times5+4\times6 \end{bmatrix} = \begin{bmatrix} 17\\39 \end{bmatrix}\]A = np.array([
[1.0, 2.0],
[3.0, 4.0],
])
x = np.array([5.0, 6.0])
result = A @ x
print(result)
print(result.shape)
$(m,n)$ 행렬과 shape이 $(n,)$인 벡터를 곱하면 shape이 $(m,)$인 벡터가 나온다.
두 행렬
\[[\boldsymbol A]= \begin{bmatrix} 1&2\\ 3&4 \end{bmatrix}, \qquad [\boldsymbol B]= \begin{bmatrix} 2&0\\ 1&3 \end{bmatrix}\]의 곱을 생각하자. 결과의 각 성분은 $A$의 한 행과 $B$의 한 열의 내적이다.
\[\begin{aligned} C_{11}&=1\times2+2\times1=4,\\ C_{12}&=1\times0+2\times3=6,\\ C_{21}&=3\times2+4\times1=10,\\ C_{22}&=3\times0+4\times3=12. \end{aligned}\]따라서
\[[\boldsymbol C] = [\boldsymbol A][\boldsymbol B] = \begin{bmatrix} 4&6\\ 10&12 \end{bmatrix}\]이다.
A = np.array([
[1.0, 2.0],
[3.0, 4.0],
])
B = np.array([
[2.0, 0.0],
[1.0, 3.0],
])
C = A @ B
print(C)
일반적으로 $A$가 $(l,m)$ 행렬이고 $B$가 $(m,n)$ 행렬이면
\[C_{ij} = \sum_{k=1}^{m}A_{ik}B_{kj}\]이고 결과 $C$의 크기는 $(l,n)$이다.
\[C_{ij} = \sum_{k=1}^{m}A_{ik}B_{kj} \text{ with } i=1,2,..., l \ \ j=1,2,...,n\]학습을 위해 반복문으로 같은 계산을 구현하면 다음과 같다.
rows_a, columns_a = A.shape
rows_b, columns_b = B.shape
C_loop = np.zeros((rows_a, columns_b))
for i in range(rows_a):
for j in range(columns_b):
for k in range(columns_a):
C_loop[i, j] += A[i, k] * B[k, j]
print(C_loop)
print(np.allclose(C, C_loop))
실제 계산에서는 반복문보다 A @ B를 사용한다. 연산 속도가 더 빠르다.
행렬곱에서는 안쪽 크기가 같아야 한다.
\[(l,m)(m,n)\rightarrow(l,n)\]예를 들어 다음 곱은 가능하다.
A = np.ones((2, 3))
B = np.ones((3, 4))
C = A @ B
print(C.shape)
$(2,3)(3,4)$의 안쪽 크기 3이 같고 결과 shape은 $(2,4)$이다.
다음 곱은 안쪽 크기가 다르므로 계산할 수 없다.
(2, 3) @ (2, 4)
행렬곱은 일반적으로 순서를 바꿀 수 없다.
\[[\boldsymbol A][\boldsymbol B] \ne [\boldsymbol B][\boldsymbol A]\]shape에 따라 한쪽 순서의 곱만 가능한 경우도 있다.
단위행렬(identity matrix)은 대각성분이 1이고 나머지가 0인 정사각행렬이다.
I = np.eye(3)
x = np.array([2.0, 3.0, 4.0])
print(I)
print(I @ x)
단위행렬을 곱하면 원래 벡터나 행렬이 유지된다.
\[[\boldsymbol I][\boldsymbol x]=[\boldsymbol x]\]전치(transpose)는 행과 열을 바꾼다.
A = np.array([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
])
A_transpose = A.T
print(A.shape)
print(A_transpose.shape)
print(A_transpose)
$(2,3)$ 행렬의 전치는 $(3,2)$ 행렬이다.
행렬식(determinant)은 정사각행렬에 대해 정의되는 스칼라이다. 2×2 행렬에서는
\[\det \begin{bmatrix} a&b\\c&d \end{bmatrix} =ad-bc\]이다.
A = np.array([
[2.0, 1.0],
[1.0, 3.0],
])
determinant = np.linalg.det(A)
print(determinant)
행렬식이 0이면 역행렬이 존재하지 않는다.
역행렬은 다음 관계를 만족한다.
\[[\boldsymbol A]^{-1}[\boldsymbol A] = [\boldsymbol I]\]A_inverse = np.linalg.inv(A)
print(A_inverse)
print(A_inverse @ A)
부동소수점 계산 결과는 정확한 정수 대신 0에 매우 가깝거나 1에 매우 가까운 값으로 나타날 수 있다. 두 배열이 가까운지 확인할 때는 np.allclose()를 사용한다.
print(
np.allclose(
A_inverse @ A,
np.eye(2),
)
)
다음 연립방정식을 생각하자.
\[\begin{aligned} 2x+y&=5,\\ x+3y&=6. \end{aligned}\]행렬 형태는
\[[\boldsymbol A][\boldsymbol x]=[\boldsymbol b]\]이며
\[[\boldsymbol A]= \begin{bmatrix}2&1\\1&3\end{bmatrix}, \quad [\boldsymbol x]= \begin{bmatrix}x\\y\end{bmatrix}, \quad [\boldsymbol b]= \begin{bmatrix}5\\6\end{bmatrix}\]이다.
A = np.array([
[2.0, 1.0],
[1.0, 3.0],
])
b = np.array([5.0, 6.0])
x = np.linalg.solve(A, b)
print(x)
print(A @ x)
해는 $x=1.8$, $y=1.4$이고 A @ x는 원래 우변 $b$와 같다.
연립방정식을 풀 때는 역행렬을 직접 계산하여 np.linalg.inv(A) @ b로 구하기보다 np.linalg.solve(A, b)를 사용하는 것이 좋다.
직교하는 격자축의 길이가 $a=2$, $b=3$, $c=4$라고 하자. 격자 좌표 $\boldsymbol p=(0.5,0.5,0.25)$를 실제 위치로 바꾸는 행렬은
\[[\boldsymbol L]= \begin{bmatrix} 2&0&0\\ 0&3&0\\ 0&0&4 \end{bmatrix}\]이다.
lattice_matrix = np.diag([2.0, 3.0, 4.0])
fractional_position = np.array([
0.5,
0.5,
0.25,
])
cartesian_position = (
lattice_matrix @ fractional_position
)
print(cartesian_position)
결과는 $(1,1.5,1)$이다.
fractional_p = np.array([0.0, 0.0, 0.0])
fractional_q = np.array([0.5, 0.5, 0.25])
cartesian_p = lattice_matrix @ fractional_p
cartesian_q = lattice_matrix @ fractional_q
distance = np.linalg.norm(
cartesian_q - cartesian_p
)
print(distance)
두 점 사이의 거리는 $\sqrt{1^2+1.5^2+1^2}\approx2.062$이다.
A * B를 행렬곱이라고 생각한다.*는 원소별 곱이고 @는 행렬곱이다.np.eye()는 단위행렬을 만든다.A.T는 전치행렬이다.np.linalg.det()와 np.linalg.inv()로 행렬식과 역행렬을 계산한다.np.linalg.solve()로 푼다.다음 행렬의 shape을 쓰시오.
\[\begin{bmatrix} 1&2&3\\ 4&5&6 \end{bmatrix}\]NumPy에서 원소별 곱과 행렬곱에 사용하는 연산자를 각각 쓰시오.
다음 행렬–벡터곱을 계산하라.
\[\begin{bmatrix} 1&0\\ 0&2 \end{bmatrix} \begin{bmatrix} 3\\4 \end{bmatrix}\]shape이 $(2,3)$인 행렬과 $(3,4)$인 행렬을 곱할 수 있는가? 결과 shape도 쓰시오.
단위행렬 $\boldsymbol I$와 벡터 $\boldsymbol x$의 곱은 무엇인가?
2×2 행렬 $\begin{bmatrix}2&1\1&3\end{bmatrix}$의 행렬식을 구하라.
행렬 $A$와 벡터 $b$로 이루어진 연립방정식 $Ax=b$를 푸는 NumPy 함수를 쓰시오.