본문 바로가기
선형대수

손으로 푸는 선형대수 16장 — SVD: 모든 행렬의 정규분해

by 카이스토 2026. 9. 24.

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

SVD — 모든 행렬의 정규분해

13장에서 대각화되지 않는 행렬을 보았고, 애초에 정사각이 아니면 고유값을 말할 수조차 없다. 그런데 특이값 분해는 모든 행렬에 존재한다. 예외가 없다. 이 장은 A = UΣVT를 15장의 양반정치성 하나에서 유도하고, 우리 A의 SVD를 분수와 근호만으로 종이 위에서 끝까지 완성한다. 2장이 "네 번째 읽기가 16장의 본체"라고 예고했던 그 약속을 여기서 회수한다.

  1. 직관 — 회전 → 늘이기 → 회전
  2. 수식 읽는 법 — ATA에서 존재를 끌어낸다
  3. 손으로 풀기 — U, Σ, V 전부를 정수로
  4. 코드 — np.linalg.svd와 소수점까지 대조
  5. 시각화 — 단위구가 타원체가 되기까지

약어 및 기호 정의

특이값 분해 singular value decomposition, SVD
A = UΣVT. U는 m×m 직교, V는 n×n 직교, Σ는 m×n 대각. 실수 행렬이면 언제나 존재한다.
특이값 σi singular value
ATA 고유값의 양의 제곱근. 관례상 σ1 ≥ σ2 ≥ … ≥ 0으로 내림차순 정렬한다. 우리 A는 σ = 4, 3, 1.
우특이벡터 vi right singular vector
V의 열. ATA의 정규직교 고유벡터다. 입력 쪽(ℝn)에 산다.
좌특이벡터 ui left singular vector
U의 열. AAT의 정규직교 고유벡터다. 출력 쪽(ℝm)에 산다.
축소형 SVD thin / reduced SVD
σ > 0인 r개만 남긴 A = UrΣrVrT. 우리 A는 r = 3이라 Ur이 4×3이 된다.
완전형 SVD full SVD
U를 m×m까지 채운 것. 남는 열은 좌영공간의 기저가 된다 — 우리 A에서는 n = (−1,5,1,−3) 하나.
직교행렬 orthogonal matrix
QTQ = QQT = I. 길이와 각도를 보존한다(9장). 기하로는 회전 또는 반사.
바깥곱 uvT outer product
랭크1 행렬 한 장(2장). SVD는 A를 이 장들의 가중합으로 다시 쓴 것이다.
의사역행렬 A+ pseudoinverse
A+ = VΣ+UT. 0이 아닌 σ만 뒤집는다. 역행렬이 없어도 존재한다(19장).
크로네커 델타 δij Kronecker delta
i = j면 1, 아니면 0. "정규직교"를 한 줄로 쓰는 기호다.
STEP 01

직관 — 어떤 행렬이든 회전 · 늘이기 · 회전이다

13장에서 고유값 이야기를 끝내고 나면 두 가지 불만이 남는다.

1
정사각이 아니면 시작조차 못 한다. Av = λv라고 쓰려면 양변이 같은 공간에 있어야 한다. 우리 A는 ℝ³ → ℝ⁴이니 v ∈ ℝ³, Av ∈ ℝ⁴. 등식 자체가 성립하지 않는다.
2
정사각이어도 대각화가 안 될 수 있다. 13장의 전단행렬 [[1,1],[0,1]]은 고유벡터가 한 방향뿐이라 기저를 못 만든다. 대각화되더라도 고유벡터가 직교하지 않으면 X−1을 따로 구해야 하고, 조건수가 나쁘면 그것이 위험하다(8장).

SVD는 이 둘을 한꺼번에 없앤다. 모양을 가리지 않고, 랭크를 가리지 않고, 실수 행렬이면 무조건 존재한다. 그리고 양쪽 기저가 둘 다 직교다. 대가는 하나 — 들어가는 기저와 나오는 기저가 다를 수 있다는 것을 받아들여야 한다. 고유벡터가 "들어간 방향이 그대로 나오는" 방향이라면, 특이벡터는 "들어간 직교틀이 나올 때도 직교틀인" 방향이다. 방향이 아니라 직각이 보존된다.

기하로 보면 한 문장이다. 특이값 분해SVD는 임의의 선형사상을 회전 → 축별 늘이기 → 회전 세 동작으로 쪼갠다.

   x  ──VT──►  회전(입력 기저를 표준축에 맞춤)
      ──Σ ──►  축별로 σ₁, σ₂, σ₃ 배 늘이기 (여기서만 모양이 변한다)
      ──U ──►  회전(표준축을 출력 기저에 맞춤)

회전은 길이도 각도도 바꾸지 않는다. 모양을 바꾸는 것은 가운데 단계뿐이다. 그래서 행렬이 하는 일 중 "정보"에 해당하는 부분은 전부 σ 세 개에 들어 있다. 그림으로 말하면 ℝ³의 단위구가 ℝ⁴ 안의 타원체가 된다 — 반축 길이가 σ₁, σ₂, σ₃ = 4, 3, 1, 반축 방향이 u₁, u₂, u₃다.

ch16-d1
도해 1. A = UΣVT의 세 동작. 단위구를 표준축에 맞추고(VT), 축마다 σ = 4, 3, 1배 늘이고(Σ), 출력 기저로 돌린다(U). 그림은 2차원 단면이지만 셋째 축이 σ₃ = 1이라 거의 변하지 않는다는 점만 더 기억하면 된다. 8장의 조건수 κ = 4는 이 타원체의 최장반축 대 최단반축 비율이다.

이 그림이 앞 장들과 그대로 이어진다. 조건수 κ₂(A) = σ₁/σ₃ = 4는 타원체가 얼마나 찌그러졌나(8장), 랭크 3은 몇 차원인가(4장), √det G = 12 = 4·3·1은 부피 배율(7장)이다. 전부 같은 세 숫자에서 나온다.

STEP 02

수식 읽는 법 — 존재를 ATA에서 끌어낸다

먼저 목표를 정확히 쓴다.

A = U Σ VT,   UTU = Im,   VTV = In,   Σ = diag(σ₁, …, σr) 확장형
(16.1) A가 m×n이면 U는 m×m, Σ는 m×n, V는 n×n. 우리 A는 4×3이므로 U가 4×4, Σ가 4×3, V가 3×3이다.

존재를 증명하는 가장 짧은 길은 이미 가진 것에서 시작하는 것이다. 우리가 이미 가진 것은 15장의 결과다.

15장이 준 것 — 이 유도의 유일한 재료

G = ATA는 대칭이고((ATA)T = ATA) 양반정치다(xTGx = ‖Ax‖² ≥ 0). 따라서 14장의 스펙트럼 정리에 의해 G는 정규직교 고유벡터 v₁,…,vn를 가지고, 고유값은 전부 λi ≥ 0이다.

이 두 줄이 전부다. SVD의 존재는 새 정리가 아니라 15장 정리의 번역이다.

이제 네 걸음이면 끝난다.

1
σ를 정의한다. λi ≥ 0이므로 σi = √λi가 실수다. 내림차순으로 정렬하고, σi > 0인 개수를 r이라 하자. r = rank(A)다 — Gv = 0 ⇔ ‖Av‖² = 0 ⇔ Av = 0이니 G의 영공간이 곧 A의 영공간이다(4장).
2
u를 정의한다. i ≤ r에 대해 ui = Avi / σi. σi > 0이니 나눌 수 있다. 정의일 뿐 아직 아무것도 주장하지 않았다.
3
u가 정규직교임을 보인다. 여기가 유일한 계산이다.
uiTuj = (Avi)T(Avj)σiσj = viT(ATA)vjσiσj = viT(λj vj)σiσj = σj2σiσj viTvj = σjσi δij = δij
마지막 등호에서 i = j일 때만 살아남으므로 σj/σi = 1이다. 입력 쪽 직교성이 G를 통과하면서 출력 쪽 직교성이 된다. 이 한 줄이 SVD 전체를 지탱한다.
4
분해를 조립한다. {vi}가 ℝn의 정규직교 기저이므로 임의의 x를 x = Σ (viTx) vi로 펼치고(9장) A를 먹이면 Ax = Σ σi ui (viTx). i > r인 항은 Avi = 0이라 사라진다. x가 임의이므로 행렬 등식으로 읽힌다.
A = rΣi=1 σi ui viT  =  Ur Σr VrT
(16.2) 2장 식 (2.4)의 네 번째 읽기 그 자체다. 무게가 σ이고 양쪽 벡터가 정규직교인 랭크1 합. 2장에서 "이 읽기가 후반부를 지배한다"고 한 것이 이것이다.

U를 m×m으로 채우려면 나머지 직교보완 기저를 붙이면 된다. u₁,…,ur이 이미 C(A)를 꽉 채우므로 남는 열은 C(A)⊥ = N(AT) — 좌영공간의 정규직교 기저다. 4장의 네 부분공간이 여기서 U와 V의 열로 갈라진다.

AAT 쪽에서 보면

같은 σ가 반대쪽에서도 나온다. 식 (16.1)을 그대로 대입하면

ATA = (UΣVT)T(UΣVT) = VΣTUTUΣVT = V (ΣTΣ) VT
AAT = (UΣVT)(UΣVT)T = UΣVTVΣTUT = U (ΣΣT) UT
(16.3) 둘 다 14장의 스펙트럼 분해 QΛQT 꼴이다. ΣTΣ는 n×n, ΣΣT는 m×m이지만 0이 아닌 대각성분은 σi²로 똑같다.

즉 V는 ATA의, U는 AAT의 고유벡터 행렬이고 둘은 0이 아닌 고유값을 공유한다. 우리 예제에서 G는 3×3에 고유값 16, 9, 1, AAT는 4×4에 16, 9, 1, 0이다. 남는 0 하나가 좌영공간이다. 같은 정보를 작은 쪽에서 계산하면 된다는 것 — 17장 PCA의 실무 요령이 정확히 이것이다.

ch16-d2
도해 2. vi는 ATA의 고유벡터, ui는 AAT의 고유벡터이고 둘은 ui = Avi/σi로 이어진다. 오른쪽에 하나 남는 u₄는 짝이 없다 — 그것이 좌영공간이고, σ₄는 존재하지 않는다. 랭크란 짝이 지어지는 방향의 개수다.
STEP 03

손으로 풀기 — U, Σ, V를 분수와 근호로 끝까지

14장에서 G = ATA의 스펙트럼 분해를 이미 끝냈다. 그 결과를 그대로 가져온다.

      ⎡ 1  1  0 ⎤          ⎡  2   1   3 ⎤
 A =  ⎢ 0  2  0 ⎥    G =   ⎢  1  14   3 ⎥    det G = 144 = 12²
      ⎢ 1  0  3 ⎥          ⎣  3   3  10 ⎦
      ⎣ 0  3  1 ⎦
 고유값 λ = 16, 9, 1        고유벡터 (정수, 서로 직교)
   λ₁=16 : (1, 5, 3)   ‖·‖² = 1+25+9  = 35
   λ₂= 9 : (1, −2, 3)  ‖·‖² = 1+4+9   = 14
   λ₃= 1 : (3, 0, −1)  ‖·‖² = 9+0+1   = 10
(16.4)

(가) Σ — 제곱근 한 번

σi = √λi다. 그뿐이다.

σ₁ = √16 = 4    σ₂ = √9 = 3    σ₃ = √1 = 1
        ⎡ 4  0  0 ⎤
  Σ  =  ⎢ 0  3  0 ⎥     (4×3. 마지막 행은 전부 0 — u₄ 에 대응하는 σ 가 없다)
        ⎢ 0  0  1 ⎥
        ⎣ 0  0  0 ⎦
(16.5) 특이값이 정수로 떨어지는 4×3 정수행렬은 흔하지 않다. 1장에서 A를 이렇게 고른 이유가 여기 있다.

(나) V — 정규화만 하면 된다

고유벡터를 각자의 길이로 나눈다. 14장에서 서로 직교인 것은 이미 확인했으니 정규화만 남았다.

v₁ = 1√35(1, 5, 3)      v₂ = 1√14(1, −2, 3)      v₃ = 1√10(3, 0, −1)
          ⎡ 1/√35   1/√14    3/√10 ⎤
   V  =   ⎢ 5/√35  −2/√14    0     ⎥       (3×3, 직교행렬)
          ⎣ 3/√35   3/√14   −1/√10 ⎦
(16.6)

(다) U — u = Av/σ를 세 번

이제 핵심 계산이다. A에 정수 고유벡터를 먹이고 σ와 길이로 나눈다. 정수벡터에 먼저 곱하고 나중에 나누는 것이 요령이다.

u₁ — λ = 16 쪽

       ⎡ 1  1  0 ⎤ ⎡ 1 ⎤   ⎡ 1·1 + 1·5 + 0·3 ⎤   ⎡  6 ⎤
A(1,5,3) = ⎢ 0  2  0 ⎥ ⎢ 5 ⎥ = ⎢ 0·1 + 2·5 + 0·3 ⎥ = ⎢ 10 ⎥
       ⎢ 1  0  3 ⎥ ⎣ 3 ⎦   ⎢ 1·1 + 0·5 + 3·3 ⎥   ⎢ 10 ⎥
       ⎣ 0  3  1 ⎦         ⎣ 0·1 + 3·5 + 1·3 ⎦   ⎣ 18 ⎦

정규화 인자는 σ₁‖v₁의 정수형‖ = 4·√35다. 짝수를 약분하면

u₁ = A(1,5,3)4√35 = (6, 10, 10, 18)4√35 = (3, 5, 5, 9)2√35

검산: ‖(6,10,10,18)‖² = 36 + 100 + 100 + 324 = 560. 그리고 (4√35)² = 16·35 = 560. 정확히 같으니 ‖u₁‖ = 1이다. 약분형으로도 9+25+25+81 = 140 = 4·35 = (2√35)² — 일치한다.

u₂ — λ = 9 쪽

A(1,−2,3) = (1−2+0,  0−4+0,  1+0+9,  0−6+3) = (−1, −4, 10, −3)
u₂ = (−1, −4, 10, −3)3√14

검산: 1 + 16 + 100 + 9 = 126 = 9·14 = (3√14)². 일치.

u₃ — λ = 1 쪽

A(3, 0, −1) = (3+0+0,  0+0+0,  3+0−3,  0+0−1) = (3, 0, 0, −1)
u₃ = (3, 0, 0, −1)1·√10 = (3, 0, 0, −1)√10

검산: 9 + 0 + 0 + 1 = 10 = (1·√10)². 일치. σ₃ = 1이라 늘이기가 없었고, 그래서 길이가 √10 그대로다.

세 검산이 전부 ‖Avi‖² = σi²‖vi‖²라는 같은 항등식이다 — 560 = 16·35, 126 = 9·14, 10 = 1·10. "행렬이 vi 방향을 정확히 σi배 늘인다"의 숫자판이다.

(라) 직교성 확인, 그리고 u₄의 정체

정리에 따르면 u₁, u₂, u₃는 자동으로 직교해야 한다. 분모는 전부 양수이니 분자 정수벡터끼리 내적해 보면 된다.

(3,5,5,9)·(−1,−4,10,−3)  = −3 − 20 + 50 − 27 = 0   ✓
(3,5,5,9)·(3,0,0,−1)     =   9 +  0 +  0 −  9 = 0   ✓
(−1,−4,10,−3)·(3,0,0,−1) =  −3 +  0 +  0 +  3 = 0   ✓
(16.7)

세 개의 0이 우연이 아니라는 것이 STEP 02의 세 번째 걸음이었다. 이제 U를 4×4로 채울 네 번째 열이 필요하다. ℝ⁴에서 u₁,u₂,u₃에 모두 수직인 방향은 1차원뿐이고, 우리는 그것을 4장에서 이미 구해 두었다.

좌영공간 기저   n = (−1, 5, 1, −3),   ‖n‖² = 1 + 25 + 1 + 9 = 36,   ‖n‖ = 6
    u₄ = n6 = (−1, 5, 1, −3)6
(16.8) ATn = 0은 4장에서 확인했다. 따라서 u₄는 C(A) 전체에, 즉 u₁,u₂,u₃ 전부에 수직이다.
직접 확인:
(3,5,5,9)·(−1,5,1,−3)      = −3 + 25 +  5 − 27 = 0   ✓
(−1,−4,10,−3)·(−1,5,1,−3)  =  1 − 20 + 10 +  9 = 0   ✓
(3,0,0,−1)·(−1,5,1,−3)     = −3 +  0 +  0 +  3 = 0   ✓

여기가 이 장에서 가장 만족스러운 지점이다. n = (−1,5,1,−3)은 4장에서 "행들 사이에 숨은 선형관계", 10장에서 "잔차 방향", 12장에서 "SSE = 36을 만드는 벡터"였다. 이제 그것은 U의 마지막 열이다. σ₄는 없다 — 랭크가 3이라 짝지을 v₄가 없기 때문이다.

        ⎡  3/2√35   −1/3√14    3/√10   −1/6 ⎤
  U  =  ⎢  5/2√35   −4/3√14    0        5/6 ⎥      (4×4, 직교행렬)
        ⎢  5/2√35   10/3√14    0        1/6 ⎥
        ⎣  9/2√35   −3/3√14   −1/√10   −3/6 ⎦
(16.9) 소수로는 u₁ ≈ (0.253546, 0.422577, 0.422577, 0.760639). STEP 04에서 numpy 출력과 소수점 여섯째 자리까지 맞춰 본다.

(마) 랭크1 분해 — 2장의 네 번째 읽기

식 (16.2)를 우리 숫자로 쓴다. 여기서 아름다운 약분이 일어난다.

σi ui viT = σi · A wiσi‖wi‖ · wiT‖wi‖ = (A wi) wiT‖wi‖²      (wi = 정수 고유벡터)
(16.10) σ도 근호도 전부 약분되어 정수 나누기 정수만 남는다. 손계산이 여기서 갑자기 쉬워진다.
4 u₁v₁T = 135 ⎡ 6⎤          3 u₂v₂T = 114 ⎡ −1⎤         1 u₃v₃T = 110 ⎡ 3⎤
          ⎢10⎥ (1, 5, 3)             ⎢ −4⎥ (1,−2,3)            ⎢ 0⎥ (3, 0,−1)
          ⎢10⎥                       ⎢ 10⎥                      ⎢ 0⎥
          ⎣18⎦                       ⎣ −3⎦                      ⎣−1⎦
  = 135⎡ 6 30 18⎤        = 114⎡ −1   2  −3⎤       = 110⎡ 9  0 −3⎤
    ⎢10 50 30⎥            ⎢ −4   8 −12⎥           ⎢ 0  0  0⎥
    ⎢10 50 30⎥            ⎢ 10 −20  30⎥           ⎢ 0  0  0⎥
    ⎣18 90 54⎦            ⎣ −3   6  −9⎦           ⎣−3  0  1⎦
(16.11)

세 장을 더하면 A가 나와야 한다. 첫 행만 확인해 보자. 분모 70으로 통분한다.

1열:  635 − 114 + 910 = 1270 − 570 + 6370 = 7070 = 1   ✓
2열:  3035 + 214 + 0     = 6070 + 1070 + 0     = 7070 = 1   ✓
3열:  1835 − 314 − 310 = 3670 − 1570 − 2170 = 0        ✓
→ 첫 행 (1, 1, 0) — A 의 첫 행 그대로.

덤으로 얻는 것이 있다. 앞의 두 장만 더하면 랭크2 최적 근사 A₂가 되는데, 우리 예제에서는 그것이 십분의 일 단위로 딱 떨어진다.

        ⎡ 0.1  1.0  0.3 ⎤          ⎡ 0.9  0  −0.3 ⎤
 A₂  =  ⎢ 0.0  2.0  0.0 ⎥   A−A₂ = ⎢ 0.0  0   0.0 ⎥ = 1·u₃v₃T
        ⎢ 1.0  0.0  3.0 ⎥          ⎢ 0.0  0   0.0 ⎥
        ⎣ 0.3  3.0  0.9 ⎦          ⎣−0.3  0   0.1 ⎦
 ‖A−A₂‖F² = 0.81 + 0.09 + 0.09 + 0.01 = 1.00 = σ₃²
(16.12) 버린 항의 크기가 정확히 σ₃ = 1이다. 18장의 에카르트–영 정리가 이것을 일반화한다: 랭크 k 근사의 오차는 버린 특이값들이 전부 말해 준다.

(바) 네 가지 검산 — 앞 장들이 전부 돌아온다

특이값 세 개를 구했으면 앞에서 손으로 구해 둔 네 숫자와 반드시 맞춰 본다. 하나라도 어긋나면 계산이 틀린 것이다.

검산 특이값으로 앞 장에서 구한 값 출처
곱 σ₁σ₂σ₃ = 4·3·1 = 12 √det G = √144 = 12 7장 행렬식
제곱합 σ₁²+σ₂²+σ₃² = 16+9+1 = 26 ‖A‖F² = tr(G) = 26 2장 · 8장
최대 σ₁ = 4 ‖A‖₂ = 4 8장 스펙트럼 노름
비율 σ₁/σ₃ = 4/1 = 4 κ₂(A) = 4 8장 조건수

네 개 전부 맞는다. 특히 두 번째 줄을 보자. 2장에서 ‖A‖F² = 26은 관측 네 건의 길이 제곱 합이었다(2+4+10+10). 여기서는 늘이기 배율의 제곱 합이다(16+9+1). 같은 숫자를 행 쪽에서도, 방향 쪽에서도 셀 수 있다 — 2장이 예고한 다리가 이것이다.

첫 줄도 기하로 읽힌다. 단위구(부피 1)가 반축 4, 3, 1짜리 타원체가 되었으니 부피가 12배다. 정사각이 아닌 행렬의 부피 배율을 말해 주는 것이 √det(ATA)이고, 그 값이 곧 특이값의 곱이다(7장).

(사) 의사역행렬 — σ만 뒤집는다

SVD가 손에 들어오면 의사역행렬pseudoinverse은 공짜다. 늘인 것을 되돌리면 된다.

A+ = V Σ+ UT = 3Σi=1 1σi vi uiT     (0 인 σ 는 건드리지 않는다)
(16.13)

12장의 최소제곱 해 β̂ = A+b를 이 식으로 다시 구해 보자. b = (1,7,5,1)이다. 계수는 uiTb / σi다.

u₁Tb = (6,10,10,18)·(1,7,5,1)4√35 = 6+70+50+184√35 = 1444√35 = 36√35
u₂Tb = −1−28+50−33√14 = 183√14 = 6√14
u₃Tb = 3+0+0−1√10 = 2√10
β̂ = 14·36√35·(1,5,3)√35 + 13·6√14·(1,−2,3)√14 + 2√10·(3,0,−1)√10
   = 935(1,5,3) + 17(1,−2,3) + 15(3,0,−1)
1성분: 9+5+2135 = 1    2성분: 45−10+035 = 1    3성분: 27+15−735 = 1
→ β̂ = (1, 1, 1)   ✓  10장·12장과 같은 답

정규방정식도, QR도, 소거도 쓰지 않았다. b를 세 특이방향으로 분해하고 늘이기를 되돌렸을 뿐이다. b에는 u₄ 성분도 있는데(n·b = 36, 즉 u₄Tb = 6) 식 (16.13)이 그 성분을 아예 쳐다보지 않는다. 버려진 그것이 정확히 잔차 n이고 SSE = 36이다. 최소제곱이 "잔차를 버린다"는 말은 SVD에서 "σ가 없는 방향을 건너뛴다"가 된다.

ch16-d3
도해 3. 4장의 네 부분공간이 U와 V의 열로 정확히 갈라진다. V의 앞 r열은 행공간, 나머지는 영공간. U의 앞 r열은 열공간, 나머지는 좌영공간. 우리 A는 r = 3, n = 3이라 영공간이 비고, m = 4라 좌영공간에 u₄ 하나가 남는다.

고유분해와 SVD — 무엇이 다른가

  고유분해 A = XΛX−1 특이값분해 A = UΣVT
적용 조건 정사각이어야 하고, 고유벡터가 기저를 이뤄야 한다(13장) 조건 없음. 모든 실수 행렬
기저 입출력 기저가 같다. 대신 직교가 아닐 수 있다 입출력 기저가 다르다. 대신 둘 다 직교
되돌리기 X−1을 따로 구해야 한다 UT, VT로 끝 — 전치가 곧 역
값의 부호 λ는 음수·복소수 가능 σ ≥ 0 항상 실수
수치 안정성 X의 조건수가 나쁘면 위험 직교행렬만 쓰므로 안정
세는 것 변하지 않는 방향 늘어나는 배율

둘이 일치하는 경우가 있다. S가 대칭 양정치면(15장) 스펙트럼 분해 S = QΛQT 자체가 SVD다 — U = V = Q, Σ = Λ. 우리 G가 그렇다: G의 고유값 16, 9, 1이 곧 G의 특이값이다. 대칭인데 음의 고유값이 있으면 σi = |λi|가 되고 부호가 ui = −vi로 U 쪽에 넘어간다.

STEP 04

코드 — 손계산과 np.linalg.svd를 소수점까지 맞춘다

먼저 손으로 구한 U, Σ, V를 그대로 조립해 A가 복원되는지 보고, 그다음 numpy 결과와 대조한다. 부호 규약은 라이브러리마다 다르므로 열마다 부호 하나만 맞춰 준다.

svd_by_hand.py — 손계산 조립 후 numpy 와 대조
import numpy as np
np.set_printoptions(precision=6, suppress=True)
A = np.array([[1,1,0],[0,2,0],[1,0,3],[0,3,1]], float)
n = np.array([-1,5,1,-3], float)
# ── 손계산 SVD 를 그대로 조립한다 ───────────────────────────
Vint = np.array([[1,5,3],[1,-2,3],[3,0,-1]], float)   # 14장의 정수 고유벡터
Vhand = (Vint / np.linalg.norm(Vint, axis=1)[:,None]).T   # 열이 v1 v2 v3
shand = np.array([4., 3., 1.])                            # σ = √(16,9,1)
Uhand = (A @ Vhand) / shand                               # u_i = A v_i / σ_i
print("U (손계산, 열 = u1 u2 u3)"); print(Uhand)
print("UᵀU = I :", np.allclose(Uhand.T@Uhand, np.eye(3)),
      "  VᵀV = I :", np.allclose(Vhand.T@Vhand, np.eye(3)))
print("UΣVᵀ = A :", np.allclose(Uhand@np.diag(shand)@Vhand.T, A))
# ── numpy 와 소수점까지 대조 (부호 규약만 보정) ───────────────
U, s, Vt = np.linalg.svd(A)
print("\nσ  손계산 :", shand, "  numpy :", s)
print("σ  최대오차 :", np.abs(s-shand).max())
sg = np.sign((Vt[:3].T * Vhand).sum(axis=0))          # 열마다 부호 하나
print("부호 규약(numpy/손) :", sg.astype(int))
print("V 최대오차 :", np.abs(Vt[:3].T*sg - Vhand).max())
print("U 최대오차 :", np.abs(U[:,:3]*sg - Uhand).max())
# ── U 의 네 번째 열 = 좌영공간 ──────────────────────────────
u4 = U[:,3]
print("\nu4 (numpy) :", u4)
print("n/6        :", n/6)
print("‖u4‖² =", round(float(u4@u4),12), "  Aᵀu4 =", np.round(A.T@u4, 12))
print("u4 = ±n/6 :", np.allclose(np.abs(u4), np.abs(n/6)))
U (손계산, 열 = u1 u2 u3)
[[ 0.253546 -0.089087  0.948683]
 [ 0.422577 -0.356348  0.      ]
 [ 0.422577  0.890871 -0.      ]
 [ 0.760639 -0.267261 -0.316228]]
UᵀU = I : True   VᵀV = I : True
UΣVᵀ = A : True
σ  손계산 : [4. 3. 1.]   numpy : [4. 3. 1.]
σ  최대오차 : 4.440892098500626e-16
부호 규약(numpy/손) : [ 1  1 -1]
V 최대오차 : 4.440892098500626e-16
U 최대오차 : 3.885780586188048e-16
u4 (numpy) : [ 0.166667 -0.833333 -0.166667  0.5     ]
n/6        : [-0.166667  0.833333  0.166667 -0.5     ]
‖u4‖² = 1.0   Aᵀu4 = [-0.  0.  0.]
u4 = ±n/6 : True

읽을 것이 세 가지다.

1
손계산이 정확하다. σ, U, V 모두 numpy와 10−16대에서 일치한다 — 배정밀도 ε ≈ 2.2×10−16 수준이니 차이가 없다는 뜻이다. u₁ ≈ (0.253546, …)은 (3,5,5,9)/2√35의 소수 표현 그대로다.
2
부호는 규약이다. 세 번째 열에서 −1이 나왔다. (u₃, v₃)를 동시에 뒤집어도 σ₃u₃v₃T는 그대로이므로 SVD는 부호 쌍만큼 유일하지 않다. LAPACK의 부호를 예측하려 들지 말고 비교할 때 맞춰 주면 된다.
3
u₄가 좌영공간이다. numpy는 −n/6을 내놨지만 방향은 같고 ‖u₄‖² = 1, ATu₄ = 0이 기계 정밀도로 확인된다. 4장에서 소거로 구했던 n이 SVD에서는 덤으로 따라 나온다 — 실무에서 영공간을 SVD로 구하는 이유다.

다음은 의사역행렬과 랭크1 재조립이다.

svd_uses.py — 의사역행렬 · 랭크1 누적 · 네 가지 검산
import numpy as np
np.set_printoptions(precision=6, suppress=True)
A = np.array([[1,1,0],[0,2,0],[1,0,3],[0,3,1]], float)
b = np.array([1,7,5,1], float); n = np.array([-1,5,1,-3], float)
Vint = np.array([[1,5,3],[1,-2,3],[3,0,-1]], float)
V = (Vint/np.linalg.norm(Vint,axis=1)[:,None]).T
s = np.array([4.,3.,1.]); U = (A@V)/s
# ── ① SVD 로 만든 의사역행렬 A⁺ = V Σ⁻¹ Uᵀ ──────────────────
Ap = V @ np.diag(1/s) @ U.T
print("144·A⁺ (정수로 떨어진다)"); print(np.round(Ap*144, 9))
print("A⁺b   =", Ap@b, "  ← β̂ = (1,1,1)")
print("A⁺A = I₃ :", np.allclose(Ap@A, np.eye(3)))
print("AA⁺ = P = I − nnᵀ/36 :", np.allclose(A@Ap, np.eye(4)-np.outer(n,n)/36))
# ── ② 랭크1 항을 한 겹씩 쌓아 A 를 복원 ──────────────────────
print("\n-- σᵢuᵢvᵢᵀ 를 한 겹씩 --")
S = np.zeros((4,3))
for i in range(3):
    S += s[i]*np.outer(U[:,i], V[:,i])
    print(f"k={i+1}  ‖A−A_k‖_F = {np.linalg.norm(A-S):.6f}"
          f"   ‖A−A_k‖₂ = {np.linalg.norm(A-S,2):.6f}")
print("복원한 A ="); print(np.round(S,10)+0.0)
# ── ③ 네 가지 검산 ──────────────────────────────────────────
print("\nσ1σ2σ3 =", round(float(np.prod(s)),9),
      " | √det(AᵀA) =", round(float(np.sqrt(np.linalg.det(A.T@A))),9))
print("Σσᵢ²   =", round(float((s**2).sum()),9),
      " | ‖A‖_F²     =", round(float((A**2).sum()),9))
print("σ1     =", s[0], " | ‖A‖₂        =", round(float(np.linalg.norm(A,2)),9))
print("σ1/σ3  =", s[0]/s[2], " | κ₂(A)       =", round(float(np.linalg.cond(A)),9))
144·A⁺ (정수로 떨어진다)
[[130.  -2.  14. -42.]
 [ 10.  22. -10.  30.]
 [-42.  -6.  42.  18.]]
A⁺b   = [1. 1. 1.]   ← β̂ = (1,1,1)
A⁺A = I₃ : True
AA⁺ = P = I − nnᵀ/36 : True
-- σᵢuᵢvᵢᵀ 를 한 겹씩 --
k=1  ‖A−A_k‖_F = 3.162278   ‖A−A_k‖₂ = 3.000000
k=2  ‖A−A_k‖_F = 1.000000   ‖A−A_k‖₂ = 1.000000
k=3  ‖A−A_k‖_F = 0.000000   ‖A−A_k‖₂ = 0.000000
복원한 A =
[[1. 1. 0.]
 [0. 2. 0.]
 [1. 0. 3.]
 [0. 3. 1.]]
σ1σ2σ3 = 12.0  | √det(AᵀA) = 12.0
Σσᵢ²   = 26.0  | ‖A‖_F²     = 26.0
σ1     = 4.0  | ‖A‖₂        = 4.0
σ1/σ3  = 4.0  | κ₂(A)       = 4.0

144A+가 정수로 떨어지는 것은 우연이 아니다. A+ = (ATA)−1AT = adj(G)AT/144이고(6장·7장) adj(G)가 정수행렬이기 때문이다. 근호투성이 U, V로 만들었는데 정수가 나온다는 것이 둘이 정확하다는 또 하나의 증거다.

AA+ = I − nnT/36도 눈여겨보자. 10장의 투영행렬이 여기서 SVD로 다시 유도된다. AA+ = U(ΣΣ+)UT이고 ΣΣ+ = diag(1,1,1,0)이므로 u₁u₁T+u₂u₂T+u₃u₃T = I − u₄u₄T. 투영행렬이란 "σ > 0인 방향만 1로 두는 대각행렬"이었던 것이다.

누적 오차 √10, 1, 0은 18장의 주제다. 미리 말해 두면 ‖A−Ak‖F² = σk+1²+…+σr²(우리 값 9+1과 1), ‖A−Ak‖₂ = σk+1(정확히 3과 1).

STEP 05

시각화 — 겹이 쌓여 행렬이 된다

식 (16.11)의 세 장이 어떻게 A가 되는지를 숫자로 그린다. 한 겹씩 얹을 때마다 A에 가까워지고, 남는 오차가 정확히 다음 σ다.

ch16-d4
도해 4. 랭크1 겹이 쌓여 A가 복원되는 과정. 두 겹만 쌓아도 A₂는 A와 1의 차이밖에 없고, 그 차이가 마지막 겹 1·u₃v₃T 그 자체다. 아래 막대는 각 겹이 ‖A‖F² = 26을 어떻게 나눠 가지는지 보여 준다 — 16 : 9 : 1.

왜 이 장이 시리즈의 정점인가

지금까지 따로 배운 것들이 전부 σ = 4, 3, 1과 U, V의 열로 환원된다. 한 표로 정리한다.

장 그때 배운 것 SVD 언어로 다시 쓰면
2장 행렬 곱의 네 번째 읽기 — 랭크1 합 A = Σ σiuiviT 그 자체
4장 랭크 3, 좌영공간 n = (−1,5,1,−3) 랭크 = σ > 0의 개수. n/6 = u₄
7장 det G = 144 (σ₁σ₂σ₃)² = 12² = 144
8장 ‖A‖₂ = 4, ‖A‖F² = 26, κ = 4 σ₁, Σσi², σ₁/σ₃
10장 투영행렬 P = I − nnT/36 u₁u₁T+u₂u₂T+u₃u₃T
12장 최소제곱 β̂ = (1,1,1) A+b = Σ (uiTb / σi) vi
14장 G의 스펙트럼 분해 QΛQT Q = V, Λ = ΣTΣ = diag(16,9,1)
15장 G가 대칭 양정치 SVD 존재 증명의 유일한 재료

제4부는 여기서 끝난다. 13장에서 "방향이 변하지 않는 축"을 물었고, 14장에서 대칭행렬이면 그 축들이 직교한다는 것을 보았고, 15장에서 ATA가 언제나 그 조건을 만족한다는 것을 확인했다. 16장은 그 세 걸음을 하나로 접어 모든 행렬에 적용했다. 이제 남은 것은 쓰는 일이다. 17장은 이 V가 데이터를 중심화했을 때 무엇이 되는지를 묻고(그것이 PCA다), 18장은 이 σ 막대를 어디서 자를지를 묻는다. 두 장 모두 새 정리를 배우지 않는다 — 이 장에서 구한 U, Σ, V를 읽는 방법만 배운다.

이것만 기억하자

  1. SVD가 항상 존재하는 이유는 ATA가 대칭 양반정치이기 때문이다. 새 정리가 아니라 15장 정리의 번역이다. σi = √λi(ATA)로 두고 ui = Avi/σi로 정의하면 uiTuj = δij가 한 줄로 따라 나온다.
  2. 기하는 "회전 → 늘이기 → 회전"이고 정보는 전부 가운데에 있다. σ = 4, 3, 1에서 부피 배율 12 = √det G, 찌그러짐 κ = 4, 에너지 16+9+1 = 26 = ‖A‖F²가 전부 나온다.
  3. U와 V는 네 부분공간의 정규직교 기저를 한 번에 준다. V의 앞 r열이 행공간, U의 앞 r열이 열공간, 나머지가 각각 영공간과 좌영공간. 우리 A에서는 u₄ = n/6 — 4장의 그 벡터가 U의 마지막 열로 돌아온다.

흔한 오해

  1. "특이값은 고유값의 절댓값이다." — 대칭행렬에서만 맞는 말이다. 일반 행렬에서는 아무 관계가 없다. [[0,10],[0,0]]은 고유값이 0, 0인데 특이값은 10, 0이다. 고유값은 "변하지 않는 방향"을, 특이값은 "늘어나는 배율"을 센다.
  2. "SVD는 유일하다." — 특이값의 목록만 유일하다. (ui, vi)를 동시에 뒤집어도 σiuiviT가 그대로이므로 부호만큼 자유도가 있고, 특이값이 겹치면 그 부분공간 안의 회전만큼 더 자유롭다. STEP 04에서 numpy가 세 번째 열의 부호를 뒤집은 이유다.
  3. "SVD를 쓰려면 ATA를 만들어 고유분해하면 된다." — 유도는 그렇게 했지만 계산은 그러면 안 된다. κ(ATA) = κ(A)²이라 조건수가 제곱되고(4 → 16), 작은 σ는 σ²에서 더 작아져 반올림에 묻힌다(8장). LAPACK의 gesdd는 A를 이중대각화해 바로 다루며 ATA를 만들지 않는다.
  4. "Σ의 0인 자리는 그냥 0으로 나눈 셈 치면 된다." — Σ+는 0이 아닌 σ만 뒤집고 0인 자리는 0으로 둔다. 실무에서는 σ < τ를 0으로 간주하는 절단값을 쓰며, numpy.linalg.pinv의 rcond가 그것이다. 이 절단이 저랭크 근사로 이어진다(18장).

댓글