확률적 디코딩
온도, 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는 추출할 후보를 제한