hyunsu blog

홈강좌LAYA 결정 모델 24편 중 10편째

LAYA 지속 학습 2 — 이어서 학습할까, 처음부터 다시 할까: 세 번씩 돌려 본 답

목차 · 6
  1. 01방법 1 — v1 에 새 데이터만 이어 학습
  2. 02방법 2 — v1 에 옛 데이터와 새 데이터를 함께 이어 학습
  3. 03방법 3 — 바탕 모델에서 누적 데이터 전체로 다시
  4. 04한 번 돌린 숫자를 믿을 수 있나
  5. 05비교 — 같은 종류 600건이면?
  6. 06실습 — 지속 학습의 규칙

9편에서 모은 정답 90건으로 v2 를 만든다. 방법 셋을 비교했다. 처음에는 한 번씩만 돌렸는데, 같은 명령을 다시 돌리니 숫자가 최대 0.2 까지 달라졌다.(8편에서 본 흔들림이다) 그래서 씨앗(seed)만 바꿔 세 번씩 다시 돌렸다. 바탕 모델에서 누적 데이터로 처음부터 다시 학습한 쪽이 평균 0.825 로 가장 높고 가장 고르게 나왔다.

laya-train 의 --base 에는 바탕 모델 이름뿐 아니라 내가 학습한 체크포인트 폴더도 줄 수 있다. 그래서 v1 에서 이어 학습하는 것은 --base checkpoints/v1 한 줄이면 된다.

01 방법 1 — v1 에 새 데이터만 이어 학습

bash
laya-train --data data/feedback.jsonl --base checkpoints/v1 --out checkpoints/v2a
python evaluate.py checkpoints/v2a
text
train items 162, calibration items 18, eval items 18 (calibration_slice), skipped {}, device mps
optimizer budget: 162 train item(s), effective batch up to 64 (micro_batch 8 x grad_accum 8), 12 optimizer updates over 4 epoch(s)

90건의 학습 계획이다. 업데이트가 12번뿐이다.

text
model: checkpoints/v2a
accuracy: 0.730 (146/200)
  account   0.800 (36/45)
  billing   0.730 (46/63)
  delivery  0.622 (28/45)
  technical 0.766 (36/47)
mean confidence: 0.979
ECE: 0.249
most common mistakes: billing -> technical ×17, delivery -> technical ×15, technical -> billing ×11
time: 35 ms per case

02 방법 2 — v1 에 옛 데이터와 새 데이터를 함께 이어 학습

새 데이터만 넣으면 처음 150건에서 배운 것을 잊을 수 있다. 옛 150건을 다시 섞는다.

bash
cat first-150.jsonl data/feedback.jsonl > data/train-v2.jsonl
laya-train --data data/train-v2.jsonl --base checkpoints/v1 --out checkpoints/v2b
python evaluate.py checkpoints/v2b
text
     240

첫 줄 240 은 합친 데이터의 줄 수다.

text
model: checkpoints/v2b
accuracy: 0.735 (147/200)
  account   0.800 (36/45)
  billing   0.905 (57/63)
  delivery  0.711 (32/45)
  technical 0.468 (22/47)
mean confidence: 0.972
ECE: 0.237
most common mistakes: technical -> billing ×25, delivery -> technical ×10, account -> billing ×9
time: 37 ms per case

결제팀은 0.905 로 크게 올랐는데 기술팀이 0.468 로 무너졌다(기술 → 결제 25번). 옛 150건은 v1 에서 이미 4에폭 학습했으니, 여기서 4에폭을 더 돌면 같은 문장을 8번 본 셈이다. 결제처럼 보이는 기술 문의("결제하기를 누르면 화면이 하얘져요")가 새로 들어오면서 경계가 흔들렸다.

03 방법 3 — 바탕 모델에서 누적 데이터 전체로 다시

v1 을 버리고, 처음과 같은 바탕 모델(multilingual)에서 누적 240건으로 다시 학습한다.

bash
laya-train --data data/train-v2.jsonl --base multilingual --out checkpoints/v2
python evaluate.py checkpoints/v2
text
model: checkpoints/v2
accuracy: 0.815 (163/200)
  account   0.889 (40/45)
  billing   0.825 (52/63)
  delivery  0.622 (28/45)
  technical 0.915 (43/47)
mean confidence: 0.951
ECE: 0.136
most common mistakes: delivery -> technical ×15, billing -> technical ×10, account -> billing ×5
time: 34 ms per case

정확도 0.815, ECE 0.136. 팀 넷이 모두 v1 보다 같거나 높다. 학습은 1~2분이라 매번 처음부터 해도 부담이 없다.

04 한 번 돌린 숫자를 믿을 수 있나

같은 명령을 처음 돌렸을 때와 다시 돌렸을 때의 팀 정확도를 비교하면 이렇다.

방법처음다시
v1 + 새 데이터만0.6900.730
v1 + 누적 데이터0.6750.735
바탕 + 누적 데이터0.8200.815

데이터도 명령도 같은데 숫자가 달라졌다. 맥 GPU(MPS)의 학습은 매번 완전히 같은 결과를 보장하지 않는다. 데이터가 수백 건뿐이면 그 작은 차이가 정확도 몇 점으로 커진다. 그래서 씨앗을 1·2·3 으로 바꿔 세 번씩 학습하고 평균과 폭(가장 높은 것 - 가장 낮은 것)을 쟀다. 비교를 위해 v1 자체(바탕 + 처음 150건)도 세 번 다시 학습했다. 9편에서 운영에 쓴 v1 은 그대로 두고 쓴다.

python
"""같은 방법을 씨앗(seed)만 바꿔 세 번씩 학습해, 한 번 돌린 숫자가 얼마나 흔들리는지 잰다.

맥 GPU(MPS)의 학습은 완전히 같은 결과를 보장하지 않고, 씨앗에 따라서도 달라진다.
v1 은 9편에서 운영에 쓴 그 판(checkpoints/v1)을 그대로 두고 비교한다.
"""
import json
import shutil
import subprocess
import statistics

import laya

APPROACHES = {
    "v1 again (base + first 150)": ("first-150.jsonl", "multilingual"),
    "continue v1 + new 90": ("data/feedback.jsonl", "checkpoints/v1"),
    "continue v1 + all 240": ("data/train-v2.jsonl", "checkpoints/v1"),
    "retrain base + all 240": ("data/train-v2.jsonl", "multilingual"),
}
SEEDS = [1, 2, 3]
cases = [json.loads(line) for line in open("tickets-eval.jsonl", encoding="utf-8")]


def team_accuracy(path):
    agent = laya.load(path)
    hits = sum(agent.predict(c["state"], c["questions"])["answers"]["team"]["choice"] == c["expected"]["team"]
               for c in cases)
    return hits / len(cases)


print(f"{'approach':<30}{'seed 1':>8}{'seed 2':>8}{'seed 3':>8}{'mean':>8}{'range':>8}")
for name, (data, base) in APPROACHES.items():
    scores = []
    for seed in SEEDS:
        out = f"checkpoints/repeat-{seed}"
        subprocess.run(["laya-train", "--data", data, "--base", base, "--out", out, "--seed", str(seed)],
                       check=True, capture_output=True)
        scores.append(team_accuracy(out))
        shutil.rmtree(out)
    spread = max(scores) - min(scores)
    cells = "".join(f"{s:>8.3f}" for s in scores)
    print(f"{name:<30}{cells}{statistics.mean(scores):>8.3f}{spread:>8.3f}", flush=True)
bash
python repeat.py
text
approach                        seed 1  seed 2  seed 3    mean   range
v1 again (base + first 150)      0.800   0.790   0.705   0.765   0.095
continue v1 + new 90             0.710   0.705   0.745   0.720   0.040
continue v1 + all 240            0.635   0.780   0.835   0.750   0.200
retrain base + all 240           0.865   0.835   0.775   0.825   0.090
그림 1.v2 만드는 방법별 — 씨앗 세 번
0.600.650.700.750.800.850.90v1 다시 (150건)v1 + 새 90건 이어v1 + 240건 이어바탕 + 240건 다시v1 다시 (150건): 0.800v1 다시 (150건): 0.790v1 다시 (150건): 0.705v1 + 새 90건 이어: 0.710v1 + 새 90건 이어: 0.705v1 + 새 90건 이어: 0.745v1 + 240건 이어: 0.635v1 + 240건 이어: 0.780v1 + 240건 이어: 0.835바탕 + 240건 다시: 0.865바탕 + 240건 다시: 0.835바탕 + 240건 다시: 0.775v1 다시 (150건): 0.765v1 + 새 90건 이어: 0.720v1 + 240건 이어: 0.750바탕 + 240건 다시: 0.825평균 0.765평균 0.720평균 0.750평균 0.8250.60.70.80.9v1 다시 (150건)v1 + 새 90건 이어v1 + 240건 이어바탕 + 240건 다시v1 다시 (150건): 0.800v1 다시 (150건): 0.790v1 다시 (150건): 0.705v1 + 새 90건 이어: 0.710v1 + 새 90건 이어: 0.705v1 + 새 90건 이어: 0.745v1 + 240건 이어: 0.635v1 + 240건 이어: 0.780v1 + 240건 이어: 0.835바탕 + 240건 다시: 0.865바탕 + 240건 다시: 0.835바탕 + 240건 다시: 0.775v1 다시 (150건): 0.765v1 + 새 90건 이어: 0.720v1 + 240건 이어: 0.750바탕 + 240건 다시: 0.825평균 0.765평균 0.720평균 0.750평균 0.825
  • 씨앗 1·2·3
  • 평균
  • 최소~최대

평가 데이터 200건의 팀 정확도

숫자로 보기
항목측정값평균
v1 다시 (150건)0.800, 0.790, 0.7050.765
v1 + 새 90건 이어0.710, 0.705, 0.7450.720
v1 + 240건 이어0.635, 0.780, 0.8350.750
바탕 + 240건 다시0.865, 0.835, 0.7750.825
방법평균폭
v1 다시(바탕 + 처음 150건)0.7650.095
v1 + 새 데이터만 이어서0.7200.040
v1 + 누적 데이터 이어서0.7500.200
바탕 + 누적 데이터 다시0.8250.090
  • 바탕에서 다시가 평균이 가장 높다. 가장 나쁜 판(0.775)도 v1 의 평균(0.765)보다 높다.
  • 새 데이터만 이어서는 고르게 낮다. 처음 데이터에서 배운 것을 일부 잊는다(파국적 망각, catastrophic forgetting).
  • 누적 데이터 이어서는 0.635 부터 0.835 까지 폭이 0.2 다. 운이 좋으면 좋고 나쁘면 v1 보다 한참 나쁘다. 운영에 올릴 방법으로는 위험하다.
  • v1 자체도 0.705~0.800 을 오간다. 9편의 v1(0.750)은 그 가운데 하나다.

05 비교 — 같은 종류 600건이면?

"데이터를 많이 넣으면 되지 않나" 를 보려고, 6편의 학습용 600건(처음 150건과 같은 문장 틀)을 모두 넣어 바탕 모델에서 학습했다.

bash
laya-train --data tickets-train.jsonl --base multilingual --out checkpoints/more-600
python evaluate.py checkpoints/more-600
text
model: checkpoints/more-600
accuracy: 0.800 (160/200)
  account   0.800 (36/45)
  billing   0.794 (50/63)
  delivery  0.622 (28/45)
  technical 0.979 (46/47)
mean confidence: 0.976
ECE: 0.183
most common mistakes: delivery -> technical ×15, billing -> technical ×10, account -> technical ×9
time: 35 ms per case

0.800 이다(이것은 한 번 돌린 숫자다). 2.5배 많은 같은 종류 데이터가, 새 표현 90건이 들어간 240건(위의 0.815, 세 번 평균 0.825)을 넘지 못했다. 운영에서 들어온 새 표현이 같은 표현을 더 쌓는 것만큼, 혹은 그 이상 값졌다. 지속 학습이 의미 있는 이유가 이것이다.

배송팀은 어느 판에서도 0.6 언저리다. 평가용 배송 문장에는 "주소를 잘못 적었어요"·"색상이 달라서 바꾸고 싶어요" 처럼 학습·운영 어디에도 비슷한 표현이 없는 것이 있다. 이런 팀은 숫자가 오르지 않는 것으로 드러나니, 다음 운영 주기에 그 팀의 표본 검사를 늘려 데이터를 모은다.

06 실습 — 지속 학습의 규칙

  1. 데이터는 계속 쌓고, 학습은 바탕 모델에서 처음부터 한다. LAYA 는 작아서 수백~수천 건이면 맥북에서 몇 분이다.
  2. 한 번 돌린 숫자로 판단하지 않는다. 데이터가 적을수록 판마다 몇 점씩 흔들린다. 중요한 결정(방법·설정을 바꿀 때) 앞에서는 씨앗을 바꿔 세 번 이상 돌린다.
  3. 이어서 학습해야 한다면(데이터가 수십만 건이라 처음부터가 너무 오래 걸릴 때) 에폭을 줄이고, 옛 데이터를 반드시 섞고, 세 번 돌려 폭을 본다.
  4. 어떤 방법이든 고정된 평가 데이터로 이전 판과 비교한다. 학습 로그의 전후 표는 이번에도 1.0000 근처였다.

이번 v2(0.815)를 운영에 올려도 될지, 다음 편에서 기준을 코드로 만들어 판정한다.