분할, 스펙트럼 반경, Jacobi·Gauss-Seidel·SOR·ILU·CG·Multigrid
직접법(LU·QR·Cholesky)은 \(O(n^3)\) 의 비용으로 정확한 해를 한 번에 구하지만, \(n\) 이 수백만인 대규모 희소 행렬에는 불가능하다. 반복법은 초기 추정에서 출발해 단계적으로 참해에 다가가는 전략이다. 이 포스트는 Strang §9.3 을 뼈대로 분할(splitting) \(A = S - T\) 프레임워크 → 수렴 조건 \(\rho(S^{-1}T) < 1\) → Jacobi / Gauss-Seidel / SOR / ILU → Multigrid → Krylov (CG, GMRES) → 전처리기까지, 왜 각 기법이 필요한지와 언제 어떤 것을 선택해야 하는지를 직관과 수식으로 정리한다.
Mathematics
Machine Learning
저자
Kwangmin Kim
공개
2026년 04월 13일
1 개요
지금까지의 선형 시스템 풀이는 직접법(direct method) 이었다. LU, QR, Cholesky 분해로 유한 단계 안에 정확한 해를 얻는다. 이들은 작은 조밀 행렬(\(n \leq 10^4\))에서는 최적이지만, \(n\) 이 \(10^6\) 을 넘는 희소(sparse) 시스템에서는 메모리와 시간 모두 한계에 부딪힌다. PDE 이산화, 그래프 라플라시안, 대규모 최적화의 Hessian이 이 영역이다.
반복법(iterative method) 은 다른 철학이다. 초기 추정 \(\mathbf{x}_0\) 에서 출발해 매 단계 단순한 연산만 수행하여 참해에 점근적으로 접근한다. 각 단계는 저렴하지만 많이 반복한다. 경쟁 요소 두 개는 다음과 같다.
단계당 비용이 작아야 한다 — 행렬-벡터 곱 수준이면 이상적.
빠르게 수렴해야 한다 — 스펙트럼 반경이 작아야 한다.
이 둘은 서로 상충한다. 단계가 단순할수록 수렴이 느리고, 수렴을 빠르게 하려면 단계가 무거워진다. 이 속도-수렴 균형이 반복법 설계의 중심 주제이며, 균형을 깨뜨리는 무기가 전처리기(preconditioner) 다.
이 포스트는 Strang (2009) Introduction to Linear Algebra Ch.9 §9.3 “Iterative Methods and Preconditioners” 의 흐름을 따르되, 현대 수치해석의 표준인 Krylov 방법(CG, GMRES)과 Multigrid, preconditioned CG까지 확장해 다룬다. 세부 증명은 Trefethen & Bau (1997) Lect.32–40, Saad (2003) Iterative Methods for Sparse Linear Systems 를 보완 참고한다.
2 분할(Splitting) 프레임워크
2.1 기본 아이디어
\(\mathbf{A}\mathbf{x} = \mathbf{b}\) 에서 \(\mathbf{A}\) 를 두 조각으로 쪼갠다.
\[
\mathbf{A} = \mathbf{S} - \mathbf{T}
\]
여기서 \(\mathbf{S}\) 는 역연산이 쉬운 행렬이고 \(\mathbf{T} = \mathbf{S} - \mathbf{A}\) 는 나머지다. 원식을 옮겨 쓰면 \(\mathbf{S}\mathbf{x} = \mathbf{T}\mathbf{x} + \mathbf{b}\) 이다. 이를 반복 공식으로 바꾼다.
정리 (수렴 조건): \(\mathbf{B}^k \to 0\) 필요충분조건은 \(\rho(\mathbf{B}) < 1\). 따라서 분할법의 수렴은 반복 행렬 \(\mathbf{B} = \mathbf{S}^{-1}\mathbf{T}\) 의 고유값이 전부 단위원 내부에 있을 때만 보장된다 (Strang §9.3 “Spectral Radius Controls Convergence”).
3.2 유도
\(\mathbf{B}\) 가 대각화 가능하면 \(\mathbf{B} = \mathbf{M}\boldsymbol{\Lambda}\mathbf{M}^{-1}\) 이고 \(\mathbf{B}^k = \mathbf{M}\boldsymbol{\Lambda}^k \mathbf{M}^{-1}\). 대각 원소는 \(\lambda_i^k\) 이며, 모두가 0으로 가려면 \(|\lambda_i| < 1\) 이 전 \(i\) 에 대해 성립해야 한다.
각 고유성분이 \(\lambda_i^k\) 배로 감쇠·증폭된다. 가장 큰 \(|\lambda_i|\) 가 장기 수렴을 지배한다.
직관: 스펙트럼 반경은 “가장 느린 성분의 감쇠율”
오차는 여러 고유방향으로 분해되어 각 방향이 독립적으로 진화한다. 어떤 방향은 \(|\lambda_i| = 0.01\) 로 즉시 사라지지만, 다른 방향은 \(|\lambda_i| = 0.999\) 로 거의 감쇠하지 않는다. 수렴 속도는 가장 끈질긴 방향 — \(\rho(\mathbf{B})\) — 에 의해 결정된다. 평균이 아니라 최댓값이 중요하다.
1에 가까운 \(\rho\) 는 “수렴하긴 하지만 실질적으로 쓸모없이 느리다”를 의미한다. 실무 PDE 이산화에서 Jacobi의 \(\rho\) 가 전형적으로 \(1 - O(h^2)\) (\(h\) 는 격자 간격)여서 Jacobi 단독으로는 느리다.
3.3 감쇠율과 자리수 감소
10진 자리수 관점에서: 반복 1회당 평균 자리수 감소는 \(-\log_{10} \rho\). 즉 \(\rho = 0.9\) 는 한 자리 감소에 약 22 반복, \(\rho = 0.99\) 는 약 230 반복이 필요하다. 실무 종료 조건(잔차 \(10^{-6}\))에 도달하려면 각각 $$130, $$1400 반복이다.
\(\omega = 1\) 이면 Gauss-Seidel, \(\omega > 1\) 이면 “과도 완화(over-relax)” — 새 값 방향으로 추가 이동.
6.2 최적 \(\omega\)
Strang \(-1, 2, -1\) 테스트 행렬(1D Poisson 이산화) 의 크기 \(n\) 에서 Jacobi 스펙트럼 반경은 \(\rho_J = \cos\frac{\pi}{n+1}\) 이고 (고유값은 \(2 - 2\cos\frac{k\pi}{n+1}\), \(k = 1, \dots, n\)), 각 방법의 스펙트럼 반경은:
\(n\) 이 커질 때 Jacobi·Gauss-Seidel의 \(1 - \rho\) 는 \(1/n^2\) 수준이므로 반복 수가 \(n^2\) 에 비례한다. SOR 최적 \(\omega^*\) 는 \(1 - \rho\) 를 \(1/n\) 수준으로 끌어올려 반복 수가 \(n\) 에 비례한다. 즉 \(n = 1000\) 격자에서 Gauss-Seidel은 \(\sim 10^6\) 반복, SOR은 \(\sim 10^3\) 반복이다 — 천 배 차이.
과도 완화가 왜 도움이 되는가. Jacobi·Gauss-Seidel은 “저주파(low-frequency) 오차 성분”을 거의 감쇠시키지 못한다 (이 성분의 고유값이 1에 매우 가깝다). \(\omega > 1\) 은 각 단계에서 더 큰 걸음을 내디뎌 저주파 감쇠율을 인위적으로 키운다. 단 너무 크면 고주파 성분이 발산하므로 정확한 최적값이 존재한다.
6.3 문제: \(\omega^*\) 를 모른다
실무에서 \(\omega^*\) 는 \(\mathbf{A}\) 의 스펙트럼에 의존하므로 선험적으로 알 수 없다. 대안:
샘플 반복으로 \(\rho\) 를 추정해 \(\omega\) 를 수치적으로 탐색
특정 문제군(Poisson, 등)에서 해석적 \(\omega^*\) 를 사용
SOR을 포기하고 Multigrid 또는 PCG로 이동
현대 실무에서 SOR은 단독 해법보다 전처리기 내부 구성 요소로 쓰인다.
7 ILU: 불완전 LU 전처리
7.1 아이디어
\(\mathbf{A}\) 의 정확한 LU 분해는 fill-in 때문에 희소성을 파괴한다. 불완전 LU (Incomplete LU) 는 LU 분해 과정에서 원래 \(\mathbf{A}\) 에서 0이었던 자리의 fill-in을 인위적으로 버린다.
\[
\mathbf{A} \approx \mathbf{L}_0 \mathbf{U}_0, \qquad \mathbf{L}_0, \mathbf{U}_0 \text{ 는 } \mathbf{A} \text{ 와 같은 희소 패턴}
\]
ILU는 단독 해법보다 Krylov 방법의 전처리기로 주로 쓰인다. GMRES + ILU(0) 조합은 비대칭 희소 시스템의 사실상 표준이다.
8 Multigrid: 다중 격자 방법
8.1 저주파 오차 문제
Jacobi·Gauss-Seidel의 치명적 약점: 저주파(low-frequency) 오차 성분이 느리게 감쇠한다. 격자 한 점에서 이웃으로 정보가 퍼지려면 여러 반복이 필요하므로, 큰 규모(낮은 주파수) 오차는 전파 속도에 지배된다.
8.2 해법: 여러 해상도를 오간다
Multigrid의 통찰: “세밀한 격자에서 저주파는 거친 격자에서는 고주파로 보인다”. 거친 격자에서는 Jacobi가 그 성분을 빨리 감쇠시킨다.
V-cycle 한 단계:
세밀한 격자에서 Jacobi 몇 번 (pre-smoothing) — 고주파 성분 제거
잔차를 거친 격자로 제한 (restriction)
거친 격자에서 재귀적으로 해결 (coarse-grid correction) — 저주파 성분 처리
해를 세밀한 격자로 보간 (prolongation)
Jacobi 몇 번 (post-smoothing) — 보간 오차 정돈
8.3 성능
타원형 PDE에서 Multigrid는 \(O(n)\) 에 기계 정밀도 에 도달한다. 즉 반복 수가 격자 크기에 의존하지 않는다. 이는 모든 반복법 중 이 종류 문제에서 최적 성능이다.
직관: Multigrid는 “여러 망원경을 번갈아 쓴다”
고배율 망원경으로 별의 세부를 보지만 전체 은하 구조는 안 보인다. 저배율 망원경으로 은하 구조를 보지만 세부는 안 보인다. 두 망원경을 번갈아 쓰면 세부와 전체를 모두 파악할 수 있다. Multigrid가 바로 이 전략이다 — 세밀한 격자(고주파 망원경)와 거친 격자(저주파 망원경)를 V자로 왕복한다.
9 Krylov 부분공간 방법
9.1 Krylov 공간
정상 반복법은 단계마다 같은 행렬 \(\mathbf{B}\) 를 곱하지만, 단계 \(k\) 의 근사 \(\mathbf{x}_k\) 는 이전 \(\mathbf{x}_{k-1}\) 의 단순 변환이라 “이전에 수집한 정보를 다 쓰지 않는다”. Krylov 방법은 \(k\) 단계까지 생성된 전 벡터 \(\mathbf{b}, \mathbf{A}\mathbf{b}, \mathbf{A}^2\mathbf{b}, \dots, \mathbf{A}^{k-1}\mathbf{b}\) 가 생성하는 부분공간에서 최적\(\mathbf{x}_k\) 를 고른다.
정의: Krylov 부분공간
\(\mathbf{A} \in \mathbb{R}^{n \times n}\), \(\mathbf{b} \in \mathbb{R}^n\) 에 대해 \(k\)-차 Krylov 부분공간은
반복 수는 \(O(\sqrt{\kappa})\) 으로 목표 정확도에 도달한다. 정상 반복법의 \(O(\kappa)\) 보다 제곱근만큼 빠르다.
직관: CG는 왜 빠른가
Jacobi·GS는 매 단계 “현재 잔차 방향”으로만 움직인다. 같은 방향으로 반복 이동하다 보니 이미 처리한 방향을 또 건드려 자기 작업을 되돌리는 일이 생긴다. CG는 \(\mathbf{A}\)-직교(conjugate) 방향들을 생성하여 “한 방향에서 한 것은 다시 건드리지 않는다”를 보장한다. \(n\) 차원 공간에서 서로 conjugate한 방향은 최대 \(n\) 개이므로 최대 \(n\) 번에 모든 방향이 처리된다. 그래서 유한 수렴이 가능하다.
일상 비유: 쇼핑 목록의 물건을 한 번씩만 사러 가는 것(CG) vs 같은 물건을 여러 번 사러 왕복하는 것(Jacobi).
9.4 GMRES, MINRES, BiCGSTAB
방법
적용
기준
CG
SPD
\(\|\mathbf{e}\|_\mathbf{A}\) 최소화
MINRES
대칭 (부정치 허용)
\(\|\mathbf{r}\|_2\) 최소화
GMRES
일반 (비대칭)
\(\|\mathbf{r}\|_2\) 최소화, Arnoldi 기반
BiCGSTAB
일반
양측 Krylov, 메모리 \(O(n)\)
GMRES의 비용은 단계마다 Arnoldi 재직교화로 \(O(kn)\) 누적(최대 \(k\) 단계), 메모리 \(O(kn)\). 이 때문에 재시작(restart) GMRES(m)이 실무 표준이다.
10 전처리기 (Preconditioner)
10.1 핵심 아이디어
조건수 \(\kappa\) 가 크면 Krylov 방법의 수렴이 느리다. 전처리(preconditioning) 는 원 시스템을 스펙트럼이 좋은 등가 시스템으로 변환한다.
Preconditioned CG (PCG): CG 알고리즘에서 내적을 \(\mathbf{M}^{-1}\) 기반으로 수정하여 효과적으로 \(\mathbf{M}^{-1}\mathbf{A}\) 의 스펙트럼으로 수렴시킨다. 각 단계에 \(\mathbf{M}\mathbf{y} = \mathbf{z}\) 풀이 한 번만 추가된다.
직관: 전처리기는 “안경”이다
근시인 사람에게 글자가 흐리게 보이는 것이 \(\kappa\) 가 큰 문제와 같다. 글자 자체를 키워 쓰게(문제 재구성) 하는 대신 안경(preconditioner)을 씌워 눈에 들어오는 상(像)을 선명하게 만든다. 안경 설계 비용(transformation 비용)이 글자 다시 쓰는 비용(직접법)보다 훨씬 싸다면 안경이 이긴다. 적절한 안경 도수는 시력(문제 스펙트럼)에 맞춰야 하므로 전처리기 선택이 문제 구조에 의존한다.
10.3 전처리 효과 수치 감각
Strang \(-1, 2, -1\) 격자(\(n = 1000\))에서 \(\kappa(\mathbf{A}) \approx 4n^2/\pi^2 \approx 4 \times 10^5\). 각 방법의 반복 수:
방법
반복 수 (잔차 \(10^{-6}\))
Jacobi
\(\sim 10^7\)
Gauss-Seidel
\(\sim 5 \times 10^6\)
SOR (최적 \(\omega\))
\(\sim 3000\)
CG
\(\sim 1000\) (= \(\sqrt{\kappa}\))
PCG (ILU 전처리)
\(\sim 100\)
PCG (Multigrid 전처리)
\(\sim 10\)
5~7 자리 차이가 실무에서 나타나는 전형적 격차다.
11 실무 의사결정 트리
선형 시스템 A x = b 를 풀어야 한다
│
├─ n ≤ 10^4 이고 dense?
│ → 직접법 (LU, Cholesky, QR): scipy.linalg.solve
│
├─ 희소 (nnz ≪ n^2) 이고 n ≤ 10^5?
│ → 희소 직접법 (SuperLU, UMFPACK): scipy.sparse.linalg.spsolve
│
└─ 대규모 희소 (n ≥ 10^5)?
├─ A 가 SPD?
│ → PCG + ILU(0) 또는 Multigrid
│ scipy.sparse.linalg.cg(A, b, M=...)
│
├─ A 가 대칭 (부정치)?
│ → MINRES + ILU
│
└─ A 가 일반 (비대칭)?
→ GMRES(m) + ILU(0) 또는 ILUT
scipy.sparse.linalg.gmres(A, b, M=..., restart=30)
12 코드 예시
12.1 Step 1: Jacobi·Gauss-Seidel·SOR 비교 구현
코드
import numpy as npdef build_K(n):"""-1, 2, -1 tridiagonal (1D Poisson)"""return2*np.eye(n) - np.eye(n, k=1) - np.eye(n, k=-1)def jacobi(A, b, tol=1e-8, max_iter=100000): n =len(b) D = np.diag(A); R = A - np.diag(D) x = np.zeros(n)for k inrange(max_iter): x_new = (b - R @ x) / Dif np.linalg.norm(x_new - x) < tol:return x_new, k+1 x = x_newreturn x, max_iterdef gauss_seidel(A, b, tol=1e-8, max_iter=100000): n =len(b); x = np.zeros(n)for k inrange(max_iter): x_old = x.copy()for i inrange(n): x[i] = (b[i] - A[i, :i] @ x[:i] - A[i, i+1:] @ x[i+1:]) / A[i, i]if np.linalg.norm(x - x_old) < tol:return x, k+1return x, max_iterdef sor(A, b, omega, tol=1e-8, max_iter=100000): n =len(b); x = np.zeros(n)for k inrange(max_iter): x_old = x.copy()for i inrange(n): sigma = A[i, :i] @ x[:i] + A[i, i+1:] @ x[i+1:] x[i] = (1-omega)*x[i] + omega*(b[i] - sigma) / A[i, i]if np.linalg.norm(x - x_old) < tol:return x, k+1return x, max_itern =50A = build_K(n); b = np.ones(n)omega_opt =2.0/ (1+ np.sin(np.pi/(n+1)))_, it_J = jacobi(A, b)_, it_GS = gauss_seidel(A, b)_, it_SOR = sor(A, b, omega_opt)print(f"Jacobi: {it_J} iterations")print(f"Gauss-Seidel: {it_GS} iterations (≈ Jacobi/2 예상)")print(f"SOR (ω*={omega_opt:.3f}): {it_SOR} iterations (차수 한 단계 빠름)")
이 코드의 핵심: \(n = 50\) 에서 Jacobi \(\sim 5000\), Gauss-Seidel \(\sim 2500\), SOR \(\sim 100\) 수준이다. 격자 \(n\) 을 늘리면 Jacobi·GS는 \(n^2\) 로 증가하지만 SOR은 \(n\) 로 증가한다.
12.2 Step 2: Conjugate Gradient 직접 구현
코드
import numpy as npdef cg(A, b, x0=None, tol=1e-10, max_iter=None):"""CG for SPD A. Returns (x, iterations, residual_history).""" n =len(b); x = np.zeros(n) if x0 isNoneelse x0.copy() r = b - A @ x; d = r.copy() rs_old = r @ r max_iter = max_iter or n hist = [np.sqrt(rs_old)]for k inrange(max_iter): Ad = A @ d alpha = rs_old / (d @ Ad) x = x + alpha * d r = r - alpha * Ad rs_new = r @ r hist.append(np.sqrt(rs_new))if np.sqrt(rs_new) < tol:return x, k+1, hist beta = rs_new / rs_old d = r + beta * d rs_old = rs_newreturn x, max_iter, hist# 비교: 같은 문제를 직접법 + CG + Jacobi로 풀어 본다n =200A = build_K(n); b = np.random.randn(n)x_direct = np.linalg.solve(A, b)x_cg, it_cg, _ = cg(A, b)print(f"CG 반복수: {it_cg} (이론 상한 n={n})")print(f"CG 오차 ||x_cg - x_direct||: {np.linalg.norm(x_cg - x_direct):.2e}")# 예측: sqrt(kappa) ~ 반복수kappa = np.linalg.cond(A)print(f"κ(A) = {kappa:.1f}, √κ ≈ {np.sqrt(kappa):.1f} (CG 반복수 상한 감각)")
12.3 Step 3: 전처리 효과 관찰 (PCG vs CG)
코드
import numpy as npfrom scipy.sparse import diags, csr_matrixfrom scipy.sparse.linalg import cg, spilu, LinearOperator# 2D Poisson 격자 (nxn) → N = n*ndef build_poisson_2d(n): e = np.ones(n) T = diags([-e, 2*e, -e], [-1, 0, 1]) I = diags([np.ones(n)], [0]) A = (np.kron(I.toarray(), T.toarray())+ np.kron(T.toarray(), I.toarray()))return csr_matrix(A)n =30A = build_poisson_2d(n); N = A.shape[0]b = np.random.randn(N)# CG without preconditionercounter = [0]def cb(xk): counter[0] +=1x_cg, info = cg(A, b, tol=1e-8, maxiter=5000, callback=cb)iters_cg = counter[0]# PCG with ILU preconditionerilu = spilu(A, drop_tol=1e-3, fill_factor=10)M = LinearOperator(A.shape, matvec=ilu.solve)counter = [0]x_pcg, info = cg(A, b, M=M, tol=1e-8, maxiter=5000, callback=cb)iters_pcg = counter[0]print(f"2D Poisson N = {N}")print(f"CG 반복수: {iters_cg}")print(f"PCG(ILU): {iters_pcg}")print(f"가속비: {iters_cg/iters_pcg:.1f}x")
이 코드의 핵심: 전처리기는 수렴 자체를 바꾸는 것이 아니라 같은 CG 알고리즘의 유효 조건수를 낮춰 반복 수를 줄인다. 실무 PDE 해법은 거의 항상 전처리와 함께 쓰인다.