수학

MATH / 중급 16번

저계수 근사와 에카르트-영: LoRA의 r을 스펙트럼으로 고르기

상위 k개 특잇값만 남긴 근사가 프로베니우스·스펙트럼 노름 모두에서 최적이라는 에카르트-영 정리를 세우고, 잘라 낸 오차가 남은 특잇값으로 그대로 계산된다는 것을 손으로 확인합니다. ΔW = BA라는 LoRA의 형태를 계수 제약으로 다시 읽고, r을 감이 아니라 실제 업데이트의 특잇값 분포에서 고르는 절차와 α/r의 뜻까지 갑니다.

PALDYN Team20 MIN READ

LoRA 설정 파일에서 가장 자주 손대는 값이 r입니다. 4인지 8인지 64인지에 따라 학습되는 파라미터 수가 열여섯 배씩 갈리는데, 정하는 근거는 대개 «남들이 8을 쓰더라» 정도입니다. 올려 보고 성능이 안 오르면 내리는 식으로 몇 번 돌려 보는 것 말고는 방법이 없어 보입니다.

그런데 이 값은 감으로 고를 필요가 없습니다. r은 랭크이고, 랭크를 몇으로 잡아야 원본을 얼마나 잃는지는 계산되는 양이기 때문입니다.

지난 글에서 특잇값을 큰 것부터 늘어놓고 «뒤쪽은 잡음이다»라고 읽었습니다. 이 글은 뒤쪽을 실제로 잘라 버리고, 그렇게 만든 근사가 얼마나 좋은지를 정확한 수로 답합니다.

특잇값 분해를 층으로 다시 쓰기

A=UΣV⊤A = U\Sigma V^\top 을 성분끼리 곱해 펼치면 다음 꼴이 됩니다.

A=σ1u1v1⊤+σ2u2v2⊤+⋯+σrurvr⊤A = \sigma_1\mathbf{u}_1\mathbf{v}_1^\top + \sigma_2\mathbf{u}_2\mathbf{v}_2^\top + \cdots + \sigma_r\mathbf{u}_r\mathbf{v}_r^\top

각 항의 uivi⊤\mathbf{u}_i\mathbf{v}_i^\top 는 열벡터에 행벡터를 곱한 것이라 외적입니다 — (m×1)(1×n)=m×n(m\times1)(1\times n) = m\times n 으로 원래 크기의 행렬이 나오고, 모든 행이 vi⊤\mathbf{v}_i^\top 의 상수배라 랭크가 1입니다.

즉 어떤 행렬이든 랭크 1짜리 층을 겹쳐 쌓은 것으로 볼 수 있고, 각 층의 무게가 σi\sigma_i 입니다. 특잇값을 큰 것부터 늘어놓았으니 앞의 층이 두껍고 뒤로 갈수록 얇습니다.

여기서 앞의 kk 개만 남기고 나머지를 버린 것을 잘라 낸 특잇값 분해라 하고 AkA_k 로 적습니다.

Ak=∑i=1kσiuivi⊤A_k = \sum_{i=1}^{k}\sigma_i\mathbf{u}_i\mathbf{v}_i^\top

랭크 1짜리 kk 개의 합이므로 AkA_k 의 랭크는 kk 이하입니다. 이렇게 원래 행렬을 더 낮은 랭크의 행렬로 대신하는 것을 저계수 근사라고 합니다.

특잇값 분해를 랭크 1 층의 합으로 쌓고 뒤쪽을 잘라 내는 그림

하필 앞에서 자르는 것이 최적인가

«큰 것부터 남긴다»는 자연스러워 보이지만, 자연스러움이 최적성의 증거는 아닙니다. 랭크 kk 짜리 행렬은 무수히 많고, 그중 AA 에 가장 가까운 것이 하필 AkA_k 라는 보장은 어디에도 없습니다.

그런데 보장이 있습니다.

에카르트-영 정리. 랭크가 kk 이하인 모든 행렬 XX 에 대해

∥A−Ak∥≤∥A−X∥\|A - A_k\| \le \|A - X\|

가 프로베니우스 노름과 스펙트럼 노름 모두에서 성립한다.

두 노름이 무엇인지 여기서 한 번 짚고 갑니다. 프로베니우스 노름은 모든 성분을 제곱해 더하고 제곱근을 씌운 값 ∥A∥F=∑i,jaij2\|A\|_F = \sqrt{\sum_{i,j}a_{ij}^2} 으로, 행렬을 길게 편 벡터의 길이입니다. 스펙트럼 노름은 가장 큰 특잇값 ∥A∥2=σ1\|A\|_2 = \sigma_1 로, 어떤 방향이 가장 크게 늘어나는지를 재는 값입니다. 둘의 성질과 관계는 「중급 17번 · 행렬 노름」이 맡고, 여기서는 «행렬 사이의 거리를 재는 두 자»로만 씁니다.

정리가 말하는 것은 강합니다. 두 자로 재도 같은 답이 최적이고, «랭크 kk 로 줄인다»는 문제는 특잇값 분해 한 번으로 완전히 풀립니다. 최적화를 돌릴 필요도, 후보를 비교할 필요도 없습니다.

랭크 k 이하인 행렬들 가운데 A_k가 A에 가장 가깝다는 도식

잃는 양은 남은 특잇값이 그대로 말한다

최적이라는 것보다 실무에 더 쓸모 있는 것은 얼마나 잃는지가 계산된다는 쪽입니다. A−AkA - A_k 는 잘라 낸 층들의 합이므로

A−Ak=∑i=k+1rσiuivi⊤A - A_k = \sum_{i=k+1}^{r}\sigma_i\mathbf{u}_i\mathbf{v}_i^\top

이고, 이것 자체가 특잇값 σk+1,…,σr\sigma_{k+1}, \dots, \sigma_r 을 가진 행렬의 특잇값 분해입니다. 그러므로 두 노름의 값이 바로 읽힙니다.

∥A−Ak∥F=σk+12+⋯+σr2,∥A−Ak∥2=σk+1\|A - A_k\|_F = \sqrt{\sigma_{k+1}^2 + \cdots + \sigma_r^2}, \qquad \|A - A_k\|_2 = \sigma_{k+1}

앞의 것은 «버린 것 전체의 크기»이고 뒤의 것은 «버린 것 중 가장 큰 하나»입니다. 특잇값 목록만 있으면 kk 를 바꿔 가며 오차를 표로 뽑을 수 있습니다 — 근사를 실제로 만들어 보지 않고도 그렇습니다.

특잇값이 10,6,3,110, 6, 3, 1 인 행렬로 표를 채워 봅니다. 제곱합이 100+36+9+1=146100+36+9+1 = 146 입니다.

kk 남긴 것 ∥A−Ak∥F\|A-A_k\|_F ∥A−Ak∥2\|A-A_k\|_2 담긴 에너지
1 10 36+9+1=6.78\sqrt{36+9+1} = 6.78 6 68.5%
2 10, 6 9+1=3.16\sqrt{9+1} = 3.16 3 93.2%
3 10, 6, 3 1=1\sqrt{1} = 1 1 99.3%
4 전부 0 0 100%

마지막 열의 에너지는 ∑i≤kσi2/∑iσi2\sum_{i\le k}\sigma_i^2 \big/ \sum_i\sigma_i^2 로, 프로베니우스 노름의 제곱을 얼마나 채웠는지를 백분율로 적은 값입니다. 지난 글의 수치 랭크에서 쓴 것과 같은 양입니다.

이 표에서 k=2k=2 가 눈에 띕니다. 랭크를 절반으로 줄였는데 93%를 지켰습니다. 특잇값이 빨리 떨어질수록 이 거래가 유리해지고, 그 감소 속도를 보고 kk 를 정하는 것이 이 글의 목표입니다.

손으로 한 번

B=(3113)B = \begin{pmatrix} 3 & 1 \\ 1 & 3\end{pmatrix} 의 랭크 1 최적 근사를 구합니다. 대칭 행렬이라 지난 글에서 본 대로 특잇값이 고윳값의 절댓값과 같습니다. tr⁡=6\operatorname{tr} = 6, det⁡=8\det = 8 이므로 λ=4,2\lambda = 4, 2 이고 σ1=4\sigma_1 = 4, σ2=2\sigma_2 = 2 입니다. 고유벡터는 (1,1)(1,1) 과 (1,−1)(1,-1) 이므로

u1=v1=12(1,1),u1v1⊤=12(1111)\mathbf{u}_1 = \mathbf{v}_1 = \tfrac{1}{\sqrt2}(1,1), \qquad \mathbf{u}_1\mathbf{v}_1^\top = \frac{1}{2}\begin{pmatrix} 1 & 1 \\ 1 & 1\end{pmatrix}

B1=σ1u1v1⊤=4⋅12(1111)=(2222)B_1 = \sigma_1\mathbf{u}_1\mathbf{v}_1^\top = 4 \cdot \frac{1}{2}\begin{pmatrix} 1 & 1 \\ 1 & 1\end{pmatrix} = \begin{pmatrix} 2 & 2 \\ 2 & 2\end{pmatrix}

오차를 직접 계산합니다.

B−B1=(1−1−11),∥B−B1∥F=1+1+1+1=2B - B_1 = \begin{pmatrix} 1 & -1 \\ -1 & 1\end{pmatrix}, \qquad \|B-B_1\|_F = \sqrt{1+1+1+1} = 2

공식이 예고한 σ2=2\sigma_2 = 2 와 정확히 같습니다.

최적이라는 말이 실제로 무슨 뜻인지 다른 랭크 1 후보와 대 봅니다.

후보 행렬 랭크 ∥B−X∥F\|B - X\|_F
잘라 낸 SVD B1B_1 열 (2,2),(2,2)(2,2),(2,2) 1 2
첫 행만 남기기 열 (3,0),(1,0)(3,0),(1,0) 1 3.16
첫 열만 남기기 열 (3,1),(0,0)(3,1),(0,0) 1 3.16

«원본에서 눈에 띄는 부분을 남긴다»는 직관적인 두 방법이 SVD보다 1.5배 넘게 나쁩니다. 덜어낼 것을 성분 단위로 고르면 안 되고 축 단위로 골라야 한다는 것이 이 표의 내용입니다.

LoRA는 계수 제약이다

이제 첫머리로 갑니다. 사전학습된 가중치 W0W_0 를 파인튜닝하면 그 자리에 W0+ΔWW_0 + \Delta W 가 들어섭니다. 문제는 ΔW\Delta W 가 W0W_0 와 같은 크기 — d×kd \times k — 라 저장할 것도 학습할 것도 그대로라는 점입니다.

LoRA는 여기에 조건을 하나 겁니다.

ΔW=BA,B∈Rd×r, A∈Rr×k\Delta W = BA, \qquad B \in \mathbb{R}^{d\times r},\ A \in \mathbb{R}^{r\times k}

rr 을 작게 잡으면 ΔW\Delta W 의 랭크가 rr 이하로 강제됩니다. 곱의 랭크는 두 인수의 랭크를 넘을 수 없고 AA 의 랭크가 최대 rr 이기 때문입니다. 앞 절의 말로 하면 업데이트를 랭크 1짜리 층 rr 개의 합으로만 표현하겠다는 선언입니다.

파라미터 수가 얼마나 줄어드는지는 곱셈 두 번이면 나옵니다. d=k=4096d = k = 4096 일 때

파라미터 수 비율
전체 ΔW\Delta W 4096×4096=16,777,2164096 \times 4096 = 16{,}777{,}216 100%
r=8r = 8 8×(4096+4096)=65,5368 \times (4096 + 4096) = 65{,}536 0.39%
r=64r = 64 64×(4096+4096)=524,28864 \times (4096 + 4096) = 524{,}288 3.13%

W와 같은 크기의 ΔW를 d×r과 r×k 두 조각으로 대신하는 모양과 파라미터 수 비교

여기서 에카르트-영이 하는 말이 분명해집니다. 랭크 rr 로 표현할 수 있는 최선이 ΔW\Delta W 의 상위 rr 개 층이고, 잃는 것은 나머지 특잇값의 제곱합입니다. LoRA가 통한다는 것은 «파인튜닝이 실제로 만드는 업데이트의 특잇값이 빨리 떨어진다»는 경험적 사실과 같은 말입니다.

주의할 것이 하나 있습니다. 낮은 랭크로 잘 근사되는 것은 ΔW\Delta W 이지 W0W_0 가 아닙니다. 사전학습된 가중치 자체의 특잇값은 대체로 완만하게 떨어져서, 90%를 채우는 데 절반 이상의 축이 필요한 것이 보통입니다. 두 행렬을 섞어 보면 판단이 어긋납니다.

r을 스펙트럼으로 고르는 절차

그러므로 근거로 삼을 것은 ΔW\Delta W 의 특잇값 분포입니다. 그런데 ΔW\Delta W 를 알려면 파인튜닝을 이미 해 봤어야 한다는 순환이 있으니, 실무에서는 이렇게 끊습니다.

  1. 한 번은 넉넉하게 돌린다. 전체 파인튜닝을 짧게 하거나, LoRA를 일부러 큰 rr — 이를테면 64 — 로 한 에폭 돌립니다.
  2. ΔW=Wft−W0\Delta W = W_{\text{ft}} - W_0 를 층마다 꺼내 특잇값을 구한다.
  3. 누적 에너지 곡선을 그리고 목표 비율을 채우는 kk 를 읽는다. 90%나 95%가 흔한 기준입니다.
  4. 층마다 나온 kk 중 큰 쪽에 맞춰 rr 을 정한다. 층마다 다르게 줄 수 있으면 그대로 쓰고, 하나로 정해야 하면 여유를 둡니다.

실제로 감쇠하는 ΔW\Delta W 하나를 만들어 재 보면 이렇습니다.

2.004, 1.444, 1.041, 0.751, 0.542, 0.391, 0.283, 0.205, …2.004,\ 1.444,\ 1.041,\ 0.751,\ 0.542,\ 0.391,\ 0.283,\ 0.205,\ \dots

목표 에너지 필요한 kk
90% 4
95% 5
99% 8

90%에서 95%로 올리는 데는 축 하나면 되는데 99%까지 가려면 셋이 더 듭니다. 뒤쪽 축의 값이 작아 에너지를 조금씩만 보태기 때문이고, 이 «올라가는 비용»이 어디서 급해지는지가 rr 을 정하는 자리입니다.

ΔW의 특잇값과 누적 에너지 곡선 위에 90·95·99% 기준선을 그은 그림

곡선이 완만하면 그 층은 저계수와 안 맞는다는 신호입니다. 90%를 채우는 데 절반 이상의 축이 필요하면 LoRA로 줄여 봐야 잃는 것이 많으니, 그 층은 전체 파인튜닝으로 두거나 다른 방법을 씁니다. 감으로 r을 올리는 대신 이 곡선을 보고 결정한다는 것이 이 절의 전부입니다.

α/r 은 무엇을 하는가

LoRA 구현에서 실제로 더해지는 것은 BABA 가 아니라 αrBA\frac{\alpha}{r}BA 입니다. 앞의 상수는 학습되지 않고 설정으로 고정됩니다.

이 상수의 역할은 앞 절과 이어집니다. BABA 는 랭크 1짜리 층 rr 개의 합이므로, 각 층이 비슷한 크기라면 rr 을 키울수록 합 전체가 커집니다. rr 로 나누면 합이 아니라 평균이 되어 rr 을 바꿔도 업데이트의 크기가 크게 흔들리지 않습니다. 그래서 rr 을 8에서 32로 올릴 때 학습률을 다시 잡지 않아도 되게 하려는 것이 이 나눗셈의 목적입니다.

α\alpha 는 그 위에 얹는 배율입니다. 실질적으로 어댑터에만 걸리는 학습률 배수처럼 작동하므로, 실무에서는 두 값을 따로 보지 말고 비 α/r\alpha/r 을 하나의 값으로 보는 편이 낫습니다.

  • rr 을 두 배로 올리면서 α\alpha 를 그대로 두면 α/r\alpha/r 이 절반이 됩니다 — 표현력은 늘렸는데 업데이트 세기는 줄인 것이라 두 변화가 섞여 원인을 못 가립니다.
  • α=2r\alpha = 2r 처럼 함께 올려 α/r\alpha/r 을 고정하면 바뀐 것이 랭크 하나뿐이 되어, 앞 절의 스펙트럼 실험과 결과를 맞대 볼 수 있습니다.

코드로 확인하기

import numpy as np

B = np.array([[3., 1], [1, 3]])
U, s, Vt = np.linalg.svd(B)
print(s)                                            # [4. 2.]

B1 = s[0] * np.outer(U[:, 0], Vt[0])                # 상위 1개 층만
print(B1)                                           # [[2. 2.] [2. 2.]]
print(np.linalg.norm(B - B1, 'fro'), s[1])          # 2.0 2.0  — 공식과 일치

다른 랭크 1 후보가 정말 더 나쁜지도 바로 확인됩니다.

for name, X in [("SVD",   B1),
                ("첫 행", np.array([[3., 1], [0, 0]])),
                ("첫 열", np.array([[3., 0], [1, 0]]))]:
    print(name, np.linalg.matrix_rank(X), round(np.linalg.norm(B - X, 'fro'), 4))
# SVD   1 2.0
# 첫 행 1 3.1623
# 첫 열 1 3.1623

rr 을 고르는 절차는 함수 하나로 씁니다.

def rank_for_energy(delta_w, target=0.95):
    s = np.linalg.svd(delta_w, compute_uv=False)
    energy = np.cumsum(s ** 2) / (s ** 2).sum()
    return int((energy < target).sum() + 1), s

rng = np.random.default_rng(5)
U, _ = np.linalg.qr(rng.standard_normal((256, 256)))
V, _ = np.linalg.qr(rng.standard_normal((256, 256)))
dW = U @ np.diag(2.0 * 0.72 ** np.arange(256) + 0.004) @ V.T   # 감쇠하는 ΔW

for t in (0.90, 0.95, 0.99):
    k, s = rank_for_energy(dW, t)
    print(t, k)
# 0.9 4  /  0.95 5  /  0.99 8

잘라 낸 근사를 실제로 만들어 오차 공식과 맞대 볼 수도 있습니다.

U2, s2, Vt2 = np.linalg.svd(dW)
for k in (4, 8, 16):
    approx = (U2[:, :k] * s2[:k]) @ Vt2[:k]
    print(k,
          round(np.linalg.norm(dW - approx, 'fro'), 6),
          round(np.sqrt((s2[k:] ** 2).sum()), 6),      # 프로베니우스 예측
          round(s2[k], 6))                             # 스펙트럼 예측
# 4  0.786909 0.786909 0.541477
# 8  0.22675  0.22675  0.148441

가운데 두 열이 소수점 여섯 자리까지 같습니다 — 근사를 만들어 재나 특잇값으로 계산하나 답이 하나입니다.

정리

  • 특잇값 분해는 랭크 1짜리 층의 합입니다. A=∑σiuivi⊤A = \sum\sigma_i\mathbf{u}_i\mathbf{v}_i^\top 이고 각 층의 무게가 σi\sigma_i 입니다.
  • 잘라 낸 SVD AkA_k 는 랭크 kk 이하인 모든 행렬 중 AA 에 가장 가깝습니다(에카르트-영). 프로베니우스 노름과 스펙트럼 노름 어느 자로 재도 그렇습니다.
  • 잃는 양이 공식으로 나옵니다. ∥A−Ak∥F=∑i>kσi2\|A-A_k\|_F = \sqrt{\sum_{i>k}\sigma_i^2}, ∥A−Ak∥2=σk+1\|A-A_k\|_2 = \sigma_{k+1} 이라 특잇값 목록만으로 표를 뽑을 수 있습니다.
  • 성분 단위로 덜어내면 안 됩니다. 행이나 열을 지우는 방식은 같은 랭크에서 1.5배 넘게 나빴습니다. 잘라야 할 것은 축입니다.
  • LoRA의 ΔW=BA\Delta W = BA 는 랭크 rr 제약이고, 4096×40964096\times4096 층에서 r=8r=8 이면 파라미터가 0.39%로 줍니다.
  • 낮은 랭크로 근사되는 것은 ΔW\Delta W 이지 W0W_0 가 아닙니다. 근거를 볼 때 둘을 섞으면 판단이 어긋납니다.
  • rr 은 누적 에너지 곡선에서 읽습니다. 큰 rr 로 한 번 돌려 ΔW\Delta W 를 얻고, 목표 비율을 채우는 kk 를 층마다 재고, 곡선이 완만한 층은 저계수와 안 맞는 층으로 판단합니다.
  • α/r\alpha/r 은 함께 봅니다. rr 로 나누는 것은 층의 합을 평균으로 바꿔 랭크를 바꿔도 업데이트 세기가 유지되게 하려는 것이므로, 비교 실험에서는 그 비를 고정합니다.

이 글에서 두 노름을 «행렬 사이의 거리를 재는 자»로 쓰기만 하고 성질은 미뤄 두었습니다. 다음 글이 그 빚을 갚습니다. ∥A∥2=σ1\|A\|_2 = \sigma_1 이 왜 그런지, 프로베니우스 노름의 제곱이 왜 특잇값 제곱합인지, 그리고 그 값이 층을 통과할 때 신호가 얼마나 커질 수 있는지를 정하는 립시츠 상수와 어떻게 같은 것인지입니다.


읽어주셔서 감사합니다. 😊

LATEST

수학의 최신 글

수학2026.09.07

양자화 오차: 격자 사상, 오차 분산, 이상치 채널

실수를 2^b개 격자에 사상할 때 오차의 분산이 왜 Δ²/12인지 유도하고, 그것이 비트당 6.02dB라는 SNR로 번역되는 과정을 실측과 대조했습니다. 이상치 하나가 나머지 값의 유효 비트를 어떻게 먹는지, 그리고 int4에서 성능이 무너지는 지점을 오차 예산으로 미리 계산하는 법까지.

중급18 MIN
수학2026.09.07

수치적으로 안정한 계산 패턴 모음

최댓값 빼기, 로그 공간, log1p·expm1, 분산의 두 공식, 정규화의 ε, fp32 누산, 역행렬 대신 solve — 프레임워크가 몰래 해 주는 일곱 가지를 하나씩 꺼내 각각 어떤 고장을 막는지 직접 재 봤습니다. 수식을 그대로 옮긴 코드가 왜 라이브러리보다 나쁜지에 대한 목록입니다.

중급22 MIN
수학2026.09.07

부동소수점은 어디서 새는가: 반올림, 상쇄, 더하는 순서

0.1 + 0.2가 0.3이 아닌 이유부터 시작해 머신 엡실론을 유도하고, 같은 16비트인데 fp16과 bf16이 서로 다른 지점에서 터지는 이유, 비슷한 수를 뺄 때 유효자리가 사라지는 파괴적 상쇄, 그리고 1,000만 개를 순서만 바꿔 더했을 때 오차가 백만 배 갈리는 실험까지 직접 재 봤습니다.

중급23 MIN