행렬 연산 기초

NumPy를 이용한 행렬–벡터곱과 행렬곱

1. 학습 목표

이 자료에서 $[\boldsymbol A]$, $[\boldsymbol x]$는 행렬·벡터의 성분 배열을 뜻하며, 배열의 행렬곱은 $[\boldsymbol A][\boldsymbol x]$처럼 붙여 쓴다. 물리적 벡터·텐서의 단일수축은 $\cdot$로 표시한다.

이번 강의가 끝나면 다음을 할 수 있어야 한다.

2. 행렬과 NumPy 배열

$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])

3. 행렬의 합과 원소별 곱

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}\]

이다.

4. 행렬–벡터곱

행렬

\[[\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,)$인 벡터가 나온다.

5. 행렬곱

두 행렬

\[[\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를 사용한다. 연산 속도가 더 빠르다.

6. 행렬곱의 shape

행렬곱에서는 안쪽 크기가 같아야 한다.

\[(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에 따라 한쪽 순서의 곱만 가능한 경우도 있다.

7. 단위행렬과 전치행렬

7.1. 단위행렬

단위행렬(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]\]

7.2. 전치행렬

전치(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)$ 행렬이다.

8. 행렬식과 역행렬

8.1. 행렬식

행렬식(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이면 역행렬이 존재하지 않는다.

8.2. 역행렬

역행렬은 다음 관계를 만족한다.

\[[\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),
    )
)

9. 연립방정식 풀기

다음 연립방정식을 생각하자.

\[\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)를 사용하는 것이 좋다.

10. 재료공학 예제

10.1. 격자 좌표를 실제 위치로 변환하기

직교하는 격자축의 길이가 $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)$이다.

10.2. 두 격자점 사이의 거리

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$이다.

11. 자주 하는 실수

12. 정리

13. 연습 문제

문제 1

다음 행렬의 shape을 쓰시오.

\[\begin{bmatrix} 1&2&3\\ 4&5&6 \end{bmatrix}\]

문제 2

NumPy에서 원소별 곱과 행렬곱에 사용하는 연산자를 각각 쓰시오.

문제 3

다음 행렬–벡터곱을 계산하라.

\[\begin{bmatrix} 1&0\\ 0&2 \end{bmatrix} \begin{bmatrix} 3\\4 \end{bmatrix}\]

문제 4

shape이 $(2,3)$인 행렬과 $(3,4)$인 행렬을 곱할 수 있는가? 결과 shape도 쓰시오.

문제 5

단위행렬 $\boldsymbol I$와 벡터 $\boldsymbol x$의 곱은 무엇인가?

문제 6

2×2 행렬 $\begin{bmatrix}2&1\1&3\end{bmatrix}$의 행렬식을 구하라.

문제 7

행렬 $A$와 벡터 $b$로 이루어진 연립방정식 $Ax=b$를 푸는 NumPy 함수를 쓰시오.