선형대수 복습

선형대수 복습

개요

수치 시뮬레이션에서 선형대수는 핵심적인 역할을 합니다. 행렬 연산, 연립방정식 풀이, 고유값 문제, 행렬 분해 등을 NumPy/SciPy로 구현하는 방법을 학습합니다.


1. 행렬 기본 연산

1.1 행렬 생성과 연산

import numpy as np
from scipy import linalg

# 행렬 생성
A = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 10]])

B = np.array([[1, 0, 0],
              [0, 1, 0],
              [0, 0, 1]])

# 기본 연산
print("행렬 A:")
print(A)
print(f"\n전치: A.T =\n{A.T}")
print(f"\n행렬곱: A @ B =\n{A @ B}")
print(f"\n요소별 곱: A * B =\n{A * B}")
print(f"\n역행렬: A⁻¹ =\n{np.linalg.inv(A)}")
print(f"\n행렬식: det(A) = {np.linalg.det(A):.4f}")
print(f"\n대각합(trace): tr(A) = {np.trace(A)}")

1.2 특수 행렬 생성

n = 4

# 항등 행렬
I = np.eye(n)

# 영 행렬
Z = np.zeros((n, n))

# 대각 행렬
D = np.diag([1, 2, 3, 4])

# 삼중대각 행렬 (tridiagonal)
diag_main = 2 * np.ones(n)
diag_off = -1 * np.ones(n - 1)
T = np.diag(diag_main) + np.diag(diag_off, k=1) + np.diag(diag_off, k=-1)

print("삼중대각 행렬:")
print(T)

# 랜덤 행렬
np.random.seed(42)
R = np.random.randn(3, 3)
print(f"\n랜덤 행렬:\n{R}")

1.3 행렬 노름

A = np.array([[1, 2], [3, 4]])

# 다양한 노름
print("행렬 노름:")
print(f"  1-노름 (열 합 최대): {np.linalg.norm(A, 1)}")
print(f"  2-노름 (스펙트럴): {np.linalg.norm(A, 2)}")
print(f"  ∞-노름 (행 합 최대): {np.linalg.norm(A, np.inf)}")
print(f"  프로베니우스 노름: {np.linalg.norm(A, 'fro')}")

# 벡터 노름
v = np.array([3, 4])
print(f"\n벡터 노름:")
print(f"  L1: {np.linalg.norm(v, 1)}")
print(f"  L2: {np.linalg.norm(v, 2)}")
print(f"  L∞: {np.linalg.norm(v, np.inf)}")

2. 연립방정식 풀이

2.1 직접 풀이

# Ax = b 풀기
A = np.array([[3, 1], [1, 2]])
b = np.array([9, 8])

# numpy.linalg.solve (권장)
x = np.linalg.solve(A, b)
print(f"해: x = {x}")
print(f"검증: Ax = {A @ x}")

# 역행렬 사용 (비권장 - 느리고 불안정)
x_inv = np.linalg.inv(A) @ b
print(f"역행렬 방식: x = {x_inv}")

2.2 과결정/미결정 시스템

# 과결정 시스템 (방정식 > 미지수) - 최소자승해
A_over = np.array([[1, 1], [1, 2], [1, 3]])
b_over = np.array([1, 2, 2])

# 최소자승해
x_lstsq, residuals, rank, s = np.linalg.lstsq(A_over, b_over, rcond=None)
print(f"최소자승해: {x_lstsq}")
print(f"잔차: {residuals}")

# 미결정 시스템 (방정식 < 미지수) - 최소 노름해
A_under = np.array([[1, 2, 3]])
b_under = np.array([6])

# 의사역행렬 사용
x_min_norm = np.linalg.pinv(A_under) @ b_under
print(f"\n최소 노름해: {x_min_norm}")
print(f"노름: {np.linalg.norm(x_min_norm):.4f}")

3. 고유값과 고유벡터

3.1 고유값 분해

A = np.array([[4, -2],
              [1,  1]])

# 고유값, 고유벡터 계산
eigenvalues, eigenvectors = np.linalg.eig(A)

print("고유값 분해:")
print(f"고유값: {eigenvalues}")
print(f"고유벡터:\n{eigenvectors}")

# 검증: A @ v = λ @ v
for i in range(len(eigenvalues)):
    lam = eigenvalues[i]
    v = eigenvectors[:, i]
    print(f"\nλ_{i} = {lam:.4f}")
    print(f"  A @ v = {A @ v}")
    print(f"  λ * v = {lam * v}")

3.2 대칭 행렬의 고유값

# 대칭 행렬 - 실수 고유값 보장
S = np.array([[2, 1, 0],
              [1, 3, 1],
              [0, 1, 2]])

# eigh는 대칭 행렬에 최적화 (더 빠르고 안정적)
eigenvalues, eigenvectors = np.linalg.eigh(S)

print("대칭 행렬 고유값 분해:")
print(f"고유값: {eigenvalues}")
print(f"\n고유벡터 직교성 검증:")
print(f"V^T @ V =\n{eigenvectors.T @ eigenvectors}")  # 단위행렬

3.3 거듭제곱법 (Power Method)

def power_method(A, max_iter=100, tol=1e-10):
    """최대 고유값과 고유벡터를 거듭제곱법으로 계산"""
    n = A.shape[0]
    v = np.random.randn(n)
    v = v / np.linalg.norm(v)

    for _ in range(max_iter):
        v_new = A @ v
        eigenvalue = np.dot(v, v_new)  # Rayleigh quotient
        v_new = v_new / np.linalg.norm(v_new)

        if np.linalg.norm(v_new - v) < tol:
            break
        v = v_new

    return eigenvalue, v_new

A = np.array([[4, 1], [2, 3]])
lam, v = power_method(A)
print(f"거듭제곱법 결과:")
print(f"  최대 고유값: {lam:.6f}")
print(f"  고유벡터: {v}")

# numpy 결과와 비교
lam_np, v_np = np.linalg.eig(A)
print(f"\nnumpy 결과:")
print(f"  고유값: {lam_np}")

4. 행렬 분해

4.1 LU 분해

from scipy.linalg import lu, lu_factor, lu_solve

A = np.array([[2, 1, 1],
              [4, 3, 3],
              [8, 7, 9]], dtype=float)

# LU 분해
P, L, U = lu(A)

print("LU 분해:")
print(f"P (순열 행렬):\n{P}")
print(f"L (하삼각):\n{L}")
print(f"U (상삼각):\n{U}")
print(f"\n검증: P @ L @ U =\n{P @ L @ U}")

# 연립방정식 풀이에 활용
b = np.array([4, 10, 24])
lu_piv = lu_factor(A)
x = lu_solve(lu_piv, b)
print(f"\n해: {x}")

4.2 Cholesky 분해

# 양정치 대칭 행렬에 대해서만 가능
A_spd = np.array([[4, 2, 2],
                  [2, 5, 1],
                  [2, 1, 6]], dtype=float)

# Cholesky 분해: A = L @ L.T
L = np.linalg.cholesky(A_spd)

print("Cholesky 분해:")
print(f"L:\n{L}")
print(f"\n검증: L @ L.T =\n{L @ L.T}")

# 연립방정식 풀이 (LU보다 2배 빠름)
b = np.array([8, 8, 9])
# L @ y = b 풀고, L.T @ x = y 풀기
y = linalg.solve_triangular(L, b, lower=True)
x = linalg.solve_triangular(L.T, y)
print(f"\n해: {x}")

4.3 QR 분해

A = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 10],
              [10, 11, 12]], dtype=float)

# QR 분해
Q, R = np.linalg.qr(A)

print("QR 분해:")
print(f"Q (직교 행렬):\n{Q}")
print(f"\nR (상삼각):\n{R}")
print(f"\n검증: Q @ R =\n{Q @ R}")
print(f"\nQ의 직교성: Q.T @ Q =\n{Q.T @ Q}")

# 최소자승 문제에 활용
b = np.array([1, 2, 3, 4])
x = linalg.solve_triangular(R, Q.T @ b)
print(f"\n최소자승해: {x}")

4.4 SVD (특이값 분해)

A = np.array([[1, 2],
              [3, 4],
              [5, 6]])

# SVD: A = U @ Σ @ V.T
U, s, Vt = np.linalg.svd(A)

print("SVD 분해:")
print(f"U (m×m 직교):\n{U}")
print(f"\n특이값: {s}")
print(f"\nV.T (n×n 직교):\n{Vt}")

# Σ 행렬 구성
Sigma = np.zeros_like(A, dtype=float)
np.fill_diagonal(Sigma, s)

print(f"\n검증: U @ Σ @ V.T =\n{U @ Sigma @ Vt}")

# 조건수 계산
cond = s[0] / s[-1]
print(f"\n조건수: {cond:.4f}")

5. 희소 행렬

5.1 희소 행렬 형식

from scipy import sparse

# COO 형식 (좌표 형식)
row = [0, 1, 2, 2]
col = [0, 1, 0, 2]
data = [1, 2, 3, 4]
A_coo = sparse.coo_matrix((data, (row, col)), shape=(3, 3))

print("COO 형식:")
print(A_coo)
print(f"\n밀집 형태:\n{A_coo.toarray()}")

# CSR 형식 (행 압축, 행렬-벡터 곱에 효율적)
A_csr = A_coo.tocsr()
print(f"\nCSR 형식: {A_csr}")

# CSC 형식 (열 압축, 열 슬라이싱에 효율적)
A_csc = A_coo.tocsc()

5.2 희소 행렬 연산

# 큰 희소 행렬 생성
n = 1000
diagonals = [np.ones(n-1), -2*np.ones(n), np.ones(n-1)]
offsets = [-1, 0, 1]
A_sparse = sparse.diags(diagonals, offsets, format='csr')

print(f"희소 행렬 크기: {A_sparse.shape}")
print(f"비영 요소 수: {A_sparse.nnz}")
print(f"희소도: {1 - A_sparse.nnz / (n*n):.4%}")

# 희소 행렬 연립방정식
b = np.random.randn(n)
x = sparse.linalg.spsolve(A_sparse, b)
print(f"\n희소 연립방정식 해의 노름: {np.linalg.norm(x):.4f}")

5.3 반복 솔버

from scipy.sparse.linalg import cg, gmres, bicgstab

# 대칭 양정치 행렬 생성
n = 100
A = sparse.diags([1, -2, 1], [-1, 0, 1], shape=(n, n), format='csr')
A = A.T @ A + 0.1 * sparse.eye(n)  # 양정치로 만들기
b = np.random.randn(n)

# CG (Conjugate Gradient) - 대칭 양정치에 최적
x_cg, info_cg = cg(A, b, tol=1e-10)
print(f"CG: 수렴 상태 = {info_cg}, 잔차 = {np.linalg.norm(A @ x_cg - b):.2e}")

# GMRES - 일반 행렬
x_gmres, info_gmres = gmres(A, b, tol=1e-10)
print(f"GMRES: 수렴 상태 = {info_gmres}, 잔차 = {np.linalg.norm(A @ x_gmres - b):.2e}")

6. 응용 예제

6.1 열전도 문제의 이산화

def heat_equation_matrix(n, alpha, dx, dt):
    """1D 열방정식의 암시적 이산화 행렬"""
    r = alpha * dt / dx**2

    # 삼중대각 행렬 생성
    main_diag = (1 + 2*r) * np.ones(n)
    off_diag = -r * np.ones(n - 1)

    A = np.diag(main_diag) + np.diag(off_diag, 1) + np.diag(off_diag, -1)
    return A

n = 10
A = heat_equation_matrix(n, alpha=0.1, dx=0.1, dt=0.01)
print("열방정식 이산화 행렬:")
print(A[:5, :5])  # 일부만 출력

6.2 이미지 압축 (SVD)

def svd_compression(image, k):
    """SVD로 이미지 압축 (상위 k개 특이값 사용)"""
    U, s, Vt = np.linalg.svd(image, full_matrices=False)

    # 상위 k개 성분만 사용
    compressed = U[:, :k] @ np.diag(s[:k]) @ Vt[:k, :]

    # 압축률 계산
    original_size = image.shape[0] * image.shape[1]
    compressed_size = k * (image.shape[0] + image.shape[1] + 1)
    compression_ratio = compressed_size / original_size

    return compressed, compression_ratio

# 예시 (랜덤 이미지)
image = np.random.randn(100, 100)
for k in [5, 10, 20, 50]:
    comp, ratio = svd_compression(image, k)
    error = np.linalg.norm(image - comp, 'fro') / np.linalg.norm(image, 'fro')
    print(f"k={k:2d}: 압축률={ratio:.2%}, 상대오차={error:.4f}")

연습 문제

문제 1

3x3 삼중대각 행렬의 고유값을 구하고, 거듭제곱법 결과와 비교하세요.

# 풀이
T = np.array([[2, -1, 0],
              [-1, 2, -1],
              [0, -1, 2]], dtype=float)

# numpy
eig_np, _ = np.linalg.eig(T)
print(f"numpy 고유값: {sorted(eig_np)}")

# 거듭제곱법
lam, v = power_method(T)
print(f"거듭제곱법 최대 고유값: {lam:.6f}")

문제 2

100x100 희소 행렬의 연립방정식을 직접법과 반복법으로 풀고 시간을 비교하세요.

import time

n = 1000
A = sparse.diags([1, -2, 1], [-1, 0, 1], shape=(n, n), format='csr')
A = A + 3 * sparse.eye(n)  # 대각 우세하게
b = np.random.randn(n)

# 직접법
start = time.time()
x_direct = sparse.linalg.spsolve(A, b)
time_direct = time.time() - start

# 반복법 (CG)
A_spd = A.T @ A
b_spd = A.T @ b
start = time.time()
x_cg, _ = cg(A_spd, b_spd, tol=1e-10)
time_cg = time.time() - start

print(f"직접법: {time_direct:.4f}초")
print(f"CG: {time_cg:.4f}초")

요약

분해 방법 용도 조건
LU 연립방정식 풀이 정방 행렬
Cholesky 연립방정식 (빠름) 대칭 양정치
QR 최소자승, 고유값 모든 행렬
SVD 압축, 의사역행렬 모든 행렬
솔버 행렬 유형 특징
spsolve 희소 직접법
CG 대칭 양정치 반복법
GMRES 일반 반복법
BiCGSTAB 비대칭 반복법
to navigate between lessons