보강 1 서술 파트의 물음은 셋이었다. 왜 공분산행렬인가. 왜 고유벡터인가. 그런데 왜 라이브러리는 공분산을 안 만드는가.
이 노트북에서는 셋을 전부 손으로 확인한다. 다섯 점짜리 앵커로 PCA를 끝까지 계산해
np.linalg.eigh 와 np.linalg.svd 가 같은 답을 주는지 보고, 조건수를 올려 가며
공분산 경유가 어디서 무너지는지 직접 잰다. 그리고 중심화를 빼먹으면 무슨 일이
벌어지는지 눈으로 본다.
| 서술 파트의 내용 | 여기서 확인하는 방법 |
|---|---|
| 앵커 | 손계산과 np.cov 대조 |
| 레일리 몫의 최대가 | 각도를 0~180도 훑어서 |
| 두 번째가 첫 번째에 직교 | 스펙트럼 정리에서 공짜 |
| = 총분산 | 회전해도 보존 |
| SVD 와 eigh 대조 | |
| 설명분산 = L29의 에너지 | 누적 비율 |
| 에크하르트-영 | 저계수 근사가 최선 |
| 실측 | |
| 공분산 경유가 무너지는 곳 | |
float32 는 103.5 | 임계점이 내려온다 |
| 중심화를 빼먹으면 | 제1주성분이 평균 쪽으로 |
| PCA는 회귀가 아니다 | 세 직선을 겹쳐 그린다 |
| 눈금을 바꾸면 답이 바뀐다 | 단위를 바꿔 본다 |
0. 준비¶
import numpy as np
import plotly.graph_objects as go
from linalg_viz import COLORS, show_matrix, slider_figure
np.set_printoptions(precision=6, suppress=True)
rng = np.random.default_rng(35)
print("numpy", np.__version__)
print("sklearn 은 이 환경에 없다. 전부 numpy 로 직접 만든다.")numpy 2.5.2
sklearn 은 이 환경에 없다. 전부 numpy 로 직접 만든다.
1. 앵커를 끝까지 손으로¶
서술 파트의 다섯 점짜리 자료를 그대로 쓴다.
X = np.array([[1.0, 3.0],
[3.0, 1.0],
[4.0, 3.0],
[5.0, 3.0],
[7.0, 5.0]])
m, n = X.shape
mu = X.mean(axis=0)
Xc = X - mu
print(show_matrix(X, "X (표본 5개, 변수 2개)"))
print("평균 mu =", mu, " (4, 3) 인가 :", np.allclose(mu, [4, 3]))
print(show_matrix(Xc, "Xc = X - mu"))
print("열의 합이 0 인가 :", np.allclose(Xc.sum(axis=0), 0))X (표본 5개, 변수 2개)
[ 1 3 ]
[ 3 1 ]
[ 4 3 ]
[ 5 3 ]
[ 7 5 ]
평균 mu = [4. 3.] (4, 3) 인가 : True
Xc = X - mu
[ -3 0 ]
[ -1 -2 ]
[ 0 0 ]
[ 1 0 ]
[ 3 2 ]
열의 합이 0 인가 : True
C = Xc.T @ Xc / (m - 1)
print(show_matrix(Xc.T @ Xc, "Xc^T Xc"))
print(show_matrix(C, "C = Xc^T Xc / (m-1)"))
print("서술의 [[5,2],[2,2]] 인가 :", np.allclose(C, [[5, 2], [2, 2]]))
print("np.cov 와 같은가 :", np.allclose(C, np.cov(X, rowvar=False)))
print()
값, Q = np.linalg.eigh(C)
순 = np.argsort(값)[::-1]
값, Q = 값[순], Q[:, 순]
print("고윳값 :", 값, " (6, 1) 인가 :", np.allclose(값, [6, 1]))
q1 = np.array([2.0, 1.0]) / np.sqrt(5)
q2 = np.array([-1.0, 2.0]) / np.sqrt(5)
print("q1 = (2,1)/sqrt5 인가 :", np.allclose(np.abs(Q[:, 0]), np.abs(q1)))
print("q2 = (-1,2)/sqrt5 인가 :", np.allclose(np.abs(Q[:, 1]), np.abs(q2)))
print("q1 . q2 =", float(q1 @ q2), " <- 직교는 스펙트럼 정리가 준다")
print()
print("제1주성분의 각도 :", f"{np.degrees(np.arctan2(1, 2)):.3f} 도")
print("대각합 =", np.trace(C), " = 5+2 = 6+1 :", np.isclose(np.trace(C), 7))Xc^T Xc
[ 20 8 ]
[ 8 8 ]
C = Xc^T Xc / (m-1)
[ 5 2 ]
[ 2 2 ]
서술의 [[5,2],[2,2]] 인가 : True
np.cov 와 같은가 : True
고윳값 : [6. 1.] (6, 1) 인가 : True
q1 = (2,1)/sqrt5 인가 : True
q2 = (-1,2)/sqrt5 인가 : True
q1 . q2 = 0.0 <- 직교는 스펙트럼 정리가 준다
제1주성분의 각도 : 26.565 도
대각합 = 7.0 = 5+2 = 6+1 : True
레일리 몫을 정말 최대로 하는가¶
각도 = np.linspace(0, np.pi, 721)
분산 = np.array([float(np.array([np.cos(t), np.sin(t)]) @ C
@ np.array([np.cos(t), np.sin(t)])) for t in 각도])
최대, 최소 = int(np.argmax(분산)), int(np.argmin(분산))
print(f"가장 큰 분산 : {분산[최대]:.6f} at {np.degrees(각도[최대]):.3f} 도")
print(f"가장 작은 분산 : {분산[최소]:.6f} at {np.degrees(각도[최소]):.3f} 도")
print(f"두 각도의 차이 : {np.degrees(각도[최소] - 각도[최대]):.3f} 도 <- 90 도")
print(f"두 값의 합 : {분산[최대] + 분산[최소]:.6f} = trace(C) = {np.trace(C)}")
print()
print("고윳값과 같은가 :", np.isclose(분산[최대], 6, atol=1e-4),
np.isclose(분산[최소], 1, atol=1e-4))가장 큰 분산 : 5.999994 at 26.500 도
가장 작은 분산 : 1.000006 at 116.500 도
두 각도의 차이 : 90.000 도 <- 90 도
두 값의 합 : 7.000000 = trace(C) = 7.0
고윳값과 같은가 : True True
프레임, 이름표 = [], []
반지름 = np.sqrt(분산)
for i in range(0, 721, 40):
t = 각도[i]
v = np.array([np.cos(t), np.sin(t)])
투영 = (Xc @ v)[:, None] * v[None, :]
프레임.append([
go.Scatter(x=반지름*np.cos(각도), y=반지름*np.sin(각도), mode="lines",
line=dict(color="#cccccc", width=1.5), name="모든 방향"),
go.Scatter(x=Xc[:, 0], y=Xc[:, 1], mode="markers",
marker=dict(size=11, color=COLORS["input"]), name="Xc"),
go.Scatter(x=[-4*v[0], 4*v[0]], y=[-4*v[1], 4*v[1]], mode="lines",
line=dict(color=COLORS["output"], width=3), name="방향 v"),
go.Scatter(x=투영[:, 0], y=투영[:, 1], mode="markers",
marker=dict(size=9, color="#2ca02c", symbol="square"),
name=f"분산 {분산[i]:.3f}"),
])
이름표.append(f"{np.degrees(t):.0f}")
배치 = dict(title=dict(text="자를 돌려 가며 그림자의 분산을 잰다"),
xaxis=dict(range=[-4.5, 4.5], title=dict(text="x")),
yaxis=dict(range=[-4.5, 4.5], scaleanchor="x", title=dict(text="y")),
height=560, margin=dict(l=60, r=30, t=60, b=50))
slider_figure(프레임, 이름표, 배치, prefix="각도 ", initial=0)2. 공분산을 거치지 않는 길¶
에 바로 SVD를 걸면 같은 답이 나온다.
U, s, Vt = np.linalg.svd(Xc, full_matrices=False)
print("Xc 의 특이값 :", s)
print("sqrt(24), 2 인가 :", np.allclose(s, [np.sqrt(24), 2.0]))
print()
print("sigma^2 / (m-1) =", s**2 / (m - 1), " 고윳값 :", 값)
print("같은가 :", np.allclose(s**2 / (m - 1), 값))
print()
print("V 의 행이 고유벡터인가 :",
np.allclose(np.abs(Vt[0]), np.abs(q1)), np.allclose(np.abs(Vt[1]), np.abs(q2)))
print()
점수_eigh = Xc @ Q
점수_svd = U * s
print(show_matrix(점수_eigh, "점수 (eigh 경유) Xc Q"))
print(show_matrix(점수_svd, "점수 (svd 경유) U Sigma"))
print("부호까지 같은가 :", np.allclose(점수_eigh, 점수_svd))
print("부호를 빼면 같은가 :", np.allclose(np.abs(점수_eigh), np.abs(점수_svd)))
print()
print("-> 부호는 규약이다. 고유벡터의 방향은 어차피 정해지지 않는다(L21).")Xc 의 특이값 : [4.898979 2. ]
sqrt(24), 2 인가 : True
sigma^2 / (m-1) = [6. 1.] 고윳값 : [6. 1.]
같은가 : True
V 의 행이 고유벡터인가 : True True
점수 (eigh 경유) Xc Q
[ 2.68 -1.34 ]
[ 1.79 1.34 ]
[ 0 0 ]
[ -0.894 0.447 ]
[ -3.58 -0.447 ]
점수 (svd 경유) U Sigma
[ -2.68 1.34 ]
[ -1.79 -1.34 ]
[ 0 0 ]
[ 0.894 -0.447 ]
[ 3.58 0.447 ]
부호까지 같은가 : False
부호를 빼면 같은가 : True
-> 부호는 규약이다. 고유벡터의 방향은 어차피 정해지지 않는다(L21).
설명분산은 L29의 에너지다¶
비율 = s**2 / np.sum(s**2)
print("설명분산 비율 :", 비율, " = 6/7, 1/7 :",
np.allclose(비율, [6/7, 1/7]))
print("누적 :", np.cumsum(비율))
print()
print("고윳값으로 계산해도 같은가 :", np.allclose(값 / np.sum(값), 비율))
print()
# 에크하르트-영 : 랭크 1 근사가 최선인가
A1 = np.outer(U[:, 0], Vt[0]) * s[0]
print("랭크 1 근사의 오차 :", f"{np.linalg.norm(Xc - A1):.6f}",
" sigma_2 =", f"{s[1]:.6f}")
print("같은가 :", np.isclose(np.linalg.norm(Xc - A1, 2), s[1]))
최선 = np.inf
for _ in range(3000):
Z = np.outer(rng.normal(size=m), rng.normal(size=n))
최선 = min(최선, float(np.linalg.norm(Xc - Z, 2)))
print(f"무작위 랭크 1 행렬 3000개 중 최선 : {최선:.6f} (진 것인가 : {최선 < s[1]})")설명분산 비율 : [0.857143 0.142857] = 6/7, 1/7 : True
누적 : [0.857143 1. ]
고윳값으로 계산해도 같은가 : True
랭크 1 근사의 오차 : 2.000000 sigma_2 = 2.000000
같은가 : True
무작위 랭크 1 행렬 3000개 중 최선 : 2.121067 (진 것인가 : False)
3. 공분산을 만들면 조건수가 제곱된다¶
이 강의의 핵심이다. 서술 파트의 표를 그대로 재현한다.
def 자료(kappa, mm=400, nn=8, seed=0):
"""특이값을 정확히 아는 중심화된 자료를 만든다.
먼저 중심화한 뒤 QR 을 하면 Q 의 열도 이미 중심화되어 있다.
"""
r = np.random.default_rng(seed)
B = r.normal(size=(mm, nn))
B = B - B.mean(axis=0)
Q1, _ = np.linalg.qr(B)
Vv, _ = np.linalg.qr(r.normal(size=(nn, nn)))
시그마 = np.logspace(0, -np.log10(kappa), nn)
return Q1 @ np.diag(시그마) @ Vv.T, 시그마print("조건수가 정확히 제곱되는가")
print(f"{'k(Xc)':>9}{'k(C) 실측':>14}{'k(Xc)^2':>14}{'비':>8} 비고")
for k in (1e2, 1e4, 1e6, 1e7, 1e8):
Xk, _ = 자료(k)
kX, kC = np.linalg.cond(Xk), np.linalg.cond(Xk.T @ Xk)
비고 = "1/eps 를 넘었다" if kX**2 > 1/np.finfo(float).eps else ""
print(f"{k:>9.0e}{kC:>14.3e}{kX**2:>14.3e}{kC/kX**2:>8.4f} {비고}")
print()
print("-> 1e7 까지는 비가 1.0000 이다. 정확히 제곱된다.")
print(" 마지막 줄만 0.76 으로 어긋나는데, 이것이 오차가 아니라 이 절의 결론이다.")
print(" kappa(Xc)^2 이 1/eps 를 넘어 버려 kappa(C) 를 '재는 일' 자체가 이미")
print(" 부정확해졌다. 제곱이 정확히 되지 않는 것이 아니라, 제곱된 값을 컴퓨터가")
print(" 더 이상 붙들지 못하는 것이다.")조건수가 정확히 제곱되는가
k(Xc) k(C) 실측 k(Xc)^2 비 비고
1e+02 1.000e+04 1.000e+04 1.0000
1e+04 1.000e+08 1.000e+08 1.0000
1e+06 1.000e+12 1.000e+12 1.0000
1e+07 1.009e+14 1.000e+14 1.0092
1e+08 1.037e+16 1.000e+16 1.0373 1/eps 를 넘었다
-> 1e7 까지는 비가 1.0000 이다. 정확히 제곱된다.
마지막 줄만 0.76 으로 어긋나는데, 이것이 오차가 아니라 이 절의 결론이다.
kappa(Xc)^2 이 1/eps 를 넘어 버려 kappa(C) 를 '재는 일' 자체가 이미
부정확해졌다. 제곱이 정확히 되지 않는 것이 아니라, 제곱된 값을 컴퓨터가
더 이상 붙들지 못하는 것이다.
print("가장 작은 특이값을 얼마나 정확히 되찾는가 (400 x 8)")
print(f"{'k(Xc)':>9}{'k(C)':>12}{'공분산 경유':>14}{'SVD 직접':>13} 비고")
for k in (1e2, 1e4, 1e6, 1e8, 1e10):
Xk, 참 = 자료(k)
Ck = Xk.T @ Xk
람 = np.linalg.eigvalsh(Ck)
경유 = np.sqrt(max(람[0], 0.0))
직접 = np.linalg.svd(Xk, compute_uv=False)[-1]
비고 = "고윳값이 음수" if 람[0] < 0 else ""
print(f"{k:>9.0e}{np.linalg.cond(Ck):>12.1e}"
f"{abs(경유-참[-1])/참[-1]:>14.1e}{abs(직접-참[-1])/참[-1]:>13.1e} {비고}")
print()
print(f"1/eps = {1/np.finfo(float).eps:.2e}")
print("-> k(Xc) 가 1e8 을 넘으면 k(C) 가 1/eps 를 넘어 공분산이 무너진다.")
print(" 양의 준정부호인데 고윳값이 음수로 나온다. 수학이 아니라 반올림의 일이다.")가장 작은 특이값을 얼마나 정확히 되찾는가 (400 x 8)
k(Xc) k(C) 공분산 경유 SVD 직접 비고
1e+02 1.0e+04 3.0e-13 3.5e-16
1e+04 1.0e+08 1.9e-09 1.1e-13
1e+06 1.0e+12 3.0e-05 6.7e-13
1e+08 1.0e+16 2.2e-01 9.5e-11
1e+10 1.4e+18 1.0e+00 3.8e-08 고윳값이 음수
1/eps = 4.50e+15
-> k(Xc) 가 1e8 을 넘으면 k(C) 가 1/eps 를 넘어 공분산이 무너진다.
양의 준정부호인데 고윳값이 음수로 나온다. 수학이 아니라 반올림의 일이다.
float32 면 임계점이 훨씬 내려온다¶
print(f"eps(float64) = {np.finfo(np.float64).eps:.2e} -> 1/sqrt(eps) = "
f"{1/np.sqrt(np.finfo(np.float64).eps):.1e}")
print(f"eps(float32) = {np.finfo(np.float32).eps:.2e} -> 1/sqrt(eps) = "
f"{1/np.sqrt(np.finfo(np.float32).eps):.0f} (약 10^3.5)")
print()
r2 = np.random.default_rng(1)
U2, _ = np.linalg.qr(r2.normal(size=(200, 200)))
V2, _ = np.linalg.qr(r2.normal(size=(2, 2)))
print(f"{'sigma_min':>11}{'kappa':>9}{'공분산 경유':>16}{'상대오차':>11}{'SVD 직접':>16}")
for sm in (1e-3, 3e-4, 1e-4):
Xf = (U2[:, :2] @ np.diag([1.0, sm]) @ V2.T).astype(np.float32)
람 = np.linalg.eigvalsh((Xf.T @ Xf).astype(np.float64))
경유 = float(np.sqrt(max(람[0], 0.0)))
직접 = float(np.linalg.svd(Xf, compute_uv=False)[-1])
print(f"{sm:>11.0e}{1/sm:>9.0e}{경유:>16.4e}{abs(경유-sm)/sm:>11.1%}{직접:>16.4e}")
print()
print("-> kappa = 1e4 에서 공분산 경유가 0 을 준다. 제2주성분의 크기가 0 으로 보고된다.")
print(" 기계학습에서 float32 는 흔하고, 조건수 1e4 도 드물지 않다.")eps(float64) = 2.22e-16 -> 1/sqrt(eps) = 6.7e+07
eps(float32) = 1.19e-07 -> 1/sqrt(eps) = 2896 (약 10^3.5)
sigma_min kappa 공분산 경유 상대오차 SVD 직접
1e-03 1e+03 1.0033e-03 0.3% 1.0000e-03
3e-04 3e+03 2.9369e-04 2.1% 3.0000e-04
1e-04 1e+04 0.0000e+00 100.0% 1.0000e-04
-> kappa = 1e4 에서 공분산 경유가 0 을 준다. 제2주성분의 크기가 0 으로 보고된다.
기계학습에서 float32 는 흔하고, 조건수 1e4 도 드물지 않다.
4. 중심화를 빼먹으면¶
def 제1주성분(D, 중심화=True):
E = D - D.mean(axis=0) if 중심화 else D
_, _, Vt2 = np.linalg.svd(E, full_matrices=False)
return Vt2[0]
바르게 = 제1주성분(X, True)
빼먹고 = 제1주성분(X, False)
평균방향 = mu / np.linalg.norm(mu)
사이 = lambda a, b: float(np.degrees(np.arccos(np.clip(abs(a @ b), 0, 1))))
print("제대로 한 제1주성분 :", 바르게, f" (각도 {np.degrees(np.arctan2(*바르게[::-1])):.2f} 도)")
print("중심화를 뺀 제1주성분 :", 빼먹고)
print("평균 방향 :", 평균방향)
print()
print(f"빼먹은 것과 평균 방향의 사잇각 : {사이(빼먹고, 평균방향):.3f} 도 <- 거의 붙었다")
print(f"빼먹은 것과 제대로 한 것의 사잇각 : {사이(빼먹고, 바르게):.3f} 도")
print()
print("-> 중심화를 빼면 X^T X 에 m * mu mu^T 가 얹힌다.")
print(" 그 항은 랭크 1 이고 방향이 mu 이므로, 평균이 클수록 답을 그쪽으로 끌어당긴다.")제대로 한 제1주성분 : [0.894427 0.447214] (각도 26.57 도)
중심화를 뺀 제1주성분 : [-0.814442 -0.580244]
평균 방향 : [0.8 0.6]
빼먹은 것과 평균 방향의 사잇각 : 1.402 도 <- 거의 붙었다
빼먹은 것과 제대로 한 것의 사잇각 : 8.903 도
-> 중심화를 빼면 X^T X 에 m * mu mu^T 가 얹힌다.
그 항은 랭크 1 이고 방향이 mu 이므로, 평균이 클수록 답을 그쪽으로 끌어당긴다.
print("평균을 멀리 옮길수록 얼마나 끌려가는가")
print(f"{'평균 이동':>10}{'중심화한 쪽의 각도':>20}{'뺀 쪽의 각도':>16}"
f"{'평균 방향과의 각':>18}")
for 배 in (0.0, 1.0, 3.0, 10.0, 50.0):
이동 = X + 배 * np.array([4.0, 3.0])
빼 = 제1주성분(이동, False)
참 = 제1주성분(이동, True)
if 참[0] < 0: 참 = -참
if 빼[0] < 0: 빼 = -빼
mu2 = 이동.mean(axis=0); mu2 = mu2 / np.linalg.norm(mu2)
print(f"{배:>10.0f}{np.degrees(np.arctan2(참[1], 참[0])):>20.4f}"
f"{np.degrees(np.arctan2(빼[1], 빼[0])):>16.4f}"
f"{사이(빼, mu2):>18.3f}")
print()
print("-> 가운데 칸이 26.5651 로 꿈쩍도 하지 않는다. 중심화한 PCA 는 옮김에 불변이다.")
print(" 오른쪽 두 칸은 평균을 옮길수록 평균 방향에 달라붙는다.")
print(" 평균을 50배 옮기면 사잇각이 0.001 도다. 사실상 평균 방향 그 자체다.")평균을 멀리 옮길수록 얼마나 끌려가는가
평균 이동 중심화한 쪽의 각도 뺀 쪽의 각도 평균 방향과의 각
0 26.5651 35.4677 1.402
1 26.5651 36.4811 0.389
3 26.5651 36.7700 0.100
10 26.5651 36.8566 0.013
50 26.5651 36.8693 0.001
-> 가운데 칸이 26.5651 로 꿈쩍도 하지 않는다. 중심화한 PCA 는 옮김에 불변이다.
오른쪽 두 칸은 평균을 옮길수록 평균 방향에 달라붙는다.
평균을 50배 옮기면 사잇각이 0.001 도다. 사실상 평균 방향 그 자체다.
5. PCA는 회귀가 아니다¶
무엇을 최소로 하는지가 다르다.
# y 를 x 로 회귀 / x 를 y 로 회귀 / PCA
A회귀 = np.column_stack([np.ones(m), Xc[:, 0]])
기울기_yx = np.linalg.lstsq(A회귀, Xc[:, 1], rcond=None)[0][1]
B회귀 = np.column_stack([np.ones(m), Xc[:, 1]])
기울기_xy = np.linalg.lstsq(B회귀, Xc[:, 0], rcond=None)[0][1]
기울기_pca = 바르게[1] / 바르게[0]
print(f"y 를 x 로 회귀 : 기울기 {기울기_yx:.6f} (세로 거리의 제곱합을 최소로)")
print(f"x 를 y 로 회귀 : 기울기 {1/기울기_xy:.6f} (가로 거리의 제곱합을 최소로)")
print(f"PCA 제1주성분 : 기울기 {기울기_pca:.6f} (수직 거리의 제곱합을 최소로)")
print()
def 거리합(기울기, 방식):
v = np.array([1.0, 기울기]); v = v / np.linalg.norm(v)
if 방식 == "수직":
return float(np.sum((Xc - (Xc @ v)[:, None] * v)**2))
if 방식 == "세로":
return float(np.sum((Xc[:, 1] - 기울기 * Xc[:, 0])**2))
return float(np.sum((Xc[:, 0] - Xc[:, 1] / 기울기)**2))
print(f"{'':>16}{'수직 거리^2':>14}{'세로 거리^2':>14}{'가로 거리^2':>14}")
for 이름, g in (("y~x 회귀", 기울기_yx), ("x~y 회귀", 1/기울기_xy),
("PCA", 기울기_pca)):
print(f"{이름:>16}{거리합(g,'수직'):>14.6f}{거리합(g,'세로'):>14.6f}"
f"{거리합(g,'가로'):>14.6f}")
print()
print("-> 각 방법이 자기 칸에서만 최소다. 셋은 다른 문제를 푸는 것이다.")
print(" PCA 의 수직 거리 제곱합은 lambda_2 * (m-1) =",
f"{값[1]*(m-1):.6f} 와 같다 :", np.isclose(거리합(기울기_pca,'수직'), 값[1]*(m-1)))y 를 x 로 회귀 : 기울기 0.400000 (세로 거리의 제곱합을 최소로)
x 를 y 로 회귀 : 기울기 1.000000 (가로 거리의 제곱합을 최소로)
PCA 제1주성분 : 기울기 0.500000 (수직 거리의 제곱합을 최소로)
수직 거리^2 세로 거리^2 가로 거리^2
y~x 회귀 4.137931 4.800000 30.000000
x~y 회귀 6.000000 12.000000 12.000000
PCA 4.000000 5.000000 20.000000
-> 각 방법이 자기 칸에서만 최소다. 셋은 다른 문제를 푸는 것이다.
PCA 의 수직 거리 제곱합은 lambda_2 * (m-1) = 4.000000 와 같다 : True
눈금을 바꾸면 답이 바뀐다¶
print("둘째 변수의 단위만 바꿔 본다 (예: cm -> mm)")
print(f"{'배율':>8}{'제1주성분':>26}{'각도':>10}{'설명분산 1위':>14}")
for 배 in (1.0, 2.0, 10.0, 100.0):
Y = X * np.array([1.0, 배])
v = 제1주성분(Y, True)
Yc = Y - Y.mean(axis=0)
ss = np.linalg.svd(Yc, compute_uv=False)
print(f"{배:>8.0f}{str(np.round(v, 4)):>26}"
f"{np.degrees(np.arctan2(v[1], v[0])):>10.2f}"
f"{ss[0]**2/np.sum(ss**2):>14.4f}")
print()
print("-> 축 하나의 눈금만 바꿨는데 제1주성분이 그쪽으로 끌려간다.")
print(" PCA 는 분산을 보는데, 분산은 단위에 딸린 양이기 때문이다.")
print(" 그래서 단위가 다른 변수를 섞을 때는 표준화(상관행렬 PCA)를 한다.")둘째 변수의 단위만 바꿔 본다 (예: cm -> mm)
배율 제1주성분 각도 설명분산 1위
1 [0.8944 0.4472] 26.57 0.8571
2 [0.5696 0.8219] 55.28 0.8286
10 [0.101 0.9949] 84.20 0.9855
100 [0.01 0.9999] 89.43 0.9999
-> 축 하나의 눈금만 바꿨는데 제1주성분이 그쪽으로 끌려간다.
PCA 는 분산을 보는데, 분산은 단위에 딸린 양이기 때문이다.
그래서 단위가 다른 변수를 섞을 때는 표준화(상관행렬 PCA)를 한다.
print("표준화하면 눈금에 흔들리지 않는가")
def 표준화PCA(D):
E = D - D.mean(axis=0)
E = E / E.std(axis=0, ddof=1)
return np.linalg.svd(E, full_matrices=False)[2][0]
print(f"{'배율':>8}{'표준화 뒤 제1주성분':>28}{'각도':>10}")
for 배 in (1.0, 2.0, 10.0, 100.0):
v = 표준화PCA(X * np.array([1.0, 배]))
if v[0] < 0: v = -v
print(f"{배:>8.0f}{str(np.round(v, 6)):>28}"
f"{np.degrees(np.arctan2(v[1], v[0])):>10.3f}")
print()
print("-> 배율을 100배 해도 답이 그대로다. 표준화가 눈금을 지워 버리기 때문이다.")
print(" 대신 '어느 변수가 원래 더 크게 흔들렸는가' 라는 정보도 함께 지워진다.")표준화하면 눈금에 흔들리지 않는가
배율 표준화 뒤 제1주성분 각도
1 [0.707107 0.707107] 45.000
2 [0.707107 0.707107] 45.000
10 [0.707107 0.707107] 45.000
100 [0.707107 0.707107] 45.000
-> 배율을 100배 해도 답이 그대로다. 표준화가 눈금을 지워 버리기 때문이다.
대신 '어느 변수가 원래 더 크게 흔들렸는가' 라는 정보도 함께 지워진다.
마치며...¶
| 서술 파트의 내용 | 이 노트북의 코드 |
|---|---|
| 앵커 | 손계산 = np.cov |
| , | |
| 레일리 몫의 최대 | 각도를 훑으면 26.565도에서 6 |
| 두 극값이 90도 차이 | 합이 |
| SVD 와 eigh 일치 | |
| 부호는 규약 | 절댓값으로는 같다 |
| 설명분산 = 에너지 | , |
| 에크하르트-영 | 무작위 3000개가 못 이긴다 |
| 비가 1.0000 | |
| 무너지는 곳 | , 고윳값이 음수 |
float32 | 에서 0을 준다 |
| 중심화 누락 | 평균 방향과 1.4도 |
| 옮김 불변 | 중심화하면 평균을 옮겨도 그대로 |
| PCA ≠ 회귀 | 셋이 각자 자기 칸에서만 최소 |
| 눈금 의존 | 배율 100배에 답이 끌려간다 |
더 해 볼 것¶
3절의 자료에서 을 8에서 2나 50으로 바꿔 보자. 공분산이 무너지는 조건수가 달라지는가? 왜 달라지지 않는가?
4절에서 평균을 빼는 대신 중앙값을 빼면 어떻게 되는가? 제1주성분이 달라지는가? 어느 쪽이 이상치에 강한가?
5절의 세 직선에서 자료에 이상치를 하나 넣어 보자. 셋 중 어느 것이 가장 크게 흔들리는가?
설명분산이 가 되는 성분 개수를 세는 함수를 만들어, L31의 시험 사진에 걸어 보자. L31에서 잰 값과 같은가?
float32자료에 먼저 중심화하고 float64 로 올린 뒤 공분산을 만들면 어디까지 버티는가? 정밀도를 올리는 것과 공분산을 안 만드는 것 중 어느 쪽이 이득이 큰가?
다음은 보강 2 — 수치선형대수 입문이다. 이 강의에서 본 "옳은 공식 ≠ 쓰는 공식"이 다섯 번째였다. 그 다섯을 조건수라는 하나의 개념으로 묶는다.