선형변환의 고유값과 고유벡터를 이해하고 NumPy로 계산하기
이 자료에서 $[\boldsymbol A]$, $[\boldsymbol x]$는 행렬·벡터의 성분 배열을 뜻하며, 배열의 행렬곱은 $[\boldsymbol A][\boldsymbol x]$처럼 붙여 쓴다. 물리적 벡터·텐서의 단일수축은 $\cdot$로 표시한다.
이번 강의가 끝나면 다음을 할 수 있어야 한다.
np.linalg.eig()를 사용하여 고유값과 고유벡터를 계산할 수 있다.행렬 $\boldsymbol A$를 벡터 $\boldsymbol v$에 곱하면 일반적으로 벡터의 방향과 크기가 모두 바뀐다. 그러나 특별한 방향의 벡터는 행렬을 곱한 뒤에도 방향이 변하지 않고 크기만 변한다.
\[[\boldsymbol A][\boldsymbol v] = \lambda [\boldsymbol v]\]이 식을 만족하는 0이 아닌 벡터 $\boldsymbol v$를 고유벡터(eigenvector)라고 하고, 그때의 스칼라 $\lambda$를 고유값(eigenvalue)이라고 한다.
고유값의 부호에 따라 다음과 같이 해석할 수 있다.
| $ | \lambda | >1$: 크기가 커짐 |
| $ | \lambda | <1$: 크기가 작아짐 |
고유벡터는 길이가 1로 정규화되어 있거나 반대 방향으로 표시될 수 있다. 같은 고유벡터라도 상수배한 벡터는 같은 방향을 나타낸다.
다음 행렬을 생각해 보자.
\[[\boldsymbol A]= \begin{bmatrix} 2&0\\ 0&1 \end{bmatrix}\]이 행렬은 $x$ 방향을 2배로 늘리고 $y$ 방향은 그대로 둔다.
\[\boldsymbol A \begin{bmatrix}1\\0\end{bmatrix} =2\begin{bmatrix}1\\0\end{bmatrix}, \qquad \boldsymbol A \begin{bmatrix}0\\1\end{bmatrix} =1\begin{bmatrix}0\\1\end{bmatrix}\]따라서 다음과 같다.
행렬이 회전을 포함하면 실수 고유벡터가 존재하지 않을 수도 있다. 예를 들어 순수한 90도 회전은 어떤 실수 벡터의 방향도 그대로 유지하지 않으므로 실수 범위의 고유벡터를 갖지 않는다. 이 경우 NumPy는 복소수 고유값을 반환할 수 있다.
고유값 방정식에서 오른쪽 항을 왼쪽으로 옮기면
\[([\boldsymbol A]-\lambda[\boldsymbol I])[\boldsymbol v]=\boldsymbol 0\]이 된다. 0이 아닌 고유벡터가 존재하려면 행렬 $\boldsymbol A-\lambda\boldsymbol I$가 역행렬을 가지지 않아야 한다. 따라서
\[\det(\boldsymbol A-\lambda\boldsymbol I)=0\]을 만족해야 한다. 이 식을 특성방정식(characteristic equation)이라고 한다.
2×2 행렬
\[[\boldsymbol A]=\begin{bmatrix}a&b\\c&d\end{bmatrix}\]의 특성방정식은
\[\lambda^2-(a+d)\lambda+(ad-bc)=0\]이다. 여기서 $a+d$는 행렬의 trace이고, $ad-bc$는 determinant이다.
import numpy as np
A = np.array([
[3.0, 2.0],
[2.0, 1.0],
])
trace = np.trace(A)
determinant = np.linalg.det(A)
roots = np.roots([1.0, -trace, determinant])
print(roots)
NumPy에서는 np.linalg.eig()를 사용한다.
import numpy as np
A = np.array([
[3.0, 2.0],
[2.0, 1.0],
])
eigenvalues, eigenvectors = np.linalg.eig(A)
print("eigenvalues:")
print(eigenvalues)
print("eigenvectors:")
print(eigenvectors)
반환된 eigenvalues[i]에 대응하는 고유벡터는 eigenvectors[:, i]이다.
for index, value in enumerate(eigenvalues):
vector = eigenvectors[:, index]
print(f"lambda = {value}")
print(f"v = {vector}")
print("check:", A @ vector, value * vector)
수치 계산에서는 아주 작은 반올림 오차가 생길 수 있다. 두 결과가 같은지 확인할 때는 ==보다 np.allclose()를 사용하는 것이 좋다.
for index, value in enumerate(eigenvalues):
vector = eigenvectors[:, index]
assert np.allclose(A @ vector, value * vector)
고유값만 필요하다면 np.linalg.eigvals()를 사용할 수 있다.
eigenvalues_only = np.linalg.eigvals(A)
print(eigenvalues_only)
고유값과 고유벡터의 순서는 특별히 보장되지 않는다. 따라서 첫 번째 결과가 항상 가장 큰 고유값이라고 가정하면 안 된다.
변위구배텐서 또는 선형변환 행렬을 $\boldsymbol F$라고 하자. 변환 전 벡터와 변환 후 벡터의 관계는 다음과 같이 쓸 수 있다.
\[[\boldsymbol v]^{\mathrm{new}}=[\boldsymbol F][\boldsymbol v]^{\mathrm{old}}\]고유벡터 방향에서는 방향이 바뀌지 않고 고유값만큼 크기가 변한다.
import numpy as np
F = np.array([
[1.10, 0.05],
[0.05, 0.95],
])
eigenvalues, eigenvectors = np.linalg.eig(F)
for index, stretch in enumerate(eigenvalues):
direction = eigenvectors[:, index]
print(f"direction = {direction}")
print(f"stretch = {stretch}")
회전이 거의 없고 고유값이 변형을 나타내는 변환이라면, $\lambda-1$을 고유벡터 방향의 변형 정도로 해석할 수 있다. 이는 문제의 변환 정의와 변형률의 종류에 따라 달라지므로, 항상 고유값을 곧바로 변형률이라고 부르지는 않아야 한다.
행렬을 반복해서 곱하는 문제에서도 고유값이 중요한 역할을 한다.
\[[\boldsymbol x]_{n+1}=[\boldsymbol A][\boldsymbol x_n]\]예를 들어 두 집단의 인구 변화를 행렬로 표현할 수 있다.
import numpy as np
transition = np.array([
[0.80, 0.10],
[0.20, 0.90],
])
population = np.array([8.0, 2.0])
for year in range(5):
print(year, population)
population = transition @ population
반복 변환에서 크기가 빠르게 커지거나 작아지는 방향은 고유값의 절댓값과 관련이 있다. 충분히 반복한 뒤의 거동을 이해하려면 가장 큰 절댓값을 가진 고유값과 그에 대응하는 고유벡터를 살펴볼 수 있다.
np.linalg.eig()는 정방행렬을 대상으로 한다. 행과 열의 개수가 다르면 고유값 문제를 정의할 수 없으므로 오류가 발생한다.
회전처럼 실수 고유벡터가 존재하지 않는 변환에서는 복소수 고유값이나 고유벡터가 나올 수 있다. 복소수 결과를 임의로 실수부만 취하지 말고, 문제의 물리적 의미를 먼저 확인해야 한다.
계산 결과가 이론값과 아주 조금 다를 수 있다. 이런 경우 np.allclose()로 허용 오차를 고려하여 비교한다.
고유값과 고유벡터를 정의하는 식을 쓰시오.
다음 행렬을 x 방향 단위벡터에 곱했을 때의 결과를 구하고, x 방향이 고유벡터 방향인지 판단하시오.
\[A=\begin{bmatrix}2&0\\0&3\end{bmatrix}, \qquad v=\begin{bmatrix}1\\0\end{bmatrix}\]NumPy에서 행렬의 고유값과 고유벡터를 계산하는 함수를 쓰시오.
다음 코드에서 eigenvectors[:, index]가 의미하는 것을 설명하시오.
eigenvalues, eigenvectors = np.linalg.eig(A)
vector = eigenvectors[:, index]