개요
35편에서 숫자를 담는 법을, 36편에서 모델을 줄이는 법을 봤다. 이번이 마지막 편이다. 줄인 모델을 내 것으로 만드는 법.
경량화까지는 어떻게든 됐다고 치자. 그런데 서비스를 만들려면 그다음 문제가 남는다. 범용 모델은 일반적인 능력은 강하지만, 특정 도메인에 특화시키려면 조정이 필요하다. 사내 문서를 이해해야 한다거나, 특정 말투로 답해야 한다거나.
새로 모델을 만들 수는 없다. 그럼 있는 모델을 튜닝해야 하는데, 700억 개짜리 모델의 가중치를 전부 갱신하는 건 사실상 처음부터 학습하는 것과 비슷한 비용이 든다.
이번 글이 답할 질문 모델 전체를 건드리지 않고 모델을 내 것으로 만들 수 있을까? 그게 LoRA고, 거기에 36편의 양자화를 합친 게 QLoRA다.
교육 과정 자료는 대외비라 원본 슬라이드는 싣지 않는다.
1. 파인튜닝을 왜 하나 — 회복과 특화, 두 가지 이유
자료가 파인튜닝을 두 관점에서 나눠 설명하는 게 좋았다. 같은 단어인데 목적이 다르다.
경량화 관점에서의 파인튜닝 — 36편에서 본 대로 모델을 깎으면 정확도가 떨어진다. 그 떨어진 정확도를 되돌리는 과정이 파인튜닝이다. QAT처럼 아예 학습 초기부터 같이 가는 방식도 여기에 속한다.
서비스 관점에서의 파인튜닝 — 이건 성능 회복이 아니라 방향 조정이다. 일반적인 능력은 원본 모델이 이미 갖고 있으니, 특정 영역에 맞게 튜닝해서 서비스한다.
하나는 잃은 걸 되찾는 일이고, 다른 하나는 없던 걸 붙이는 일이다. LoRA는 주로 두 번째 쪽에서 쓴다.
2. 전부 다시 학습하는 게 왜 문제인가
가장 단순한 방법은 Full fine tuning이다. 특정 도메인 데이터를 가지고 원본 모델의 모든 가중치를 다시 학습한다.
장점은 분명하다.
- 처음부터 새로 학습하는 것에 비하면 적은 데이터로도 도메인에 특화시킬 수 있다
- 도메인 특화 관점에서 정확도 이득을 기대할 수 있다
그런데 단점이 셋이나 붙는다.
- 학습 비용 — 모델이 클수록 모든 가중치를 갱신하는 부담이 그대로 커진다
- 메모리 — 가중치뿐 아니라 기울기와 옵티마이저 상태까지 들고 있어야 해서, 사실상 처음부터 학습하는 수준의 자원이 필요할 수 있다
- 망각 — 원본 모델의 가중치 자체가 변하는 과정에서 기존에 학습한 정보가 지워질 수 있다
세 번째가 특히 무섭다. 사내 문서에 특화시키려다 원래 잘하던 일반 상식이 망가지는 것이다. 이걸 catastrophic forgetting(파국적 망각) 이라고 부른다.
그래서 질문이 바뀐다. “모든 가중치를 갱신하지 않고도 튜닝할 수는 없을까?” 이게 PEFT의 출발점이다.
3. PEFT — 원본을 얼마나 건드릴 것인가
PEFT(Parameter Efficient Fine Tuning), 파라미터 효율적 파인튜닝. 이름 그대로 일부 파라미터만 학습해서 튜닝 비용을 낮추는 방법들이다.
핵심 질문은 두 개다. 모델을 특화시킬 추가 weight를 어디에, 어떻게 붙일 것인가.
In-context 학습
학습을 아예 안 한다. 원본 모델을 그대로 두고 프롬프트만 고민해서 원하는 방향으로 동작을 유도한다. 앞서 여러 편에 걸쳐 정리했던 프롬프트 엔지니어링이 이쪽이다. 가장 가볍지만 모델 자체는 아무것도 변하지 않는다.
Adapter layer
모델 내부에 새로운 층을 끼워 넣는다. 보통 MLP 형태의 작은 모듈이다. 목표 데이터로 이 어댑터 층만 학습하고 원본 weight는 고정한다.
- Full tuning보다는 적지만 그래도 상당히 많은 추가 weight가 필요하다
- 대신 표현력은 꽤 높다
- 단점이 결정적이다. 원본 모델의 구조 자체가 바뀐다. 추론할 때마다 이 층을 거쳐야 하니 메모리·연산 오버헤드가 계속 따라붙는다
Prompt tuning
입력 임베딩 앞이나 중간에 학습 가능한 가짜 토큰(pseudo-token) 을 끼워 넣는다. 사람이 읽을 수 있는 단어가 아니라 학습으로 만들어진 벡터다.
- 원본 모델의 구조를 전혀 안 건드린다
- 아주 적은 추가 weight로 동작을 조정한다
- 매우 가볍고 배포가 쉽지만, 표현력의 한계로 성능에 제한이 있다
LoRA
Low-Rank Adaptation. 특정 가중치 행렬 옆에 저랭크(low-rank) 업데이트를 하나 붙인다. 원본 weight는 고정하고 추가된 저랭크 weight만 학습한다.
- 구조를 바꾸지 않는다 (Adapter의 단점 회피)
- Adapter보다 훨씬 적은 추가 weight로 높은 표현력을 낸다 (Prompt tuning의 단점 회피)
스펙트럼의 양 끝에서 하나씩 문제가 있었고, LoRA는 그 가운데를 잡았다. 지금 파인튜닝이라고 하면 대체로 이걸 뜻할 정도가 됐다.
4. LoRA — 큰 행렬 대신 가느다란 둘
여기가 이번 편의 핵심이다. 개인적으로 저랭크 분해라는 말이 제일 안 와닿았는데, 그림으로 보니 단순했다.
원래 계산은 이렇다. 입력 x에 가중치 행렬 W를 곱해서 출력 h를 만든다.
$$ h = xW $$
Full fine tuning은 이 W 전체를 학습한다. W가 d × d 라면 학습할 파라미터가 d² 개다.
LoRA는 W를 그대로 얼려두고, 옆에 작은 경로를 하나 더 낸다.
$$ h = xW + xAB $$
여기서 A는 d × r, B는 r × d 짜리 행렬이다. r은 아주 작은 값을 쓴다.
- W를 직접 학습: d × d
- A와 B만 학습: 2 × d × r
d가 10,000이고 r이 8이면, 1억 개가 16만 개가 된다. 600분의 1 아래로 줄어든다.
왜 이게 되나
처음엔 이게 왜 통하는지 의아했다. 큰 행렬을 가느다란 둘로 대신하면 표현력이 확 떨어질 것 같은데.
여기 깔린 가정이 하나 있다. 파인튜닝으로 생기는 변화량은 원래 모델만큼 복잡하지 않다는 것이다. 이미 다 배운 모델을 특정 도메인 쪽으로 살짝 미는 일이니, 그 밀어내는 방향 자체는 훨씬 단순한 구조로 표현될 수 있다는 가정이다.
그래서 W 자체를 저랭크로 근사하는 게 아니라, W에 더할 변화량(ΔW)만 저랭크로 근사한다. 이 차이가 중요하다.
LoRA는 모델을 압축하는 기법이 아니다. 모델의 변화량을 압축하는 기법이다.
덤으로 따라오는 게 두 가지 있다.
- 원본 weight를 안 건드리니 망각 문제에서 자유롭다
- 학습 결과가 어댑터 파일 하나로 떨어진다. 같은 베이스 모델에 어댑터만 바꿔 끼우면 다른 도메인용 모델이 된다
더 쉬운 비유로
Full fine tuning: 책을 통째로 다시 쓴다 LoRA: 책은 그대로 두고 포스트잇을 붙인다
교과서 한 권을 우리 회사용으로 고친다고 해보자. 전부 다시 쓰면 확실하긴 한데, 쓰는 동안 원래 있던 내용이 바뀌거나 빠질 수 있다. 무엇보다 오래 걸린다.
포스트잇은 다르다. 원본은 그대로다. 필요한 페이지에만 “여기선 이렇게 읽어라” 를 붙인다. 포스트잇만 떼면 원래 책으로 돌아가고, 다른 팀은 자기 포스트잇을 붙여서 쓴다. 책은 하나, 포스트잇은 여러 벌.
어디에 붙일 것인가 — target_modules
포스트잇을 아무 데나 붙이진 않는다. LoRA를 적용할 모델 내부 레이어를 지정하는 게 target_modules 설정이다.
가장 대표적인 자리는 Attention 층의 Query·Key·Value 연산 부분이다. 12편에서 어텐션을 정리할 때 봤던 그 QKV다. 이 부분이 모델 성능에 큰 영향을 주기 때문에, 여기에 붙이면 작은 수정만으로도 효과가 크다.
5. 손잡이 두 개 — r과 alpha
LoRA를 실제로 쓸 때 정해야 하는 값이 두 개 있다. 이름이 비슷해서 처음엔 같은 걸 두 번 적는 줄 알았다.
| 값 | 무엇을 정하나 | 크면 |
|---|---|---|
| r (rank) | 어댑터 행렬 A·B의 내부 차원 크기 | 더 많이 배우지만 파라미터와 연산량이 늘어난다 |
| alpha | 어댑터 출력의 스케일(세기) | 학습한 어댑터가 원래 모델에 더 강하게 영향을 준다 |
r은 어댑터를 얼마나 크게 만들 것인가이고, alpha는 그 어댑터를 얼마나 세게 반영할 것인가다. 하나는 그릇의 크기, 하나는 볼륨 손잡이다.
- r은 보통 8, 16, 32 같은 값을 쓴다
- alpha는 보통 2r 로 두고 시작한다
r을 키우면 표현력이 올라가지만 무거워지고, alpha를 키우면 학습한 게 세게 나오지만 원래 능력을 덮어쓸 수 있다. 둘 다 적당히가 답인 값들이다..
6. QLoRA — 4비트로 눌러놓고 어댑터만 BF16으로
여기서 36편 내용이 합류한다. QLoRA는 이름 그대로 양자화(Quantization) + LoRA다.
발상이 재밌었다. 두 기법을 순서대로 적용하는 게 아니라, 한 모델 안에서 정밀도를 두 가지로 나눠 쓴다.
- 원본 모델을 강하게 양자화한다. 4비트 NF4(4bit normal float) 같은 포맷을 쓴다
- 여기까지만 하면 36편에서 본 대로 범용 성능 하락을 피할 수 없다
- 그런데 LoRA 어댑터는 BF16 같은 높은 해상도로 학습한다
전체 모델의 복잡도는 대폭 줄이면서, 목표로 하는 작업에 대해서는 높은 성능을 유지하는 구조다.
이게 되는 이유는 4번 섹션의 관점을 그대로 이어받는다. 원본 가중치는 어차피 학습하지 않는 부분이다. 학습하지 않을 거라면 정밀도가 조금 뭉개져도 감당할 수 있다. 반대로 실제로 배우는 어댑터는 전체에서 아주 작은 일부라 해상도를 지켜도 부담이 없다.
자료에 실린 비교 결과도 그 방향이었다.
| 설정 | GLUE 정확도 | 큰 모델에서의 성능 |
|---|---|---|
| BF16 (원본 그대로) | 88.6 | 기준 |
| LoRA BF16 | 88.8 | 기준과 비슷 |
| QLoRA Int8 | 88.8 | 기준과 비슷 |
| QLoRA FP4 | 88.6 | 기준과 비슷 |
| QLoRA NF4 + DQ | — | 기준과 비슷 |
4비트까지 눌렀는데 성능이 거의 안 떨어졌다. 36편에서 “경량화하면 성능이 떨어진다”고 못 박아뒀던 걸 생각하면 이게 왜 화제가 됐는지 알 것 같았다.
전체를 강하게 눌러놓고, 배우는 부분만 해상도를 지킨다. 경량화와 파인튜닝을 따로 하지 않고 한 번에 하는 것이 QLoRA의 답이었다.
7. 실제로는 이렇게 굴린다
마지막으로 실습 파이프라인 미리보기가 있었다. 개념이 실제로 어떤 순서로 코드가 되는지 보여주는 대목이라 정리해둔다.
라이브러리와 베이스 모델
Unsloth 같은 파인튜닝 라이브러리를 쓴다. GPU 연산을 최적화하고 4비트 양자화를 지원해서, 같은 자원으로 더 큰 모델을 다루고 학습 속도를 크게 줄여준다.
베이스 모델은 unsloth/gemma-3-2b-it 같은 사전 학습된 모델을 가져온다. 약간의 조정만 더해서 새 모델을 만드는 게 핵심이라, 처음부터 학습하는 일은 없다.
같이 불러오는 게 토크나이저다. 텍스트를 모델이 처리할 수 있는 숫자 단위 토큰으로 바꿔주는 도구다. 문장을 모델의 언어로 바꿔주는 번역기 같은 역할이다.
데이터셋을 대화 형식으로
파인튜닝에 쓸 데이터를 모델이 이해하는 대화 형식(Chat Format) 으로 바꿔야 한다.
| 역할(Role) | 하는 일 |
|---|---|
| System | 설명자 — 어떻게 행동할지 |
| User | 질문자 |
| Assistant | 답변자 |
역할을 나눠 기록해두면 모델이 “질문 → 답변”의 흐름을 더 효과적으로 배운다. Unsloth는 get_chat_template 같은 함수로 이 변환을 대신 해준다.
학습과 저장
전처리된 데이터와 LoRA 설정값을 가지고 SFTTrainer 같은 도구로 어댑터를 학습한다. 이때 전체 모델이 아니라 어댑터 파라미터만 갱신되니 훨씬 빠르고 가볍게 끝난다.
저장 방식은 두 가지다.
| 방식 | 특징 |
|---|---|
| 어댑터만 저장 | 용량이 작고 가볍다. 나중에 베이스 모델과 합쳐서 쓴다 |
| Merge (병합) | 학습된 어댑터를 베이스 모델에 직접 합쳐 하나의 완성된 모델로 저장. 추론이 편하지만 용량이 커진다 |
어댑터만 저장하는 쪽이 4번 섹션에서 말한 “책 하나에 포스트잇 여러 벌” 을 그대로 살리는 방식이다. 배포까지 생각하면 Merge가 편할 때도 있다.
추론
학습된 모델로 실제 질문에 답하게 해서 학습이 잘 됐는지 확인한다. 어댑터만 붙여서 쓰거나, 병합된 모델을 직접 불러와서 쓰거나.
정리
- 파인튜닝에는 두 얼굴이 있다. 경량화로 떨어진 성능을 되찾는 일과 도메인에 특화시키는 일
- Full fine tuning은 비용·메모리도 문제지만, 원본 가중치가 변하면서 기존 지식이 지워지는 것이 가장 큰 위험이다
- PEFT는 “추가 weight를 어디에 어떻게 붙일까”의 문제다. Adapter는 구조를 바꾸고, Prompt tuning은 표현력이 부족하다. LoRA는 그 가운데를 잡았다
- LoRA는 모델이 아니라 모델의 변화량(ΔW)을 저랭크로 근사한다. d×d 대신 2×d×r만 학습하니 파라미터가 수백 분의 1로 줄고, 원본을 안 건드리니 망각도 없다
- 붙이는 자리는 보통 Attention의 QKV다. 작은 수정으로 효과가 큰 지점이라서
- r은 어댑터 내부 크기, alpha는 어댑터 효과 크기. 보통 r은 8~32, alpha는 2r
- QLoRA는 원본을 4비트로 누르고 어댑터만 BF16으로 학습한다. 안 배우는 쪽은 뭉개도 되고, 배우는 쪽은 작아서 해상도를 지켜도 부담이 없다는 판단이다
- 실무에서는 베이스 모델 로드 → LoRA 설정 → 대화 형식 전처리 → 어댑터 학습 → 저장(어댑터 or Merge) → 추론 순으로 돈다
세 편을 한 줄로
35편에서 컴퓨터가 숫자를 어떻게 담는지 봤고, 36편에서 그 숫자를 깎고 자르고 물려주는 법을 봤고, 37편에서 줄인 채로 튜닝하는 법을 봤다.
세 편을 관통하는 문장이 하나 있다면 이거였다.
바꿔야 하는 부분만 정확히 골라내면, 나머지는 얼마든지 눌러 담을 수 있다.
2의 보수가 부호 비트를 따로 두지 않은 것도, 양자화가 안 쓰는 칸을 버린 것도, LoRA가 변화량만 저랭크로 잡은 것도 결국 같은 이야기였다.
참고 자료
- LoRA: Low-Rank Adaptation of Large Language Models
- QLoRA: Efficient Finetuning of Quantized LLMs
- Unsloth 공식 문서
- Hugging Face PEFT 공식 문서
한줄 평
전부 바꾸는 대신 바꿀 곳만 정확히 고른다는 발상이 세 편 내내 반복됐는데, 마지막에 QLoRA에서 그게 제일 선명하게 보여서 기분이 좋았다!