본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 05. 18. 08:34

SLM 파인튜닝을 통한 설계서 생성 시도: 버전 충돌과 과적합을 겪은 기록

요약

본 글은 SLM(Small Language Model)을 파인튜닝하여 기업의 설계서나 매뉴얼처럼 '특정 형식과 말투'를 재현하는 것을 목표로 한 실험 기록입니다. Llama 3.2 3B Instruct 모델을 Unsloth와 LoRA 기법으로 T4 GPU 환경에서 학습시키며, 내용 정확성보다는 형식 재현에 초점을 맞췄습니다. 이 과정에서 '라이브러리 버전 불일치' 및 '과적합(Overfitting)' 등의 기술적 시행착오를 겪었으며, 성공적인 진행을 위해 Google Drive 마운트 등 필수적인 환경 설정 절차도 상세히 다루고 있습니다.

핵심 포인트

  • SLM 파인튜닝은 범용 모델이 알 수 없는 기업 특유의 형식과 말투 재현에 효과적이다.
  • Llama 3.2 3B Instruct와 Unsloth를 활용하여 무료 Colab T4 GPU 환경에서도 경량 모델 학습이 가능하다.
  • 실험 목표는 내용 생성보다는 '정해진 포맷' 및 '자사 특유의 형식 재현'에 중점을 두었다.
  • 학습 과정에서 발생한 주요 문제점으로는 라이브러리 버전 충돌(Version Conflict)과 과적합(Overfitting)이 있었다.
  • Colab 환경에서 학습 데이터와 모델을 영속적으로 보존하기 위해 Google Drive 마운트가 필수적이다.

SLM 파인튜닝을 통한 설계서 생성 시도: 버전 충돌과 과적합을 겪은 기록

설계서를 매번 처음부터 쓰는 것은 번거롭고, 매뉴얼은 프로젝트에 따라 표기 규칙이 독특하여 범용적인 ChatGPT에 그대로 맡겨도 포맷에 잘 맞지 않는다. 경량 모델(SLM)을 직접 파인튜닝(Fine-tuning)하여 "형식에 맞춘 초안을 출력하는" 단계까지 갈 수 없을까 하는 생각에 시도해 보았다.

사용 모델은 Llama 3.2 3B InstructUnsloth로 LoRA 튜닝하는 구성이다. Google Colab의 무료 T4 GPU로 완결할 수 있다. 실제 데이터는 전혀 사용하지 않고, 직접 만든 더미(Dummy) 설계서와 매뉴얼로 재현한 결과를 기록한다.

결론부터 말하겠다. 형식의 재현은 실용적인 수준까지 도달했다. 내용의 정확성은 리뷰를 전제로 한 초안 수준에 머물렀다. 그리고 그 단계에 도달하기까지 「라이브러리 버전 불일치」와 「과적합 (Overfitting)」을 차례대로 겪었다. 성공담보다는 이러한 시행착오가 더 도움이 될 것이라 생각하여, 발생한 일을 시계열로 그대로 적는다.

Colab의 기초 조작(계정, 노트북 생성, GPU 선택, 셀 실행)은 본문에서 생략한다. Colab의 셀 조작이 가능하다는 전제하에 진행한다.

왜 이 용도에 SLM 파인튜닝인가

  • 범용 모델은 일반론은 말할 수 있지만, 자사의 설계 사상, 표기 규칙, 용어까지는 알지 못한다.
  • 파인튜닝한 SLM은 형식과 말투의 재현이 능숙해진다.
  • 3B급 모델이라면 무료 Colab T4에서 구동 가능 → 기밀 데이터를 외부로 유출하지 않고 테스트할 수 있다.

설계서나 업무 매뉴얼은 "정해진 포맷"과 "자사 특유의 말투"가 많으므로, 내용 생성보다는 형식의 재현에서 효과가 나타나기 쉬운 영역이라는 가설로 시작했다. 이 가설이 맞았는지는 마지막에 확인하겠다.

Step 0: 작업을 잃어버리지 않기 위한 영속화 (가장 중요)

절차를 진행하기 전에 반드시 이것부터 해야 한다. Colab의 세션에는 수명이 있으며, 방치하거나 장시간 사용하면 연결이 끊긴다. 연결이 끊기면 런타임 (Runtime)이 리셋되어 /content/ 디렉토리 직하의 파일, 설치 환경, 변수가 모두 삭제된다.

삭제되면 곤란한 것들(학습 데이터, 학습 완료된 모델)은 Colab 로컬이 아닌 Google Drive에 둔다. 첫 번째 셀에서 마운트(Mount)한다.

from google.colab import drive
drive.mount('/content/drive')

성공하면 /content/drive/MyDrive/ 아래가 영속 영역이 된다. 이후 데이터와 모델 모두 여기에 둔다.

주의할 점:

  • 경로는 MyDrive (공백 없음)이다. 일본어 UI 표시로는 「마이 드라이브」이지만, 코드상으로는 MyDrive이다. 이 부분을 「마이 드라이브」나 공백을 포함해 작성하면 FileNotFoundError가 발생하기 쉽다.
  • 여러 Google 계정으로 로그인되어 있으면 의도하지 않은 Drive에 마운트될 수 있다.
  • 세션이 끊겨 재개할 때는 이 마운트 셀을 다시 한 번 실행해야 한다.

이 Step 0를 해둔 덕분에, 후술할 버전 지옥으로 인해 몇 번이나 런타임을 재시작해야 했음에도 학습 데이터는 무사했다. 미리 해둘 가치가 있다.

Step 1~3: 환경·모델·데이터 (최종적으로 동작한 형태)

버전 지옥의 경위는 다음 장에서 정리하겠다. 여기에는 최종적으로 동작한 구성만 먼저 배치한다.

Step 1: 설치

!pip install -U "trl>=0.18.2,<=0.24.0,!=0.19.0" "datasets>=3.4.1,<4.4.0"
!pip install --no-deps bitsandbytes accelerate peft triton cut_cross_entropy unsloth_zoo
!pip install --no-deps unsloth

설치 후 반드시 버전을 확인한다.

import trl, transformers, datasets
print("trl", trl.__version__)
print("transformers", transformers.__version__)
...

필자 환경에서 안정적이었던 버전은 trl 0.24.0 / transformers 5.5.0 / datasets는 3.x 대이다. 왜 이 고정이 필요한지는 다음 장에서 설명한다.

Step 2: 모델 로드

from unsloth import FastLanguageModel
import torch
model, tokenizer = FastLanguageModel.from_pretrained(
...

Step 3: 학습 데이터 만들기

이 부분이 결과에 가장 큰 영향을 미친다. 후술하겠지만, 여기서 만드는 방법을 잘못하여 과적합 (Overfitting)으로 한 차례 실패했다. 올바른 제작 방법을 먼저 기술한다.

사내 데이터는 사용하지 않고, 더미 (Dummy) 데이터를 셀에서 직접 Drive로 내보낸다. 설계서와 매뉴얼을 동일한 배열에 나열하여 하나의 파일로 만든다.

import json
data = [
{
...

버전 지옥: 오래된 Unsloth 기사 복사 붙여넣기는 2026년에 막힌다

이곳이 첫 번째 관문이었다. 인터넷에 널려 있는 Unsloth 튜토리얼은 !pip install ... "trl<0.9.0"과 같이 오래된 버전을 고정(Pinning)해 둔 경우가 많다. 이를 2026년 5월의 Colab에서 그대로 실행하면 에러가 연쇄적으로 발생한다. 실제로 겪었던 순서대로 기록한다.

Trainer.__init__() got an unexpected keyword argument 'tokenizer'

  1. 오래된 TRL을 전제로 한 템플릿은 SFTTrainer(..., tokenizer=tokenizer, ...)라고 작성한다. 오래된 TRL은 내부적으로 Trainer(tokenizer=...)를 호출하지만, 새로운 transformers는 Trainer에서 tokenizer 인수를 폐지하고 processing_class로 이름을 변경했다. 오래된 TRL × 새로운 transformers 사이에서 이러한 불일치가 발생한다.

cannot import name 'SFTConfig' from 'trl'

  1. 새로운 API에 맞추려고 from trl import SFTConfig를 작성했더니 이번에는 이 에러가 발생했다. SFTConfig는 새로운 TRL에만 존재한다. 즉, 환경의 TRL이 오래된 버전임을 확정할 수 있다.

trl 0.8.6 / transformers 5.5.0

  1. 버전을 확인하니 최악의 미스매치(Mismatch)였다. trl 0.8.6 (2024년)과 transformers 5.5.0 (최신 세대). 이는 인수를 수정하는 방식으로는 해결할 수 없다. 조합을 맞춰주는 수밖에 없다.

--no-deps가 업그레이드를 무력화하는 함정

  1. !pip install --no-deps ... trl ...를 실행해도 trl은 0.8.6 상태 그대로였다. --no-deps는 의존성 해결 (Dependency Resolution)을 하지 않기 때문에, 기존의 오래된 trl이 덮어씌워지지 않고 그대로 남는다. 이것이 "pip를 교체해도 적용되지 않는" 정체다.

5. 이번에는 너무 최신임. unsloth-zoo의 범위를 벗어남

!pip install -U "trl>=0.12.0"을 했더니, pip가 최신 버전인 trl 1.4.0까지 가져갔다. 그러자 다음 에러가 발생했다:

unsloth-zoo 2026.5.1 requires trl!=0.19.0,<=0.24.0,>=0.18.2, but you have trl 1.4.0 which is incompatible.
unsloth-zoo 2026.5.1 requires datasets ...,<4.4.0, but you have datasets 4.8.5 which is incompatible.

너무 오래되어도, 너무 최신이어도 안 된다. unsloth-zoo가 허용하는 범위를 이쪽에서 명시적으로 지정하여 고정할 수밖에 없다.

해결: 범위를 고정하기

!pip install -U "trl>=0.18.2,<=0.24.0,!=0.19.0" "datasets>=3.4.1,<4.4.0"

이렇게 하여 trl 0.24.0 (상한선에 딱 맞음)에 안착했고, pip의 충돌 (Conflict) 에러도 사라졌다. Trainer는 새로운 API인 SFTConfig 버전으로 작성한다.

from trl import SFTTrainer, SFTConfig
from unsloth import is_bfloat16_supported
trainer = SFTTrainer(
...

이 장의 교훈: 오래된 Unsloth 기사의 trl<0.9.0

2026년의 핀(Pin)을 믿어서는 안 된다. 버전은 '너무 오래되지도, 너무 최신이지도 않은' 상태로 직접 범위를 지정하여 고정해야 한다. --no-deps는 업그레이드 목적으로 사용하지 않는다. 수동으로 해결하며 에너지를 소모하느니, 차라리 Unsloth 공식에서 배포하는 정합성이 검증된 Colab 노트북을 열어 데이터만 교체하는 것이 결국 더 빠르다.

데이터 건수와 step 수의 관계: 적으면 학습이 성립되지 않는다

버전 지옥을 벗어나 학습이 돌아가기 시작했지만, 이번에는 데이터 건수라는 또 다른 벽에 부딪혔다. 실측 수치를 그대로 공개한다.

데이터 2건으로 실행

Num examples = 2 | Num Epochs = 2 | Total steps = 2
Step 1: 3.420545
Step 2: 3.420545

loss가 한 자리도 움직이지 않는다. 2건으로는 학습이 성립되지 않는다.

데이터 12건으로 실행

Num examples = 12 | Num Epochs = 2 | Total steps = 4
Step 1: 3.572 → Step 4: 3.090

loss는 낮아졌지만, 고작 4 step뿐이다. 파라미터(Parameter)를 4번밖에 업데이트하지 않았으므로 이 역시 실질적으로 학습되었다고 보기 어렵다.

원인은 계산해 보면 알 수 있다. 총 배치 사이즈(Total Batch Size)는 per_device_batch(2) × grad_accum(4) × GPU(1) = 8이다. 데이터가 12건이라면 1 epoch ≒ 12÷8로 1~2 step, 2 epoch이면 4 step밖에 되지 않는다. step 수는 데이터 건수와 총 배치 사이즈로부터 역산하여 설계해야 한다.

데이터 84건으로 실행

Num examples = 84 | Num Epochs = 2 | Total steps = 22
Step 1: 3.152
Step 6: 2.294
...

3.15에서 0.53으로 깔끔하게 수렴했다. 전반부에 급강하하고 후반부에 완만해지는, 교과서적인 학습 곡선(Learning Curve)이다. '이것은 성공했다'라고 생각했다. 하지만 이것이 함정이었다.

과적합(Overfitting)의 실례: loss가 깔끔해도 출력에서 망한다

loss 곡선이 깔끔했기에, 84건·loss 0.53인 모델로 생성을 시도했다. 프롬프트(Prompt)는 학습시키지 않은 주제인 "재고 관리 기능의 기본 설계서를 작성해 주세요. 요구사항: 실시간 업데이트, CSV 임포트 대응"이었다. 결과물은 다음과 같았다.

【기본 설계서】
1. 기능 개요
- 재고 관리 기능
...

마지막의 ...(템플릿에 따른 본문)에 주목해 주길 바란다. 이것은 학습 데이터의 assistant 측에 작성했던 문자열 그 자체다. 당시 더미 데이터(Dummy Data)를 만들 때 본문을 생략하고 "3. 처리 플로우\n ...\n(템플릿에 따른 본문)"라고 대충 작성했었다. 모델은 그것을 충실히 암기하여 기능명만 바꿔서 출력한 것이다. 주제에 맞춰 내용을 생성하는 것이 아니라, 형식을 통째로 복사하고 있을 뿐이다.

이것이 과적합(Overfitting)의 전형적인 사례다. 중요한 점은, loss 곡선은 그토록 깔끔했음에도 불구하고 출력은 쓰레기였다는 사실이다. loss 추이만 보고 있다면 '성공'이라고 오판하게 된다. 판단은 반드시 생성된 출력물로 해야 한다는 실례가 되었다.

원인은 학습 파라미터가 아니라 데이터의 내용이었다. assistant 측을 ... (템플릿에 따른 본문)과 같이 생략했던 것이 치명타였다. 3B 모델에 84건, loss 0.5까지 학습시키면 적혀 있는 그대로 생략된 부분까지 충실히 암기한다. 데이터의 질이 전부라는 말이 바로 이런 의미였다.

데이터를 끝까지 작성하여 재학습: 서식 전사는 성공했다

더미 데이터의 assistant 측을 생략 없이 끝까지 작성한 진짜 설계서와 매뉴얼로 전부 다시 썼다. ...(템플릿에 따른 본문)도 실제로 통용되는 본문으로 교체했다. 90건으로 재학습을 진행했다.

Num examples = 90 | Num Epochs = 2 | Total steps = 24
Step 1: 1.866
Step 8: 0.851
...

시작 지점이 1.86으로, 이전의 3.15보다 낮다. 양질의 데이터는 모델이 예측하기 쉽기 때문에 시작 loss가 낮아진다. 이는 데이터 품질이 올라갔다는 증거이기도 하다. 최종적으로 0.25에서 평탄해졌다.

동일한 미학습 주제로 생성시킨 결과:

【기본 설계서】
1. 기능 개요
- 재고 관리 기능
...

이전의 통째로 암기하던 모습에서 명확히 벗어났다. ...

단순히 통째로 베껴 쓰는 모습이 사라지고, 학습시키지 않은 「재고 관리」라는 주제에 대해 처리 흐름(Process Flow)을 스스로 채워 넣고 있다. 서식의 틀(【기본 설계서】 → 번호가 매겨진 헤더 → 「(◯◯ 템플릿 준수)」로 마무리)도 재현하고 있다. 서식 전사(Format Transcription)는 성립했다.

하지만 무조건적인 성공은 아니다. 내용을 읽어보면 「첨부 파일: 억지로 길지만 필요한 정보가 있는 경우는 업로드」 부분은 일본어가 깨져 있고, 재고 관리에 이메일 확인이나 첨부 파일이 등장하는 것은 요구사항과 맞지 않는다. 학습 데이터(경비 정산 매뉴얼 등)의 어휘가 재고 관리라는 주제에 끌려 나와 유출되고 있다. 3B 모델에 90건, Loss 0.68 규모라는, 어떤 의미에서는 당연한 한계다.

최종 Loss 0.25는 지난번의 0.52보다 더욱 낮아졌음에도 여전히 어휘 유출이 발생하고 있다. 여기서 에포크(Epoch)를 더 늘리면 다시 통째로 암기하는 방향으로 돌아간다. 2 에포크가 적절한 중단 시점이라는 것이 실측을 통한 판단이었다.

결론: 가설은 맞았다

서두의 가설은 "효과가 있는 것은 서식의 재현이며, 내용의 정확성은 별개의 문제다"였다. 실측 결과는 이와 완전히 일치했다.

포맷 재현은 실용적인 수준. 미지의 주제에서도 자사 서식의 틀을 재현할 수 있음 -
내용의 정확성은 리뷰를 전제로 한 초안 수준. 어휘 유출 및 일본어 파괴가 있어 그대로 사용할 수는 없음 - 3B + 수백 건 규모로는 내용을 보장할 수 있는 AI가 될 수 없음. 이는 설정의 문제가 아니라 구조적인 한계

그리고 솔직하게 적어두어야 할 본말전도(本末転倒)가 있다. 과적합(Overfitting)을 피하려면 학습 데이터의 어시스턴트(Assistant) 측 답변을 생략하지 않고 끝까지 다 작성해야 하는데, 이는 결국 「설계서를 처음부터 쓰는 작업」과 거의 맞먹는 수고가 든다. 설계서 생성 AI를 만들기 위해 설계서를 양산해야 하는 구조를 안게 된다. 소형 모델 + 소량 데이터 파인튜닝(Fine-tuning)의 현실적인 비용으로서, 이 점은 숨기지 말고 인식해 두어야 한다고 생각한다.

품질을 한 단계 더 높이고 싶다면, 데이터 건수보다는 다양성(장르를 분산시켜 어휘 편향을 줄임)을 확보하거나, lora_dropout을 0에서 0.05로 높여 일반화(Generalization)를 유도하는 방식 정도가 있다. 다만 이는 초안의 정밀도를 높이는 이야기이지, 내용을 보장하는 이야기는 아니다.

막히는 포인트 요약

앞으로 같은 작업을 할 사람들이 밟게 될 지뢰를 실측 순서대로 정리해 둔다.

  • 오래된 Unsloth 기사의 trl < 0.9.0 고정은 2026년에 막힌다. tokenizer 인자 에러 → SFTConfig 없음 → trl 0.8.6 × transformers 5.5.0 불일치로 연쇄 작용함
  • --no-deps는 업그레이드를 무시한다. pip 교체가 먹히지 않는 원인
  • 버전은 범위를 고정할 것 (trl >= 0.18.2, <= 0.24.0 등). 너무 최신이면 unsloth-zoo의 범위를 벗어나 거부됨
  • 데이터 건수가 적으면 전체 스텝(Total steps)이 극단적으로 적어져서, Loss가 움직이더라도 실질적으로는 학습되지 않는다. 스텝 수는 총 배치 사이즈(Total batch size)로부터 역산할 것
  • Loss 곡선이 깨끗하더라도 과적합(Overfitting)되어 있는 경우가 있다. 판단은 반드시 생성된 출력물로 할 것
  • 학습 데이터의 어시스턴트(Assistant) 측을 생략하면, 생략된 형태 그대로 암기한다. 요령을 피울 수 없다
  • 사라지면 곤란한 파일은 Drive에 둔다. 버전 지옥으로 인해 몇 번이고 재시작해야 하므로 이것이 효과적이다

성공담이 아니라, 이렇게 막혔던 기록이 앞으로 시도할 사람들에게는 더 도움이 될 것이다.

참조: Unsloth 공식 리포지토리 / Unsloth 공식 Colab 문서

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0