logo

확률적 디코딩

온도, top-k, top-p를 조절하며 확률적 텍스트 생성을 비교한다.

확률적 디코딩

  • 확률적 디코딩(stochastic decoding): 다음 토큰의 확률분포에서 토큰을 무작위로 추출하는 방법
  • 필요한 이유: 결정론적 디코딩은 같은 입력에 항상 같은 답을 내고 반복이 잦음
    • 대화, 창작처럼 다양한 답이 필요한 과업에서는 무작위 추출이 유리
  • do_sample=True: Transformers에서 확률에 따른 무작위 추출을 사용
  • 특징: 같은 입력과 설정에서도 실행할 때마다 다른 결과가 나올 수 있음
  • 난수 시드(random seed): 무작위 추출 순서를 고정해 결과를 비교할 때 사용

실습 준비

!pip install -q "transformers==4.55.4" accelerate jinja2
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_name = "LGAI-EXAONE/EXAONE-4.0-1.2B"  # 허브 저장소 식별자
tokenizer = AutoTokenizer.from_pretrained(model_name)  # 토크나이저 로드
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.float16,  # T4 GPU용 16비트 자료형
    device_map="auto",         # 사용 가능한 GPU에 자동 배치
)

온도

  • 온도(temperature): 토큰 사이의 확률 차이를 조절하는 양수
  • : 원래 확률분포를 유지
  • : 확률이 높은 토큰에 더 집중
    • 가 0에 가까워지면 최고 확률 토큰만 남아 탐욕 탐색과 비슷해짐
  • : 확률 차이를 줄여 다양한 토큰을 선택할 가능성을 높임
  • 소프트맥스(softmax): 모형의 점수를 확률로 바꾸는 함수
    • 온도: 점수를 로 나눈 뒤 소프트맥스를 적용
  • : 토큰 에 대한 모형의 점수
  • 간단한 비교: 세 토큰의 점수를 각각 2, 1, 0으로 가정
logits = torch.tensor([2.0, 1.0, 0.0])  # 세 토큰의 가상 점수

for temperature in [0.7, 1.0, 1.3]:
    probabilities = torch.softmax(
        logits / temperature,
        dim=-1,  # 토큰 차원의 점수를 확률로 변환
    )
    print(temperature, probabilities)
실행 결과
0.7 tensor([0.7710, 0.1848, 0.0443])
1.0 tensor([0.6652, 0.2447, 0.0900])
1.3 tensor([0.5959, 0.2761, 0.1280])
  • 낮은 온도: 첫 토큰의 확률이 0.7710으로 증가
  • 높은 온도: 세 토큰의 확률 차이가 감소

같은 입력으로 비교

  • 비교 원칙: 일반 문자열과 최대 생성 길이를 고정하고 디코딩 설정만 변경
prompt = "인공지능은 데이터를 학습하여 패턴을 찾는다. 이를 활용하면"  # 공통 입력
inputs = tokenizer(
    prompt,
    return_tensors="pt",  # 토큰 ID를 PyTorch 텐서로 반환
).to(model.device)         # 입력을 모형과 같은 장치로 이동

input_length = inputs["input_ids"].shape[1]  # 입력 토큰 수
inputs["input_ids"].shape  # 배치 크기와 입력 토큰 수
실행 결과
torch.Size([1, 16])
  • sample_text(): 난수 시드와 디코딩 설정을 받아 새로 생성한 부분만 복원
def sample_text(seed, **decoding_args):  # 디코딩 설정을 키워드 인자로 받음
    torch.manual_seed(seed)  # 반복 비교를 위해 난수 시드 고정
    with torch.inference_mode():  # 기울기 계산을 꺼 메모리와 연산 절약
        output_ids = model.generate(
            **inputs,           # input_ids와 attention_mask 전달
            max_new_tokens=24,  # 입력 뒤에 최대 24개 토큰 생성
            do_sample=True,     # 확률분포에서 다음 토큰을 추출
            use_cache=True,     # 이전 토큰의 Key·Value를 KV 캐시에 저장해 재사용
            **decoding_args,    # temperature, top_k, top_p 등을 전달
        )
    return tokenizer.decode(
        output_ids[0, input_length:],  # 첫 결과에서 입력을 제외한 토큰만 선택
        skip_special_tokens=True,     # 제어용 특수 토큰은 문자열에서 제외
    )

온도에 따른 생성

  • 비교 조건: 난수 시드, top-k, top-p를 고정하고 온도만 변경
for temperature in [0.7, 1.0, 1.3]:
    text = sample_text(
        seed=5,                  # 동일한 난수 순서로 비교
        temperature=temperature,  # 현재 반복의 온도
        top_k=50,                # 상위 50개 토큰만 후보로 유지
        top_p=1.0,               # 누적 확률에 따른 후보 제한은 끄기
    )
    print(temperature, text)
실행 결과
0.7  더 큰 규모의 시스템과의 호환성이 보장될 수 있습니다.
1.0  더 큰 규모의 시스템과의 호환성이 emerges합니다.
1.3  더 큰 규모의 계획과 결합을 수행할 수 있다.
  • 온도 변화: 같은 난수를 사용해도 서로 다른 토큰을 선택
  • 온도 1.0의 결과: 한국어 문장에 영어 토큰 emerges가 섞임
    • 무작위 추출에서는 확률이 높지 않은 토큰도 뽑힐 수 있음을 보여 줌
  • 생성 품질: 온도가 높거나 낮다는 사실만으로 결정되지 않음

Top-k 추출

  • Top-k 추출: 확률이 높은 개 토큰만 남기고 그 안에서 다음 토큰을 추출
  • 작은 : 확률이 높은 소수의 토큰에 집중
  • : 선택 범위가 넓어지지만 낮은 확률의 토큰도 포함될 수 있음
for top_k in [5, 20, 50]:
    text = sample_text(
        seed=1,            # 동일한 난수 순서로 비교
        temperature=1.0,  # 원래 확률분포 유지
        top_k=top_k,       # 현재 반복의 후보 개수
        top_p=1.0,         # 누적 확률에 따른 후보 제한은 끄기
    )
    print(top_k, text)
실행 결과
5  데이터를 직접적으로 학습할 수 있습니다.
20  데이터를 직접 업데이트할 수 있는 구조를 가지게 할 수 있으며, 필요성까지 고려할 수 있
50  데이터를 직접 가지는 상황을 말할 수 있다.
  • top_k=5: 가장 높은 확률의 토큰 다섯 개 안에서만 선택
  • 의 증가: 후보가 늘어나며 생성 결과도 달라짐
  • 최대 생성 길이: 두 번째 결과는 문장을 끝내기 전에 24개 토큰에 도달

Top-p 추출

  • Top-p 추출: 확률이 높은 순서로 토큰을 모아 누적 확률이 이상이 되는 최소 후보 집합에서 추출
  • 핵 추출(nucleus sampling): Top-p 추출의 다른 이름
  • 후보 수: 각 단계의 확률분포에 따라 달라짐
    • 확신이 높은 분포에서는 적은 토큰만 선택
    • 확률이 고른 분포에서는 많은 토큰을 선택
  • top_k=0: top-k 제한을 끄고 top-p의 효과만 비교
for top_p in [0.7, 0.9, 0.95]:
    text = sample_text(
        seed=5,            # 동일한 난수 순서로 비교
        temperature=1.0,  # 원래 확률분포 유지
        top_k=0,           # top-k 후보 제한은 끄기
        top_p=top_p,       # 현재 반복의 누적 확률 기준
    )
    print(top_p, text)
실행 결과
0.7  더 큰 규모의 시스템이 가능하다고 할 수 있다.
0.9  최신 정보를 계속해서 수렴시킬 수 있는 제품이 필요하다.
0.95  최신 정보를 계속해서 붙여서 더다는 의도가 된다는 사실이 정답입니다. 최종 답안 리스트를 환경부 가상으로 선정
  • 낮은 : 확률이 높은 소수의 후보에 집중
  • 높은 : 후보 범위가 넓어져 예상하기 어려운 표현이 포함될 수 있음

온도와 후보 제한 조합

  • 실제 생성: 온도, top-k, top-p를 함께 사용하는 경우가 많음
  • 아래 설정: 온도를 조금 낮추고 낮은 확률의 꼬리 부분을 제한
  • 비교 조건: 디코딩 설정은 고정하고 난수 시드만 변경
for seed in [3, 6, 9]:
    text = sample_text(
        seed=seed,        # 현재 반복의 난수 시드
        temperature=0.8,  # 상위 확률 토큰에 조금 더 집중
        top_k=50,         # 상위 50개 토큰만 후보로 유지
        top_p=0.9,        # 누적 확률 0.9에 도달하는 최소 후보만 유지
    )
    print(seed, text)
실행 결과
3  추가 정보를 수집할 수 있다.
6  데이터를 정제하는 데 필요한 시간을 측정할 수 있다.
9  추가 정보가 있을 수 있다.
  • 같은 설정: 난수 시드에 따라 서로 다른 문장을 생성
  • 매개변수 선택: 다양성뿐 아니라 문맥 적합성, 반복, 문법도 함께 확인

실행 결과의 차이

  • 난수 시드가 같아도 GPU 종류와 라이브러리 버전이 다르면 결과가 달라질 수 있음

방법 비교

방법후보를 정하는 기준값을 높이면
온도전체 확률분포의 평탄한 정도낮은 확률의 토큰을 선택할 가능성 증가
Top-k확률이 높은 토큰의 개수후보 수 증가
Top-p후보의 누적 확률후보 수가 대체로 증가
  • 공통점: do_sample=True에서 확률에 따라 다음 토큰을 추출
  • 차이점: 온도는 확률분포를 바꾸고, top-k와 top-p는 추출할 후보를 제한
Previous
KV 캐시와 생성 속도