순서만 바꿔도 정확도가 흔들린다

few-shot 프롬프트에서 예시의 내용은 그대로 두고 순서만 바꿔도 분류 정확도가 크게 요동치는 현상이 반복적으로 관찰된다. 같은 4개 예시라도 어떤 배열은 90%, 어떤 배열은 60%를 낸다. 이는 모델이 예시의 의미를 균형 있게 반영하는 대신, 특정 위치에 놓인 예시에 과하게 의존하기 때문이다. 실무에서는 "프롬프트를 고쳤더니 좋아졌다"가 사실은 순서 우연에 불과한 경우가 많다.

편향의 세 가지 원인

순서 편향은 대략 세 축으로 나뉜다. 첫째, 최신성 편향(recency): 마지막 예시의 레이블 쪽으로 예측이 쏠린다. 둘째, 다수 레이블 편향(majority): 예시에 많이 등장한 레이블을 선호한다. 셋째, 공통 토큰 편향: 예시 끝에 자주 나온 표현이 출력에 스며든다. 이 편향들은 모델 자체의 사전 확률(prior)에서 오므로, 좋은 예시를 고르는 것만으로는 사라지지 않는다.

대응 1: 순서 앙상블(permutation ensemble)

가장 견고한 방법은 예시 순서를 여러 개로 셔플해 각각 추론한 뒤 결과를 집계하는 것이다. 단일 배열의 우연을 평균이 상쇄한다.

import itertools, random
from collections import Counter

def order_ensemble(client, query, shots, k=5):
    perms = list(itertools.permutations(shots))
    random.shuffle(perms)
    votes = []
    for p in perms[:k]:
        prompt = "".join(f"입력: {x}\n레이블: {y}\n\n" for x, y in p)
        prompt += f"입력: {query}\n레이블:"
        out = client.classify(prompt).strip()
        votes.append(out)
    return Counter(votes).most_common(1)[0][0]

비용이 k배 늘지만, 안정성이 중요한 배치 파이프라인에서는 충분히 값어치를 한다. k는 3~7 사이에서 정확도가 대부분 수렴한다.

대응 2: 사전 확률 보정(contextual calibration)

내용이 없는 중립 입력(빈 문자열, "N/A")을 넣어 모델의 기본 편향을 측정한 뒤, 그 편향을 나눠서 상쇄하는 방법이다. 순서를 늘리지 않으므로 저비용이다.

import numpy as np

def calibrate(logits_query, logits_null):
    # logits_null: 중립 입력에 대한 클래스별 확률
    p_null = np.exp(logits_null) / np.exp(logits_null).sum()
    W = np.diag(1.0 / p_null)          # 편향의 역수로 보정 행렬
    p_q = np.exp(logits_query) / np.exp(logits_query).sum()
    adjusted = W @ p_q
    return int(np.argmax(adjusted))

logprobs를 반환하는 API에서만 쓸 수 있다는 제약이 있지만, 추가 호출이 중립 입력 1회뿐이라 실시간 서비스에 적합하다.

대응 3: 레이블 균형과 배치 설계

예시 구성 단계에서 편향을 줄일 수도 있다. 레이블별 개수를 맞추고, 같은 레이블이 연속으로 끝에 몰리지 않게 배치한다.

def balanced_layout(shots_by_label, per_class=2):
    picked = []
    for label, items in shots_by_label.items():
        picked += [(x, label) for x in items[:per_class]]
    # 같은 레이블이 인접하지 않도록 라운드로빈 재배열
    picked.sort(key=lambda t: (t[1],))
    interleaved = []
    buckets = {}
    for x, y in picked:
        buckets.setdefault(y, []).append((x, y))
    while any(buckets.values()):
        for y in list(buckets):
            if buckets[y]:
                interleaved.append(buckets[y].pop(0))
    return interleaved

기법 비교

기법추가 비용logprobs 필요적합한 상황
순서 앙상블높음 (k배)불필요배치·오프라인 평가
사전 확률 보정낮음 (+1회)필요실시간 분류
레이블 균형 배치없음불필요모든 상황의 기본기

주의점

세 기법은 배타적이지 않다. 균형 배치를 기본으로 깔고, 정확도가 민감한 경로에만 앙상블이나 보정을 더하는 조합이 현실적이다. 다만 앙상블은 다수결이 오답으로 수렴할 수 있어, 반드시 순서를 고정한 홀드아웃 세트로 실측 검증해야 한다. 보정은 중립 입력 선택에 결과가 민감하므로 도메인에 맞는 중립값을 여러 개 시험해보는 편이 안전하다. 또한 순서 편향은 모델·버전마다 강도가 달라, 모델을 교체하면 프롬프트 순서 튜닝은 재검증이 필요하다는 점을 파이프라인 전제로 두는 것이 좋다.