본문 바로가기
선형대수

손으로 푸는 선형대수 14장 — 대칭행렬의 스펙트럼 분해

by 카이스토 2026. 9. 24.

14 손으로 푸는 선형대수 · 제4부 고유값과 특이값

대칭행렬의 스펙트럼 분해

13장에서 본 나쁜 경우 — 고유값이 복소수가 되는 회전행렬, 고유벡터가 모자라는 결손행렬 — 이 대칭행렬에서는 하나도 일어나지 않는다. 대칭이라는 조건 하나가 실수성·직교성·대각화를 전부 보장한다. 이 장은 그 정리를 직접 증명하고, 우리 G = ATA에서 고유값 16·9·1과 정수 고유벡터를 특성다항식부터 끝까지 손으로 뽑는다.

  1. 직관 — 대칭이면 나쁜 일이 일어나지 않는다
  2. 수식 읽는 법 — 스펙트럼 정리와 두 줄짜리 증명
  3. 손으로 풀기 — λ³ − 26λ² + 169λ − 144 = 0부터 Q까지
  4. 코드 — eigh 대조와 야코비 회전법 직접 구현
  5. 시각화 — QΛQT는 회전·늘이기·되돌리기

약어 및 기호 정의

S = ST 대칭행렬 symmetric matrix
전치해도 자기 자신인 정사각행렬. 우리 G = ATA가 그렇다 — 1장에서 만들 때부터 그랬다.
스펙트럼 분해 spectral decomposition
S = QΛQT. 대칭행렬을 직교행렬과 대각행렬로 쪼개는 것. 이 장의 목표.
Λ Lambda
고유값을 대각에 늘어놓은 대각행렬. 이 장에서는 diag(16, 9, 1).
Q 직교행렬 orthogonal matrix
열이 정규직교인 정사각행렬. QTQ = I이므로 Q−1 = QT다 — 9장의 핵심.
특성다항식 characteristic polynomial
p(λ) = det(S − λI). 근이 곧 고유값이다. 13장에서 2×2로 연습했고 여기서 3×3을 한다.
주소행렬식 principal minor
같은 번호의 행과 열을 함께 남겨 만든 부분행렬식. 특성다항식의 계수가 이것들의 합이다.
정규직교 orthonormal
서로 직교하고(내적 0) 각자 길이가 1인 벡터들. 직교만으로는 부족하고 길이까지 1이어야 한다.
야코비 회전법 Jacobi rotation
비대각 성분을 한 쌍씩 0으로 만드는 회전을 반복해 대칭행렬을 대각화하는 고전 알고리즘.
STEP 01

직관 — 대칭이면 나쁜 일이 일어나지 않는다

13장은 고유값 이야기를 기분 좋게 끝내지 않았다. 두 가지 반례가 있었다.

1
고유값이 복소수가 된다. 90° 회전행렬 R = [[0,−1],[1,0]]은 특성방정식이 λ² + 1 = 0이라 실수 해가 없다. 당연하다 — 평면을 90° 돌리면 방향이 그대로 남는 실벡터가 하나도 없다.
2
고유벡터가 모자란다. 전단형 행렬 N = [[2,1],[0,2]]는 고유값이 2 하나뿐인데(중복도 2) 고유벡터는 (1,0) 방향 하나밖에 없다. 2×2를 채울 기저가 안 나오므로 대각화가 불가능하다.

그래서 13장의 결론은 "모든 행렬이 대각화되는 것은 아니다"였다. 그런데 13장은 동시에 예고를 하나 남겼다. 대칭행렬은 항상 대각화되고, 게다가 고유벡터를 서로 직교하게 잡을 수 있다. 이 장이 그 예고를 증명하고 예제로 확인한다.

왜 대칭이 그렇게 강한지 감을 먼저 잡자. 회전행렬이 말썽을 부린 이유는 비대칭이었기 때문이다. R의 (1,2) 성분은 −1인데 (2,1) 성분은 +1이다. 이 어긋남이 곧 "돌린다"는 뜻이고, 돌리는 변환에는 불변 방향이 없다. 대칭행렬에는 정의상 그 어긋남이 없다. 대칭행렬은 돌리지 않는다. 오직 축을 따라 늘이거나 줄일 뿐이다.

ch14-d1
도해 1. 13장의 두 반례(왼쪽·가운데)와 이 장의 주인공(오른쪽). 회전은 불변 방향이 없어 고유값이 복소수가 되고, 전단은 불변 방향이 하나뿐이라 기저를 못 채운다. 대칭행렬은 직교하는 두 축을 골라 각각 늘이기만 한다 — 두 사고가 모두 불가능하다. 오른쪽의 S = [[2,3],[3,10]]은 G에서 1·3행과 1·3열만 남긴 축소판이고 고유값은 정확히 11과 1이다.

오른쪽 그림의 S는 G의 축소판이라 손으로 바로 확인된다. tr S = 12, det S = 20 − 9 = 11이므로 λ² − 12λ + 11 = 0, 근은 λ = 11, 1. 고유벡터는 (1, 3)과 (3, −1)이고 내적이 3 − 3 = 0, 정확히 직교한다. 길이도 둘 다 √10으로 같다. 이것이 우연이 아니라 정리라는 것이 STEP 02의 내용이다.

STEP 02

수식 읽는 법 — 스펙트럼 정리

정리부터 적고, 세 조항을 하나씩 증명한다.

스펙트럼 정리 spectral theorem

S ∈ ℝn×n가 실대칭이면

  ① 고유값 λ₁, …, λₙ 이 전부 실수다.
  ② 고유벡터를 정규직교로 잡을 수 있다 — qi·qj = 0 (i≠j), ‖qi‖ = 1.
  ③ 따라서 S = QΛQT 로 반드시 분해된다. (Q = [q₁ … qₙ], Λ = diag(λ₁ … λₙ))
(14.1) "반드시"가 핵심이다. 13장의 일반 행렬에는 이런 보장이 없었다.

① 고유값이 실수인 이유

실수라는 것을 보이려면 일단 복소수일 수도 있다고 가정하고 시작해야 한다. Sv = λv라 하고, λ와 v가 복소수라고 두자. 양변 왼쪽에 v̄T(켤레 전치)를 곱한다.

1
v̄TSv = λ v̄Tv = λ ‖v‖². 여기서 ‖v‖² = Σ |vi|²는 반드시 양수인 실수다(고유벡터는 영벡터가 아니므로).
2
이제 좌변 v̄TSv가 실수임을 보인다. 이것은 1×1 스칼라이므로 켤레를 취해도, 전치를 해도 자기 자신과 같은 대상이다. 켤레 전치를 취하면 (v̄TSv)* = v̄TSTv인데 S가 실대칭이라 ST = S, 따라서 (v̄TSv)* = v̄TSv.
3
켤레가 자기 자신인 복소수는 실수다. 그러므로 λ = (v̄TSv) / ‖v‖² 는 실수 나누기 양의 실수, 곧 실수다. ∎

증명이 ST = S를 딱 한 번 쓴다는 점을 보자. 회전행렬에서는 RT = −R이라 2단계에서 부호가 뒤집히고 λ는 순허수 ±i가 된다. 정리가 성립하는 이유와 반례가 생기는 이유가 같은 줄에 들어 있다.

② 고유벡터가 직교하는 이유

이쪽이 더 짧다. 서로 다른 고유값 λ₁ ≠ λ₂에 대한 고유벡터 v₁, v₂를 잡고, (Sv₁)·v₂를 두 가지로 계산한다.

(왼쪽에 붙여 계산)   (Sv₁)·v₂ = (λ₁v₁)·v₂ = λ₁ (v₁·v₂)
(오른쪽으로 넘겨 계산) (Sv₁)·v₂ = v₁TSTv₂ = v₁TSv₂ = v₁·(Sv₂) = λ₂ (v₁·v₂)
가운데 등호 ST = S 한 번이 전부다. (14.2)

같은 값을 두 방식으로 쓴 것이니 둘은 같아야 한다.

λ₁ (v₁·v₂) = λ₂ (v₁·v₂) ⟹ (λ₁ − λ₂)(v₁·v₂) = 0 λ₁ ≠ λ₂이므로 앞 괄호는 0이 아니다. 따라서 v₁·v₂ = 0. (14.3)

이게 전부다. 서로 다른 고유값에 붙은 고유벡터는 자동으로 직교한다. 고유값이 겹칠 때는 그 고유공간 안에서 그람-슈미트(11장)를 돌려 직교 기저를 만들면 되므로, 어느 경우든 ②가 성립한다. 우리 G는 16·9·1이 전부 다르므로 겹침 걱정은 없다.

③ S = QΛQT — 그리고 랭크1 분해

②에서 정규직교 고유벡터 n개를 얻었으니 이것을 열로 세워 Q를 만든다. SQ = QΛ는 열마다 Sqi = λiqi를 적은 것에 불과하다. 여기서 Q가 직교행렬이라 Q−1 = QT, 즉 역행렬을 구할 필요가 없다(9장). 오른쪽에 QT를 곱하면 바로 나온다.

S = QΛQT          QTQ = QQT = I
(14.4)

13장의 일반 대각화 A = PΛP−1와 비교하면 달라진 것은 P−1이 QT로 바뀐 것뿐인데, 이 차이가 실무에서 전부다. P는 열이 기울어져 조건수가 얼마든지 커지지만, Q는 조건수가 항상 1이고 역행렬이 전치다.

그리고 식 (14.4)를 2장의 네 번째 읽기로 풀어 쓰면 이 장에서 가장 쓸모 있는 형태가 나온다. 2장에서 행렬곱을 "랭크1 행렬의 합"으로 읽는 법을 배웠다. QΛ의 열은 λiqi이고 QT의 행은 qiT이므로,

S = nΣi=1 λi qiqiT 대칭행렬 = 정규직교 방향들의 랭크1 조각을 고유값만큼 실어 더한 것 (14.5)

1장 코드에서 g_rank1 = Σ outer(ai, ai)로 G를 만들어 본 적이 있다. 그때는 행 네 개의 바깥곱이었고 계수가 전부 1이었다. 지금은 고유방향 세 개의 바깥곱이고 계수가 16·9·1이다. 같은 G를 서로 다른 랭크1 조각으로 쓴 것인데, 후자에서는 조각들이 서로 직교하고 계수가 크기순으로 정렬된다. 이 정렬이 16장 SVD와 18장 저랭크 근사의 전부다.

STEP 03

손으로 풀기 — G의 스펙트럼 분해

(가) 특성다항식을 전개한다

주인공은 1장에서 만든 G = ATA다.

            ⎡  2   1   3 ⎤                     ⎡ 2−λ   1     3   ⎤
 G = AᵀA =  ⎢  1  14   3 ⎥      G − λI =       ⎢  1   14−λ   3   ⎥
            ⎣  3   3  10 ⎦                     ⎣  3    3    10−λ ⎦
(14.6)

첫 행을 따라 여인수 전개한다(6장). 부호는 + − +.

det(G − λI)를 끝까지 전개

첫째 항. 안쪽 2×2부터.

(14−λ)(10−λ) − 3·3 = 140 − 24λ + λ² − 9 = λ² − 24λ + 131

(2−λ)(λ² − 24λ + 131) = 2λ² − 48λ + 262 − λ³ + 24λ² − 131λ

= −λ³ + 26λ² − 179λ + 262

둘째 항. 부호가 −다.

− 1 · [ 1·(10−λ) − 3·3 ] = −[10 − λ − 9] = −[1 − λ] = λ − 1

셋째 항. 부호가 +다.

+ 3 · [ 1·3 − (14−λ)·3 ] = 3[3 − 42 + 3λ] = 3[3λ − 39] = 9λ − 117

합친다. λ 차수별로 모은다.

λ³: −λ³  ·  λ²: +26λ²  ·  λ: −179λ + λ + 9λ = −169λ  ·  상수: 262 − 1 − 117 = 144

det(G − λI) = −λ³ + 26λ² − 169λ + 144

양변에 −1을 곱해 최고차 계수를 1로 만든다.

λ³ − 26λ² + 169λ − 144 = 0

계수를 따로 검산하자. 3×3 특성다항식의 계수에는 이름이 있다.

λ³ − (tr G)λ² + (주소행렬식 합)λ − (det G) = 0 (14.7)

계수 세 개를 독립적으로 확인

1차 계수 — tr G. 대각합이다. 2 + 14 + 10 = 26 ✓

2차 계수 — 2×2 주소행렬식의 합. 같은 번호의 행·열을 함께 지운다.

1행1열 지움: det[[14,3],[3,10]] = 140 − 9 = 131

2행2열 지움: det[[2,3],[3,10]] = 20 − 9 = 11

3행3열 지움: det[[2,1],[1,14]] = 28 − 1 = 27

합 = 131 + 11 + 27 = 169 ✓ (169 = 13²이다)

상수항 — det G. 6장에서 구한 값, = 144 = 12² ✓

세 계수 26, 169, 144가 전개 결과와 정확히 일치한다.

여기서 미리 역방향 검산도 해 두자. 근이 16, 9, 1이라면 근과 계수의 관계가 성립해야 한다.

근의 합     16 + 9 + 1                 = 26   ✓  = tr G
쌍곱의 합   16·9 + 16·1 + 9·1 = 144+16+9 = 169  ✓  = 주소행렬식 합
근의 곱     16 · 9 · 1                 = 144  ✓  = det G

tr G = 26은 1장에서 ‖A‖F² = 26으로도 나왔다. 그것이 고유값의 합이었다.

(나) 삼차방정식을 손으로 푼다

삼차방정식은 근의 공식이 아니라 유리근 정리rational root theorem로 한 근을 찾고 차수를 낮추는 것이 정석이다. 최고차 계수가 1, 상수항이 −144이므로 유리근이 있다면 144의 약수여야 한다.

144 의 약수 : 1, 2, 3, 4, 6, 8, 9, 12, 16, 18, 24, 36, 48, 72, 144

가장 작은 것부터 넣어 본다. p(λ) = λ³ − 26λ² + 169λ − 144.

유리근 찾기 → 조립제법

p(1) = 1 − 26 + 169 − 144 = 0  → 첫 시도에 걸렸다. λ = 1은 근이다.

따라서 (λ − 1)로 나누어떨어진다. 조립제법으로 몫을 구한다.

  1 |   1   −26    169   −144
    |        1    −25    144
    -----------------------------
        1   −25    144  |    0   ← 나머지 0, 확인

몫은 λ² − 25λ + 144. 이제 이차방정식이다.

인수분해를 시도한다. 곱이 144, 합이 25인 두 수를 찾는다. 16 × 9 = 144, 16 + 9 = 25. 바로 나온다.

λ² − 25λ + 144 = (λ − 16)(λ − 9) = 0

판별식으로도 확인: 25² − 4·144 = 625 − 576 = 49 = 7². 완전제곱이므로 λ = (25 ± 7)/2 = 16, 9 ✓

고유값 λ₁ = 16, λ₂ = 9, λ₃ = 1. 계산기 없이 끝났다.

셋 다 양의 실수다. 실수인 것은 스펙트럼 정리 ①이 보장했다. 양수인 것은 아직 보장받은 적이 없는데, 이것이 15장 양정치의 주제다. 미리 말하면 G = ATA 꼴이고 A의 열이 독립이면 고유값이 반드시 양수가 된다.

(다) 각 고유값의 고유벡터를 구한다

(G − λI)v = 0을 푼다. λ가 고유값이므로 G − λI는 반드시 특이행렬이고, 영공간이 1차원일 것이다(3·5장).

λ₁ = 16

           ⎡ −14   1    3 ⎤
 G − 16I =  ⎢   1  −2    3 ⎥
           ⎣   3   3   −6 ⎦

3행이 3으로 나누어떨어진다. r₃′ : x₁ + x₂ − 2x₃ = 0.

2행 그대로: r₂ : x₁ − 2x₂ + 3x₃ = 0.

r₂ − r₃′: (−2 − 1)x₂ + (3 + 2)x₃ = −3x₂ + 5x₃ = 0  → x₂ = 53x₃

분수를 없애려고 x₃ = 3으로 잡는다. 그러면 x₂ = 5.

r₃′에 넣으면 x₁ = 2x₃ − x₂ = 6 − 5 = 1.

v₁ = (1, 5, 3)

검산 — 쓰지 않은 1행에 넣어 본다: −14(1) + 1(5) + 3(3) = −14 + 5 + 9 = 0 ✓

원식으로도: Gv₁ = (2+5+9, 1+70+9, 3+15+30) = (16, 80, 48) = 16(1,5,3) ✓

λ₂ = 9

          ⎡ −7   1   3 ⎤
 G − 9I =  ⎢  1   5   3 ⎥
          ⎣  3   3   1 ⎦

2행과 3행을 쓴다. r₂ : x₁ + 5x₂ + 3x₃ = 0, r₃ : 3x₁ + 3x₂ + x₃ = 0.

r₃에서 x₃ = −3x₁ − 3x₂. 이것을 r₂에 대입한다.

x₁ + 5x₂ + 3(−3x₁ − 3x₂) = x₁ + 5x₂ − 9x₁ − 9x₂ = −8x₁ − 4x₂ = 0

→ x₂ = −2x₁. x₁ = 1로 잡으면 x₂ = −2, x₃ = −3(1) − 3(−2) = −3 + 6 = 3.

v₂ = (1, −2, 3)

검산 — 1행: −7(1) + 1(−2) + 3(3) = −7 − 2 + 9 = 0 ✓

원식: Gv₂ = (2−2+9, 1−28+9, 3−6+30) = (9, −18, 27) = 9(1,−2,3) ✓

λ₃ = 1

         ⎡ 1   1   3 ⎤
 G − I =  ⎢ 1  13   3 ⎥
         ⎣ 3   3   9 ⎦

3행이 1행의 3배다 — 한눈에 특이행렬임이 보인다. 1·2행만 쓴다.

r₂ − r₁: (13 − 1)x₂ = 12x₂ = 0  → x₂ = 0

r₁에 넣으면 x₁ + 3x₃ = 0, 즉 x₁ = −3x₃.

x₃ = −1로 잡으면 x₁ = 3.

v₃ = (3, 0, −1)

검산 — 원식: Gv₃ = (6+0−3, 3+0−3, 9+0−10) = (3, 0, −1) = 1·(3,0,−1) ✓

세 고유벡터가 전부 정수로 떨어졌다. 보통 교과서 예제라면 여기서 (1, 2.317, −0.884) 같은 것이 나와 손계산이 끊긴다. 1장에서 "이 A를 왜 골랐는가"를 이야기한 이유가 이 지점이다.

(라) 직교성 확인과 Q 만들기

스펙트럼 정리 ②를 내적으로 직접 본다.

v₁·v₂ = (1)(1)  + (5)(−2) + (3)(3)  = 1 − 10 + 9 = 0  ✓
v₁·v₃ = (1)(3)  + (5)(0)  + (3)(−1) = 3 +  0 − 3 = 0  ✓
v₂·v₃ = (1)(3)  + (−2)(0) + (3)(−1) = 3 +  0 − 3 = 0  ✓

세 쌍 모두 0이다. 우리가 한 일은 각 고유값마다 따로따로 영공간을 푼 것뿐이고 직교성은 요구한 적이 없다. 그런데도 저절로 나왔다. 이것이 식 (14.3)이 말한 내용이다.

이제 길이를 1로 만든다. 길이 제곱부터.

‖v₁‖² = 1 + 25 + 9 = 35      ‖v₂‖² = 1 + 4 + 9 = 14      ‖v₃‖² = 9 + 0 + 1 = 10

√35, √14, √10은 무리수라 여기서 처음 정수가 깨진다. 그래서 이 시리즈는 분모에 근호를 남겨 두는 표기를 쓴다 — 16장 SVD도 같다.

          ⎡ 1/√35    1/√14    3/√10 ⎤
 Q =      ⎢ 5/√35   −2/√14    0     ⎥         Λ = diag(16, 9, 1)
          ⎣ 3/√35    3/√14   −1/√10 ⎦
(14.8) 열 순서는 고유값 내림차순. 각 열의 부호는 뒤집어도 여전히 고유벡터다 — 코드에서 이 자유도를 다룬다.

QTQ = I 확인

대각 — 각 열의 길이 제곱이 1이어야 한다.

(1² + 5² + 3²)/35 = 35/35 = 1 ✓   (1² + 2² + 3²)/14 = 14/14 = 1 ✓   (3² + 0² + 1²)/10 = 10/10 = 1 ✓

비대각 — (라) 맨 위에서 이미 분자가 0임을 확인했다. 분모가 무엇이든 0/분모 = 0이다.

q₁·q₂ = 0/√(35·14) = 0 ✓   q₁·q₃ = 0 ✓   q₂·q₃ = 0 ✓

따라서 QTQ = I, 곧 Q−1 = QT. 역행렬을 구하는 계산이 통째로 사라졌다.

(마) 랭크1 분해로 G를 다시 조립한다

식 (14.5)를 우리 숫자로 적는다. qiqiT = viviT / ‖vi‖²이므로 분모가 깔끔하게 정리된다.

G = 1635 v₁v₁T + 914 v₂v₂T + 110 v₃v₃T (14.9)

세 바깥곱을 적어 두자. 전부 대칭이고 랭크 1이다.

          ⎡ 1   5   3 ⎤             ⎡ 1  −2   3 ⎤             ⎡ 9   0  −3 ⎤
v₁v₁ᵀ =   ⎢ 5  25  15 ⎥   v₂v₂ᵀ =   ⎢−2   4  −6 ⎥   v₃v₃ᵀ =   ⎢ 0   0   0 ⎥
          ⎣ 3  15   9 ⎦             ⎣ 3  −6   9 ⎦             ⎣−3   0   1 ⎦

성분 여섯 개를 분수로 검산

대칭이므로 위쪽 삼각 여섯 개만 보면 된다. 통분 분모는 35, 14, 10의 최소공배수 70.

(1,1): 1635(1) + 914(1) + 110(9) = 3270 + 4570 + 6370 = 14070 = 2 ✓

(2,2): 1635(25) + 914(4) + 0 = 40035 + 3614 = 807 + 187 = 987 = 14 ✓

(3,3): 1635(9) + 914(9) + 110(1) = 28870 + 40570 + 770 = 70070 = 10 ✓

(1,2): 1635(5) + 914(−2) + 0 = 167 − 97 = 77 = 1 ✓

(1,3): 1635(3) + 914(3) + 110(−3) = 9670 + 13570 − 2170 = 21070 = 3 ✓

(2,3): 1635(15) + 914(−6) + 0 = 487 − 277 = 217 = 3 ✓

여섯 성분 전부 G와 일치한다. 무리수 분모가 제곱되어 전부 사라졌다.

이제 (마)의 요점이다. 세 항의 비중이 같지 않다. 고유값이 곧 그 방향에 실린 무게이고, 총합은 tr G = 26이다.

항 고유값 몫 누적 ‖G − 부분합‖F
16 q₁q₁T 16 16/26 = 61.5% 61.5% √82 ≈ 9.055
9 q₂q₂T 9 9/26 = 34.6% 96.2% 1
1 q₃q₃T 1 1/26 = 3.8% 100% 0

첫 항 하나로 G의 61.5%가 설명되고, 두 항이면 96.2%다. 마지막 항은 3.8%밖에 안 된다. 버려도 되는 방향이 있다는 발상이 여기서 시작하고, 18장 저랭크 근사가 그것을 정리로 만든다. 잔차 노름이 √82 → 1 → 0으로 줄어드는데, 남은 항들의 고유값 제곱합 √(9²+1²) = √82, √(1²) = 1과 정확히 같다는 점을 눈여겨보자 — 이것이 에카르트-영 정리의 예고편이다.

왜 A의 열은 직교가 아닌데 G의 고유벡터는 직교하는가

혼동이 자주 일어나는 지점이다. A의 열은 c₁ = (1,0,1,0), c₂ = (1,2,0,3), c₃ = (0,0,3,1)이고, c₁·c₂ = 1 ≠ 0이다. 직교가 아니다. 애초에 G의 비대각이 0이 아니라는 말이 정확히 그 뜻이었다(1장).

그런데 방금 G의 고유벡터는 직교했다. 모순이 아니다. 두 가지는 서로 다른 대상이기 때문이다.

열 cj는 ℝ⁴에 산다. 그리고 그것은 그냥 자료가 준 방향이다. 특징 1을 어떤 단위로 쟀는지, 특징 2와 얼마나 같이 움직이는지 — 전부 세상이 정한 것이고 서로 직각일 이유가 없다.

고유벡터 vi는 ℝ³에 산다. 그것은 자료가 준 방향이 아니라 G가 골라낸 방향이다. "이 세 열을 어떤 비율로 섞으면 G가 늘이기만 하고 돌리지는 않는가"라는 질문의 답이다. v₁ = (1,5,3)은 ℝ⁴의 벡터가 아니라 섞는 비율이다.

정리하면 이렇다. cj는 재료이고 vi는 배합비다. 재료끼리 직각일 필요는 없지만, 대칭행렬이 고르는 배합비는 언제나 서로 직각이다.

그리고 이 배합비를 실제로 ℝ⁴에 태우면 어떻게 될까. Av₁ = (6,10,10,18), Av₂ = (−1,−4,10,−3), Av₃ = (3,0,0,−1)인데 — Av₁·Av₂ = −6 − 40 + 100 − 54 = 0이다. 비직교인 열들을 이 비율로 섞으면 ℝ⁴에서도 직교하는 방향이 나온다. 이것이 16장 SVD의 좌특이벡터이고, 17장 PCA에서는 주성분이라고 불린다.

PCA가 하는 일을 한 문장으로 줄이면 이렇다. 서로 얽혀 있는 원래 특징들을, 공분산행렬이 고른 배합비로 다시 섞어 직교하는 새 특징을 만든다. 배합비가 직교한다는 보장이 바로 이 장의 스펙트럼 정리다.

STEP 04

코드 — 손계산과 소수점까지 대조

특성다항식 계수, eigh 고유쌍, 랭크1 재조립을 한꺼번에 확인한다. 고유벡터는 부호가 자유이므로 대조 전에 손계산 쪽에 부호를 맞춰야 한다 — 빠뜨리면 멀쩡한 결과가 틀린 것처럼 보인다.

ch14_spectral.py
import numpy as np
np.set_printoptions(precision=6, suppress=True)
G = np.array([[2,1,3],[1,14,3],[3,3,10]], dtype=float)
# (1) 특성다항식 계수 — 손계산 λ³ − 26λ² + 169λ − 144 와 대조
minors = (np.linalg.det(G[1:,1:]) + np.linalg.det(G[np.ix_([0,2],[0,2])])
          + np.linalg.det(G[:2,:2]))
print("tr G =", np.trace(G), "| 주소행렬식 합 =", round(minors), "| det G =", round(np.linalg.det(G)))
print("np.poly(G) :", np.round(np.poly(G), 10))
print("손계산     : [  1. -26. 169. -144.]")
# (2) eigh 와 손계산 고유쌍 대조 (열 부호 보정)
lam, Q = np.linalg.eigh(G)                     # eigh 는 오름차순
o = np.argsort(-lam); lam, Q = lam[o], Q[:, o]
hand = np.array([[1,5,3],[1,-2,3],[3,0,-1]], float).T
hand = hand / np.linalg.norm(hand, axis=0)     # ‖v‖² = 35, 14, 10
Q = Q * np.sign((Q*hand).sum(axis=0))          # 부호만 손계산에 맞춘다
print("\n고유값 :", np.round(lam, 10), " 손계산 [16 9 1]")
print("eigh Q :\n", np.round(Q, 6))
print("손계산 :\n", np.round(hand, 6))
print("최대 차이 :", np.abs(Q-hand).max(), "| QᵀQ=I :", np.allclose(Q.T@Q, np.eye(3)))
# (3) 랭크1 분해 G = Σ λ q qᵀ 를 한 항씩 쌓아 복원
acc = np.zeros((3,3))
for k in range(3):
    acc += lam[k] * np.outer(Q[:,k], Q[:,k])
    print(f"\n{k+1}항까지 (λ={lam[k]:.0f} 누적) :\n", np.round(acc, 4))
    print(f"  ‖G−합‖_F = {np.linalg.norm(G-acc):.6f} | 에너지 {lam[:k+1].sum()/lam.sum()*100:.1f}%")
tr G = 26.0 | 주소행렬식 합 = 169 | det G = 144
np.poly(G) : [   1.  -26.  169. -144.]
손계산     : [  1. -26. 169. -144.]
고유값 : [16.  9.  1.]  손계산 [16 9 1]
eigh Q :
[[ 0.169031  0.267261  0.948683]
[ 0.845154 -0.534522 -0.      ]
[ 0.507093  0.801784 -0.316228]]
손계산 :
[[ 0.169031  0.267261  0.948683]
[ 0.845154 -0.534522  0.      ]
[ 0.507093  0.801784 -0.316228]]
최대 차이 : 1.1102230246251565e-16 | QᵀQ=I : True
1항까지 (λ=16 누적) :
[[ 0.4571  2.2857  1.3714]
[ 2.2857 11.4286  6.8571]
[ 1.3714  6.8571  4.1143]]
  ‖G−합‖_F = 9.055385 | 에너지 61.5%
2항까지 (λ=9 누적) :
[[ 1.1  1.   3.3]
[ 1.  14.   3. ]
[ 3.3  3.   9.9]]
  ‖G−합‖_F = 1.000000 | 에너지 96.2%
3항까지 (λ=1 누적) :
[[ 2.  1.  3.]
[ 1. 14.  3.]
[ 3.  3. 10.]]
  ‖G−합‖_F = 0.000000 | 에너지 100.0%

최대 차이가 1.11 × 10−16 — 배정밀도의 기계 엡실론 수준이다. 손으로 푼 (1,5,3)을 √35로 나눈 값과 LAPACK이 반복법으로 얻은 값이 표현 가능한 마지막 자리까지 같다. 첫 줄 0.169031 = 1/√35, 둘째 줄 0.845154 = 5/√35다. ‖G−합‖F가 9.055385 → 1 → 0으로 떨어진 것도 (마)의 표와 일치한다 — √82 = 9.055385…이다.

야코비 회전법 — 대각화를 눈으로 보기

eigh가 내부에서 하는 일을 가장 직관적으로 보여 주는 알고리즘이 야코비 회전법Jacobi rotation method이다. 비대각 성분 한 쌍을 골라 그 자리만 0으로 만드는 회전을 가한다. 회전 J는 직교행렬이라 JTSJ는 여전히 대칭이고 고유값도 그대로다. 한 쌍을 0으로 만들면 다른 쌍이 조금 되살아나지만 비대각 전체의 노름은 반드시 줄어든다. 줄어들기만 하면 언젠가 0이 되고, 그때 남은 대각이 곧 고유값이다.

회전각은 2×2 블록 [[spp, spq],[spq, sqq]]를 대각화하는 각이다.

θ = 12 arctan 2 spqsqq − spp 13장의 2×2 고유값 계산을 한 쌍마다 반복하는 셈이다.

쌍 고르는 법을 두 가지로 비교한다. 절댓값이 가장 큰 비대각을 고르는 방식, 그리고 (0,1) → (0,2) → (1,2)를 그냥 돌리는 순환 방식이다.

ch14_jacobi.py
import numpy as np
G = np.array([[2,1,3],[1,14,3],[3,3,10]], dtype=float)
off = lambda M: np.sqrt((M**2).sum() - (np.diag(M)**2).sum())
def jacobi(pick, label):
    S, V, hist = G.copy(), np.eye(3), [off(G)]
    print(f"\n--- {label} ---   시작 비대각 노름 = {off(S):.8f}")
    for it in range(30):
        p, q = pick(S, it)
        if abs(S[p,q]) < 1e-14:
            if label.startswith("순환") and it % 3: continue
            break
        theta = 0.5*np.arctan2(2*S[p,q], S[q,q]-S[p,p])   # 2×2 블록을 0으로
        c, s = np.cos(theta), np.sin(theta)
        J = np.eye(3); J[p,p]=J[q,q]=c; J[p,q]=s; J[q,p]=-s
        S, V = J.T @ S @ J, V @ J
        hist.append(off(S))
        print(f" {it+1:2d}회전 (p,q)=({p},{q}) θ={theta:+.6f}  비대각 = {off(S):.8f}")
    print(" 대각성분 :", np.round(np.sort(np.diag(S))[::-1], 10),
          "| VᵀV=I :", np.allclose(V.T@V, np.eye(3)),
          "| VΛVᵀ=G :", np.allclose(V@S@V.T, G))
    return hist
greedy = jacobi(lambda S, it: max(((i,j) for i in range(3) for j in range(i+1,3)),
                                  key=lambda ij: abs(S[ij])), "최대원소 선택")
cyclic = jacobi(lambda S, it: [(0,1),(0,2),(1,2)][it % 3], "순환 방식")
fmt = lambda h: " ".join(f"{x:.2e}" for x in h)
print("\n최대원소 비대각 이력 :", fmt(greedy))
print("순환    비대각 이력 :", fmt(cyclic))
print("eigh 고유값 :", np.round(np.sort(np.linalg.eigvalsh(G))[::-1], 10))

--- 최대원소 선택 ---   시작 비대각 노름 = 6.16441400
  1회전 (p,q)=(0,2) θ=+0.321751  비대각 = 4.47213595
  2회전 (p,q)=(1,2) θ=+1.006854  비대각 = 0.00000000
대각성분 : [16.  9.  1.] | VᵀV=I : True | VΛVᵀ=G : True
--- 순환 방식 ---   시작 비대각 노름 = 6.16441400
  1회전 (p,q)=(0,1) θ=+0.082574  비대각 = 6.00000000
  2회전 (p,q)=(0,2) θ=+0.298097  비대각 = 4.57811989
  3회전 (p,q)=(1,2) θ=+1.026551  비대각 = 1.34460179
  4회전 (p,q)=(0,1) θ=-0.062033  비대각 = 1.15033171
  5회전 (p,q)=(0,2) θ=-0.054230  비대각 = 0.07131256
  6회전 (p,q)=(1,2) θ=+0.007193  비대각 = 0.00386542
  7회전 (p,q)=(0,1) θ=+0.000342  비대각 = 0.00002781
  8회전 (p,q)=(0,2) θ=+0.000001  비대각 = 0.00000000
  9회전 (p,q)=(1,2) θ=+0.000000  비대각 = 0.00000000
대각성분 : [16.  9.  1.] | VᵀV=I : True | VΛVᵀ=G : True
최대원소 비대각 이력 : 6.16e+00 4.47e+00 0.00e+00
순환    비대각 이력 : 6.16e+00 6.00e+00 4.58e+00 1.34e+00 1.15e+00 7.13e-02 3.87e-03 2.78e-05 0.00e+00 0.00e+00
eigh 고유값 : [16.  9.  1.]

읽을 것이 셋 있다.

1
비대각 노름은 한 번도 늘지 않았다. 시작값 6.164414 = √(2(1² + 3² + 3²)) = √38이고 순환 방식에서도 6.16 → 6.00 → 4.58 → 1.34 → … 단조 감소다. 야코비법이 반드시 수렴한다는 보장이 이 단조성이다.
2
최대원소 방식은 회전 두 번에 정확히 끝났다. 우리 G가 특별해서다 — 첫 회전이 (0,2)를 0으로 만들면서 남은 배치가 한 번 더 회전하면 완전히 대각이 되는 모양이 됐다. 중간값 4.47213595 = √20 = 2√5도 깔끔한 수다.
3
순환 방식은 후반부에서 오차가 제곱으로 줄어든다. 7.13×10−2 → 3.87×10−3 → 2.78×10−5 → 0. 자릿수가 대략 두 배씩 늘어나는데, 이것이 야코비법의 이차 수렴이다. 뉴턴법에서 본 것과 같은 현상이다.

두 방식 모두 VTV = I이고 VΛVT = G다. 회전을 몇 번 하든 어떤 순서로 하든 직교성이 중간에 깨지지 않는다 — 회전행렬의 곱은 여전히 회전행렬이기 때문이고, 이것이 야코비법이 수치적으로 안전한 이유다.

STEP 05

시각화 — QΛQT가 하는 일

식 (14.4)를 오른쪽부터 읽으면 Sx = Q(Λ(QTx))다. 세 단계가 순서대로 들어 있다.

ch14-d2
도해 2. Sx = Q(Λ(QTx))의 세 단계. ①의 단위원과 ②③의 타원은 서로 다른 축척으로 그렸다(출력 쪽을 약 1/6로 줄였다) — 중요한 것은 두 반축의 비가 정확히 11 : 1이라는 점이다. 13장의 회전행렬이라면 ②단계가 존재할 수 없고, 전단행렬이라면 ①의 두 축이 직각이 아니게 된다. 조건수 κ(S) = 11이 곧 이 타원의 납작한 정도다 — 12장에서 본 그림이 여기서 고유값으로 설명된다.

다음은 3×3. G의 세 고유방향은 ℝ³에서 직각인 축 세 개를 이룬다.

ch14-d3
도해 3. 세 고유방향이 이루는 직교 좌표계. 왼쪽은 ℝ³을 평면에 투영한 그림이라 각도가 왜곡돼 보이지만, 오른쪽의 내적 세 줄이 실제로 직각임을 말해 준다. 이 세 축이 곧 Q의 열이고, 17장 PCA에서는 주성분 방향으로 다시 나타난다.

랭크1 분해 식 (14.9)는 쌓아 가는 그림으로 보는 것이 좋다.

ch14-d4
도해 4. 랭크1 항 세 개가 쌓여 G가 되는 과정. 칸의 진하기는 절댓값 크기다. 첫 조각은 (2,2) 자리에 무게가 몰려 있고 둘째 조각은 (3,3) 쪽, 셋째는 (1,1) 쪽이다. 각 조각은 랭크가 1이라 정보량이 거의 없는데(벡터 하나로 결정된다) 셋을 합치면 G가 정확히 복원된다.

마지막으로 야코비 회전이 대각화해 가는 과정이다. 세로축은 비대각 노름, 로그 눈금이다.

ch14-d5
도해 5. 코드가 실제로 출력한 값을 그대로 옮긴 그림. 빈 동그라미는 정확히 0이 되어 로그 눈금에 찍을 수 없는 점이다. 순환 방식은 초반 네 회전 동안 거의 줄지 않다가(6.16 → 1.15) 후반에 급격히 떨어진다 — 비대각이 작아질수록 한 번의 회전이 나머지를 되살리는 정도가 제곱으로 작아지기 때문이다. 어느 경로로 가든 도착점은 같은 {16, 9, 1}이다.

다섯 그림을 한 줄로 묶으면 이렇다. 대칭행렬은 직교하는 축 위에서 늘이기만 하는 변환이고, 스펙트럼 분해는 그 축과 배율을 찾아내는 일이다. 15장은 여기에 "배율이 전부 양수인가"를 더하고, 16장은 대칭이 아닌 A 자체로 확장한다.

이것만 기억하자

  1. 대칭이면 전부 보장된다. 고유값은 실수(ST = S를 한 번 쓰면 나온다), 서로 다른 고유값의 고유벡터는 자동으로 직교(식 14.3), 따라서 S = QΛQT가 반드시 존재한다. 13장의 복소수·결손 사고는 대칭행렬에서 일어날 수 없다.
  2. Q−1 = QT가 실무의 전부다. 일반 대각화 PΛP−1에서는 P의 조건수가 얼마든지 나빠질 수 있지만 직교행렬의 조건수는 항상 1이다. 대칭행렬에 eig 대신 eigh를 쓰는 이유가 이것이다.
  3. S = ΣλiqiqiT — 크기순으로 정렬된 랭크1 조각들. 우리 G에서는 첫 조각이 61.5%, 두 조각이면 96.2%다. 어떤 조각을 버려도 되는가라는 질문이 저랭크 근사(18장)와 PCA(17장)의 출발점이다.

흔한 오해

  1. "모든 행렬의 고유벡터는 직교한다." — 전혀 아니다. 직교성은 대칭에서만 나온다. 13장의 전단행렬은 고유벡터가 하나뿐이었고, 일반 행렬의 고유벡터들은 서로 몹시 기울어져 있을 수 있다. 그 기울어짐이 곧 P−1의 수치 불안정이다.
  2. "A의 열이 직교가 아니니 G의 고유벡터도 직교가 아닐 것이다." — 둘은 다른 공간의 다른 대상이다. 열 cj는 ℝ⁴의 재료이고 고유벡터 vi는 ℝ³의 배합비다. 재료가 아무리 얽혀 있어도 대칭행렬이 고르는 배합비는 언제나 직교한다 — 그리고 그 배합비로 재료를 섞으면 Avi들도 직교한다. 이것이 PCA다.
  3. "고유값이 실수라는 건 대칭이니 당연한 것 아닌가." — 당연하지 않다. 증명에 ST = S가 정확히 어디서 쓰이는지 보라. 그 등호가 RT = −R로 바뀌는 순간 같은 계산에서 λ = ±i가 나온다. 정리와 반례가 한 줄 차이다.
  4. "고유벡터는 유일하다." — 아니다. 상수배만큼 자유롭고, 정규화한 뒤에도 부호가 자유롭다. numpy의 eigh와 손계산을 대조할 때 부호를 맞춰 주지 않으면 멀쩡한 결과가 틀린 것처럼 보인다. 고유값이 겹치면 자유도는 더 커져서 그 고유공간 안의 어떤 직교 기저든 정답이 된다.

댓글