5교시부터 행렬을 "공간을 옮기는 규칙"으로 보았습니다. 행렬을 곱하면 거의 모든 벡터가 방향을 틀고 길이도 바뀝니다. 그런데 행렬마다 방향이 바뀌지 않는 특별한 방향이 숨어 있는 경우가 많습니다. 그 방향의 벡터는 변환을 거쳐도 같은 직선 위에 남고, 길이만 몇 배가 됩니다.
이 특별한 방향을 고유벡터, 그때의 배율을 고유값이라고 부릅니다. 이름은 어렵게 들리지만 쓰임새는 아주 넓습니다. 검색 엔진이 웹 페이지의 중요도를 매기는 방법(PageRank), 다리나 건물이 흔들리는 고유한 진동 모양, 15교시에 볼 주성분 분석(PCA)이 모두 "어떤 행렬의 고유벡터를 찾는 일"입니다. 다음 교시에 볼 "같은 변환을 100번 반복하면 어떻게 되나"라는 질문의 답도 고유값이 쥐고 있습니다.
5교시 실습대에서 "고유벡터 점선"을 잠깐 보았습니다. 이번 교시에는 그 점선이 무엇인지 직접 찾아보고, 손으로 계산하는 법과 NumPy로 구하는 법을 차례로 익힙니다.
행렬 를 벡터 에 곱했더니, 결과가 원래 벡터를 몇 배 한 것과 똑같아졌다고 합시다.
이 식이 성립하는 (영벡터가 아닌 것)를 의 고유벡터(eigenvector) 라고 하고, 그때의 숫자 를 고유값(eigenvalue) 이라고 합니다. 뜻을 말로 풀면 이렇습니다.
로 옮겨도 는 원래 직선 위에 그대로 있고, 길이만 배가 된다.
| 책에 적힌 모양 | 읽는 법 | 뜻 | 파이썬에서는 |
|---|---|---|---|
| 람다 | 고유값. 고유벡터가 늘어나는 배율 | vals[0] | |
| A v는 람다 v | v는 A의 고유벡터, 배율은 람다 | np.allclose(A @ v, lam * v) | |
| 람다 일, 람다 이 | 첫째, 둘째 고유값 | vals[0], vals[1] | |
| 아이 | 항등행렬(6교시) | np.eye(2) | |
| 디터미넌트 A 빼기 람다 I는 영 | 특성방정식. 고유값을 찾는 식 | 11-3에서 |
"eigen"은 독일어로 "자기 자신의, 고유한"이라는 뜻입니다. 행렬마다 자기만의 방향을 갖고 있다는 느낌으로 기억하면 됩니다.
예를 들어 에 몇 가지 벡터를 넣어 봅시다.
은 로 가면서 방향이 틀어졌습니다. 그런데 은 같은 방향으로 3배가 되었고, 은 아예 그대로입니다. 그러니 이 행렬의 고유벡터는 방향(고유값 3)과 방향(고유값 1)입니다.
아래 실습대에서 직접 찾아보세요. 청록 화살표가 v, 진홍 화살표가 Av입니다. 처음에는 v = (1.5, 0.5)라서 "아직: v와 Av의 방향이 17° 어긋나 있습니다"라고 나옵니다. 청록 v의 끝을 끌어 진홍 Av와 한 직선에 겹치게 만들면 두 화살표가 굵어지고 "고유벡터입니다! Av = 3·v"처럼 배율까지 알려 줍니다.
아직: v와 Av의 방향이 17° 어긋나 있습니다
NumPy로 확인하기
실습대에서 값을 정할 때마다, 서버의 NumPy가 같은 숫자로 계산한 결과입니다.
>>> A = np.array([[2, 1], [1, 2]])
>>> v = np.array([1.5, 0.5])
>>> A @ v
array([3.5, 2.5])
>>> vals, vecs = np.linalg.eig(A)
>>> np.round(vals, 6)
array([3.+0.j, 1.+0.j])
>>> np.round(vecs, 6)
array([[ 0.707107+0.j, -0.707107+0.j],
[ 0.707107+0.j, 0.707107+0.j]])
>>> np.round(A @ vecs[:, 0], 6)
array([2.12132+0.j, 2.12132+0.j])
>>> np.round(vals[0] * vecs[:, 0], 6)
array([2.12132+0.j, 2.12132+0.j])'v를 한 바퀴 돌려 보기'를 누르면 v가 반지름 1.5인 원을 따라 돌고, 진홍 Av는 타원을 그립니다. 원 위의 점이 행렬을 거쳐 타원 위로 옮겨 간 것입니다. 돌아가는 동안 v와 Av가 한 직선에 겹치는 순간이 정확히 고유벡터 방향입니다. '대칭'에서는 타원의 가장 긴 지름과 가장 짧은 지름 방향이 곧 두 고유벡터입니다. 이 사실은 14교시 SVD에서 다시 만납니다.
'정답 (2×2 공식)'을 켜면 노랑 점선으로 고유벡터 방향이 그어지고, λ 값과 길이 1로 맞춘 고유벡터가 함께 나옵니다. 어떻게 이 정답을 계산하는지는 11-3에서 다룹니다.
한 가지 짚어 둘 점이 있습니다. v를 (1, 1)에서 (2, 2)나 (−1, −1)로 바꿔도 여전히 고유벡터입니다. 고유벡터는 "화살표 하나"가 아니라 원점을 지나는 직선 하나라고 생각하는 편이 정확합니다. 그 직선 위의 벡터는 영벡터만 빼고 모두 고유벡터입니다. 영벡터를 빼는 이유는, A0 = λ0이 어떤 λ로든 성립해 버려서 아무 정보도 주지 않기 때문입니다.
고유벡터 방향에서 행렬이 하는 일은 "배 하기" 하나뿐입니다. 그래서 의 값만 보면 그 방향에서 무슨 일이 일어나는지 알 수 있습니다.
| 의 값 | 그 방향에서 일어나는 일 | 예 |
|---|---|---|
| 같은 방향으로 늘어남 | : 3배 | |
| 그대로 | 변하지 않는 축 | |
| 같은 방향으로 줄어듦 | : 절반 | |
| 원점으로 납작하게 눌림 | 9교시의 영공간 방향 | |
| 뒤집힌 뒤 배 | : 거울에 비친 듯 반대로 |
인 줄이 특히 중요합니다. 이니, 고유값 0의 고유벡터는 9교시에 본 영공간의 방향입니다. 그래서 고유값에 0이 있으면 이고, 역행렬이 없습니다(7교시).
실습대의 상태 문구는 v와 Av가 같은 쪽을 보든 반대쪽을 보든 한 직선 위에만 있으면 "고유벡터입니다!"라고 알려 줍니다. 고유값이 음수인 방향도 고유벡터이기 때문입니다.
실습대에서 v를 돌려 보며 찾는 방법은 직관을 주지만, 정확한 값을 얻으려면 계산이 필요합니다. 핵심은 앞 교시들에서 배운 내용을 한 줄로 엮는 것입니다.
1단계: 식을 옮깁니다. 에서 오른쪽을 왼쪽으로 넘깁니다. 를 로 쓰면 로 묶을 수 있습니다.
2단계: 그림으로 읽습니다. 영벡터가 아닌 가 행렬 를 거쳐 원점으로 사라졌습니다. 이것은 가 평면을 납작하게 누른다는 뜻입니다(9교시: 영공간이 원점뿐이 아님). 납작하게 누르는 행렬은 행렬식이 0입니다(7교시).
이 식을 특성방정식(characteristic equation) 이라고 합니다. 하나만 모르는 식이니 풀 수 있습니다.
3단계: 2×2에서 계산합니다. 이면 이고, 7교시의 공식 를 쓰면 다음과 같습니다.
는 대각선 성분의 합으로 대각합(trace) 이라고 부르고, 는 행렬식입니다. 그래서 2×2 행렬의 고유값 두 개는 더하면 대각합, 곱하면 행렬식이 됩니다. 검산할 때 아주 편리합니다.
예: . 대각합은 4, 행렬식은 입니다.
4단계: 고유값마다 고유벡터를 찾습니다. 을 넣으면 , 즉 연립방정식(8교시)이 됩니다.
두 줄이 같은 식이라 해가 무수히 많습니다(8교시의 "해 무수히"). 이것이 정상입니다. 고유벡터는 직선 하나이기 때문입니다. 대표로 을 고릅니다. 같은 방법으로 이면 이 나와 입니다. 11-1에서 직접 곱해 본 결과와 정확히 맞습니다.
검산: (대각합), (행렬식). 맞습니다.
삼각행렬은 더 쉽습니다. 위삼각 의 특성방정식은 이라, 고유값이 대각선 성분 그대로 3과 2입니다.
실습대의 네 가지 행렬은 일부러 성격이 다른 것으로 골랐습니다. 칩을 하나씩 바꿔 가며 아래 표와 맞춰 보세요.
| 칩 | 행렬 | 특성방정식 | 고유값 | 고유벡터 |
|---|---|---|---|---|
| 대칭 | 3, 1 | , : 서로 수직 | ||
| 위삼각 | 3, 2 | , : 수직 아님 | ||
| 회전 | 실수 해 없음 | 없음 | ||
| 밀기 | 1 (두 번) | 방향 하나뿐 |
회전. 90° 회전은 모든 벡터를 90° 돌리니, 방향이 그대로인 벡터가 있을 리 없습니다. 실습대에서 v를 아무리 돌려도 "90° 어긋나 있습니다"에서 바뀌지 않고, Av의 자취는 타원이 아니라 원입니다. 특성방정식 은 실수 해가 없고, 허수 를 써야 로 풀립니다. 이렇게 고유값이 복소수로 나오면 그 행렬에 회전 성분이 있다고 읽으면 됩니다.
밀기. 가로로 미는 변환은 x축 위의 점을 움직이지 않습니다. 그래서 방향은 고유값 1의 고유벡터입니다. 하지만 다른 방향은 모두 오른쪽으로 기울어지므로, 고유벡터 직선이 하나뿐입니다. 고유값은 1이 "두 번" 나오는데 직선은 하나뿐인 이런 행렬은 고유벡터만으로 평면 전체를 설명할 수 없습니다. 다음 교시의 대각화가 통하지 않는 대표적인 경우입니다.
대칭. 와 (5교시의 전치)가 같은 행렬을 대칭행렬이라고 합니다. 대칭행렬은 늘 실수 고유값을 갖고, 서로 다른 고유값의 고유벡터는 항상 서로 수직입니다. 으로 확인할 수 있습니다. 데이터에서 나오는 공분산 행렬이 대칭이라서, 15교시의 PCA가 이 성질을 그대로 이용합니다.
실습대 아래 "NumPy로 확인하기" 칸에는 처음에 이런 코드가 나옵니다. 한 줄씩 직접 쳐 보며 읽어 보겠습니다.
>>> import numpy as np
>>> A = np.array([[2, 1], [1, 2]])
>>> v = np.array([1.5, 0.5])
>>> A @ v
array([3.5, 2.5])A @ v는 5교시에서 본 행렬-벡터 곱입니다. (1.5, 0.5)는 (3.5, 2.5)로 갔습니다. 3.5 / 1.5와 2.5 / 0.5가 다르니 "같은 배율로 늘어난 것"이 아니고, 따라서 고유벡터가 아닙니다.
고유값과 고유벡터는 np.linalg.eig 한 번으로 함께 나옵니다. 결과가 두 개라서 vals, vecs = …처럼 이름 두 개로 나눠 받습니다(파이썬의 튜플 풀기). 실습대의 NumPy 칸은 그다음 줄들을 보기 좋게 np.round(…, 6)으로 감싸, 소수 여섯째 자리까지 반올림해서 보여 줍니다.
>>> vals, vecs = np.linalg.eig(A)
>>> np.round(vals, 6)
array([3.+0.j, 1.+0.j])
>>> np.round(vecs, 6)
array([[ 0.707107+0.j, -0.707107+0.j],
[ 0.707107+0.j, 0.707107+0.j]])np.round(배열, 6)은 배열의 모든 원소를 소수 여섯째 자리까지 반올림한 새 배열을 줍니다. 원래 vals, vecs는 바뀌지 않습니다. 반올림하지 않은 원래 값은 이렇습니다.
>>> vals
array([3.+0.j, 1.+0.j])
>>> vecs
array([[ 0.70710678+0.j, -0.70710678+0.j],
[ 0.70710678+0.j, 0.70710678+0.j]])여기서 처음 보는 것이 세 가지 있습니다.
① +0.j: 복소수 표시. 파이썬은 허수 단위를 j로 씁니다. 3.+0.j는 "실수 부분 3, 허수 부분 0", 즉 그냥 3입니다. NumPy 2.5의 eig는 행렬이 대칭이 아니라고 미리 가정하고 계산하기 때문에, 회전 행렬처럼 복소수 고유값이 나올 수도 있다는 전제로 답을 늘 복소수 배열로 돌려줍니다. 실수 부분만 원하면 .real을 붙입니다.
>>> vals.dtype
dtype('complex128')
>>> vals.real
array([3., 1.])② vecs의 열이 고유벡터입니다. vecs는 2×2 행렬이고, 0번 열이 vals[0]의 고유벡터, 1번 열이 vals[1]의 고유벡터입니다. 4교시에서 본 M[:, 0](모든 행의 0번 칸 = 0번 열)으로 꺼냅니다. vecs[0]은 0번 행이라 전혀 다른 것입니다.
>>> vecs[:, 0]
array([0.70710678+0.j, 0.70710678+0.j])
>>> vecs[0]
array([ 0.70710678+0.j, -0.70710678+0.j])③ 길이가 1로 맞춰져 있습니다. 손으로 구한 (1, 1) 대신 (0.7071…, 0.7071…)이 나온 이유는, NumPy가 고유벡터를 단위벡터(3교시)로 정규화해 주기 때문입니다. 0.70710678은 1/√2입니다. 같은 직선 위의 벡터는 모두 고유벡터이니, 첫 성분으로 나누면 손으로 구한 모양이 돌아옵니다.
>>> 1 / np.sqrt(2)
np.float64(0.7071067811865475)
>>> vecs[:, 0] / vecs[0, 0]
array([1.+0.j, 1.+0.j])
>>> np.linalg.norm(vecs[:, 0])
np.float64(0.9999999999999999)
>>> np.isclose(np.linalg.norm(vecs[:, 0]), 1)
np.True_길이가 정확히 1이 아니라 0.9999999999999999로 나왔습니다. 3교시에서 본 부동소수점 오차입니다. 실수 계산 결과는 == 대신 np.isclose로 비교합니다.
1번 열이 (1, −1)이 아니라 (−0.707, 0.707)로, 손으로 구한 것과 부호가 반대인 점도 보세요. 같은 직선 위이니 둘 다 맞는 답입니다. 고유벡터의 부호는 정해져 있지 않습니다. 프로그램이나 버전에 따라 부호가 다를 수 있으니, 부호까지 같기를 기대하는 코드는 쓰지 않는 것이 좋습니다.
NumPy 칸의 마지막 두 줄은 정의를 그대로 검사합니다. 왼쪽 A @ vecs[:, 0]은 Av, 오른쪽 vals[0] * vecs[:, 0]은 λv입니다. 칸에서는 둘 다 np.round(…, 6)으로 감싸 보여 줍니다.
>>> np.round(A @ vecs[:, 0], 6)
array([2.12132+0.j, 2.12132+0.j])
>>> np.round(vals[0] * vecs[:, 0], 6)
array([2.12132+0.j, 2.12132+0.j])
>>> A @ vecs[:, 0]
array([2.12132034+0.j, 2.12132034+0.j])
>>> vals[0] * vecs[:, 0]
array([2.12132034+0.j, 2.12132034+0.j])
>>> np.allclose(A @ vecs[:, 0], vals[0] * vecs[:, 0])
True반올림한 2.12132(끝자리 0이 생략된 2.121320)와 원래 값 2.12132034는 자릿수만 다를 뿐 같은 값입니다. 두 쪽이 같은지 눈으로 비교하는 대신, 코드에서는 마지막 줄처럼 np.allclose로 확인합니다.
고유벡터 두 개를 한 번에 확인하는 방법도 있습니다. vecs * vals는 브로드캐스팅(2교시)으로 0번 열에 vals[0], 1번 열에 vals[1]을 곱합니다. 그러니 A @ vecs와 같아야 합니다.
>>> np.allclose(A @ vecs, vecs * vals)
Truenp.linalg.eigh행렬이 대칭이라는 것을 알면 eigh를 씁니다. 대칭행렬 전용이라 더 빠르고 안정적이며, 답이 실수 배열로, 고유값이 작은 것부터 큰 것 순서(오름차순) 로 나옵니다.
>>> vals2, vecs2 = np.linalg.eigh(A)
>>> vals2
array([1., 3.])
>>> vecs2
array([[-0.70710678, 0.70710678],
[ 0.70710678, 0.70710678]])
>>> vecs2[:, 0] @ vecs2[:, 1]
np.float64(0.0)eig는 순서를 약속하지 않았지만(이번에는 3, 1 순서), eigh는 1, 3 순서입니다. 마지막 줄은 두 고유벡터의 내적이 0, 즉 11-4에서 말한 대로 서로 수직이라는 확인입니다. 가장 큰 고유값은 마지막(vals2[-1])에 있다는 점을 기억해 두세요. 15교시 PCA에서 그대로 씁니다.
>>> R = np.array([[0, -1], [1, 0]])
>>> np.linalg.eigvals(R)
array([0.+1.j, 0.-1.j])고유값만 필요하면 np.linalg.eigvals를 씁니다. 회전 행렬은 11-4에서 말한 대로 1.j와 -1.j, 곧 ±i가 나옵니다. 이번에는 허수 부분이 0이 아니니 .real로 버리면 안 됩니다.
>>> S = np.array([[1, 1], [0, 1]])
>>> vals, vecs = np.linalg.eig(S)
>>> vals
array([1.+0.j, 1.+0.j])
>>> vecs
array([[ 1.00000000e+00+0.j, -1.00000000e+00+0.j],
[ 0.00000000e+00+0.j, 2.22044605e-16+0.j]])밀기 행렬에서 NumPy는 열을 두 개 돌려주지만, 둘째 열은 (−1, 0.000…0002)로 사실상 첫째 열과 같은 직선입니다. 2.22044605e-16은 2.22 × 10⁻¹⁶, 곧 부동소수점 오차 수준의 0입니다. eig가 열을 두 개 줬다고 해서 고유벡터 방향이 두 개라는 뜻은 아닙니다. 두 열이 일차독립인지는 4교시의 계수로 확인할 수 있습니다.
>>> np.linalg.matrix_rank(vecs)
np.int64(1)계수가 1이니 고유벡터가 한 직선뿐입니다. 실습대에서 '밀기'를 고르고 '정답 (2×2 공식)'을 켜도 노랑 점선이 하나만 그어집니다.
위삼각 행렬로는 11-3의 "더하면 대각합, 곱하면 행렬식"을 확인해 봅니다. np.trace는 대각선 성분의 합입니다.
>>> B = np.array([[3, 1], [0, 2]])
>>> vals = np.linalg.eigvals(B)
>>> vals
array([3.+0.j, 2.+0.j])
>>> np.trace(B)
np.int64(5)
>>> np.linalg.det(B)
np.float64(6.0)
>>> vals.sum()
np.complex128(5+0j)
>>> vals.prod()
np.complex128(6+0j)np.complex128(5+0j)도 그냥 5입니다. 합이 대각합 5, 곱이 행렬식 6과 맞습니다.
고유벡터를 행으로 꺼내기. vecs[0]은 고유벡터가 아닙니다. 고유벡터는 열이니 vecs[:, 0]입니다. 위에서 본 것처럼 대칭행렬에서는 vecs[0]이 우연히 그럴듯한 숫자로 나와서 실수를 알아채기 어렵습니다. 의심스러우면 np.allclose(A @ v, lam * v)로 확인하세요.
대칭이 아닌 행렬에 eigh 쓰기. eigh는 행렬이 대칭이라고 믿고 아래 삼각 부분만 읽습니다. 대칭이 아닌 행렬을 넣어도 오류 없이 틀린 답을 돌려줍니다. 아래 행렬의 진짜 고유값은 5와 2인데, eigh는 엉뚱한 값을 냅니다.
>>> C = np.array([[4, 1], [2, 3]])
>>> np.linalg.eigvals(C).real
array([5., 2.])
>>> np.linalg.eigh(C).eigenvalues
array([1.43844719, 5.56155281])eig와 eigh의 결과는 이름 붙은 튜플이라 .eigenvalues, .eigenvectors로 꺼낼 수도 있습니다. 대칭인지 모르면 np.allclose(C, C.T)로 먼저 확인하고, 아니면 eig를 씁니다.
정사각이 아닌 행렬. 고유값은 "같은 공간 안에서 방향이 유지되는가"를 묻는 것이라, 정사각행렬에만 있습니다.
>>> np.linalg.eig(np.array([[1, 2, 3], [4, 5, 6]]))
Traceback (most recent call last):
...
numpy.linalg.LinAlgError: Last 2 dimensions of the array must be square정사각이 아닌 행렬에서 비슷한 일을 하는 도구가 14교시의 SVD입니다.
@로 인지 확인하세요.1번: 대각합은 , 행렬식은 이니 , 곧 이라 , 입니다. 검산: , .
2번: 이면 의 첫 줄이 이라 입니다. 이면 첫 줄이 이라 입니다.
>>> C @ np.array([1, 1])
array([5, 5])
>>> C @ np.array([1, -2])
array([ 2, -4])
>>> D = np.array([[2, 0], [0, 0.5]])
>>> vals, vecs = np.linalg.eig(D)
>>> vals.real
array([2. , 0.5])
>>> vecs.real
array([[1., 0.],
[0., 1.]])(5, 5) = 5·(1, 1), (2, −4) = 2·(1, −2)이니 둘 다 고유벡터입니다.
3번: 대각행렬은 x축 방향을 2배, y축 방향을 0.5배 하는 "축 따라 늘이기·줄이기"입니다. 그래서 고유값은 대각선 성분 2와 0.5, 고유벡터는 표준기저 î = (1, 0)과 ĵ = (0, 1)입니다. 다음 교시에는 거꾸로, 어떤 행렬이든 고유벡터 방향에서 보면 이런 대각행렬처럼 보인다는 사실을 이용합니다.
이번 교시 정리
고유벡터는 행렬을 거쳐도 같은 직선 위에 남는 방향, 고유값 λ는 그 방향의 배율이다: Av = λv.
고유벡터는 원점을 지나는 직선 하나다. 그 위의 벡터(영벡터 제외)는 모두 고유벡터이고, 부호도 정해져 있지 않다.
고유값은 특성방정식 det(A − λI) = 0으로 찾는다. 2×2에서는 λ² − (대각합)λ + det = 0.
회전은 실수 고유벡터가 없고(복소수 고유값), 밀기는 하나뿐이며, 대칭행렬은 서로 수직인 고유벡터를 갖는다.
vals, vecs = np.linalg.eig(A): 고유벡터는 vecs[:, i] (열!). NumPy 2.5의 eig는 늘 복소수로 주니 필요하면 .real.
대칭행렬에는 np.linalg.eigh: 실수, 오름차순. 대칭이 아닌 행렬에 쓰면 틀린 답이 조용히 나온다.
다음 교시에는 고유벡터를 10교시의 기저로 삼아, 행렬을 "축 따라 늘이기"로 바꾸는 대각화를 배웁니다. 그러면 행렬을 100번 곱하는 일이 숫자 몇 개를 100제곱하는 일로 바뀌고, 피보나치 수열에서 황금비가 튀어나오는 이유도 보입니다.
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.