Text generation

Text generation

대형 언어 모델(LLM)에서 가장 많이 쓰는 기능은 역시 텍스트 생성이에요. LLM은 주어진 초기 텍스트(프롬프트)와 그동안 스스로 만들어 낸 출력을 바탕으로 다음 단어(토큰)를 생성하도록 학습돼요. 미리 정해진 길이에 도달하거나 종료 시퀀스(EOS) 토큰에 닿을 때까지 이 과정을 반복하죠. Transformers에서는 generate() API가 텍스트 생성의 모든 것을 담당해요. 생성 능력이 있는 모든 모델에서 사용할 수 있어요. 이 가이드에서는 generate()의 기본 사용법과 자주 겪게 될 함정 몇 가지를 살펴볼게요.

출처: Text generation · Hugging Face Transformers

기본 generate

시작 전에 매우 큰 모델을 양자화해 메모리 사용량을 줄이려면 bitsandbytes를 설치해 두면 좋아요.

!pip install -U transformers bitsandbytes

Bitsandbytes는 CUDA 기반 GPU 외에도 여러 백엔드를 지원해요. 멀티백엔드 설치 가이드를 참고해요.

from_pretrained()로 LLM을 불러올 때 메모리 요구량을 줄이기 위해 두 파라미터를 추가해요.

  • device_map="auto"는 Accelerate의 Big Model Inference 기능을 켜서 사용 가능한 모든 디바이스(가장 빠른 GPU부터)에 모델 가중치를 자동으로 분배해요.
  • quantization_config는 양자화 설정을 정의하는 구성 객체예요. 이 예시는 bitsandbytes를 양자화 백엔드로 쓰고 모델을 4-bits로 불러와요.
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(load_in_4bit=True)
model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1", device_map="auto", quantization_config=quantization_config)

입력을 토크나이즈하고, LLM은 패딩 토큰부터 생성을 이어가도록 학습되지 않았으므로 padding_side 파라미터를 "left"로 설정해요. 토크나이저는 input ids와 attention mask를 반환해요.

토크나이저에 문자열 리스트를 넘기면 여러 프롬프트를 한 번에 처리할 수 있어요. 배치를 쓰면 지연시간과 메모리 비용이 조금 늘지만 처리량은 크게 올라가요.

tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1", padding_side="left")
model_inputs = tokenizer(["A list of colors: red, blue"], return_tensors="pt").to(model.device)

generate()에 입력을 넣어 토큰을 생성하고, batch_decode()로 생성된 토큰을 다시 텍스트로 바꿔요.

generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
"A list of colors: red, blue, green, yellow, orange, purple, pink,"

Generation configuration

모든 생성 설정은 GenerationConfig에 담겨요. 위 예시에서 생성 설정은 mistralai/Mistral-7B-v0.1generation_config.json 파일에서 비롯돼요. 모델에 저장된 구성이 없으면 기본 디코딩 전략이 사용돼요.

generation_config 속성으로 구성을 확인할 수 있어요. 기본 구성과 다른 값만 보여주는데, 이 경우 bos_token_ideos_token_id네요.

from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1", device_map="auto")
model.generation_config
GenerationConfig {
  "bos_token_id": 1,
  "eos_token_id": 2
}

GenerationConfig의 파라미터와 값을 덮어써서 generate()를 커스터마이즈할 수 있어요. 아래는 자주 조정하는 파라미터예요.

# enable beam search sampling strategy
model.generate(**inputs, num_beams=4, do_sample=True)

generate()는 외부 라이브러리나 커스텀 코드로도 확장할 수 있어요.

  1. logits_processor 파라미터는 다음 토큰 확률 분포를 조작하는 커스텀 LogitsProcessor 인스턴스를 받아요.
  2. stopping_criteria 파라미터는 텍스트 생성을 멈추는 커스텀 StoppingCriteria를 지원해요.
  3. custom_generate 플래그로 다른 커스텀 생성 메서드를 불러올 수 있어요.

탐색·샘플링·디코딩 전략에 대한 더 자세한 내용은 Generation strategies 가이드를 참고해요.

저장

GenerationConfig 인스턴스를 만들고 원하는 디코딩 파라미터를 지정해요.

from transformers import AutoModelForCausalLM, GenerationConfig

model = AutoModelForCausalLM.from_pretrained("my_account/my_model")
generation_config = GenerationConfig(
    max_new_tokens=50, do_sample=True, top_k=50, eos_token_id=model.config.eos_token_id
)

save_pretrained()로 특정 생성 구성을 저장하고 push_to_hub 파라미터를 True로 설정하면 허브에 업로드돼요.

generation_config.save_pretrained("my_account/my_model", push_to_hub=True)

config_file_name 파라미터는 비워 둬요. 이 파라미터는 한 디렉토리에 여러 생성 구성을 저장할 때 어떤 구성을 불러올지 지정하는 데 쓰여요. 하나의 모델에 여러 생성 작업(샘플링으로 창의적 생성, 빔 서치로 요약)을 위한 서로 다른 구성을 만들어 둘 수 있어요.

from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, GenerationConfig

tokenizer = AutoTokenizer.from_pretrained("google-t5/t5-small")
model = AutoModelForSeq2SeqLM.from_pretrained("google-t5/t5-small")

translation_generation_config = GenerationConfig(
    num_beams=4,
    early_stopping=True,
    decoder_start_token_id=0,
    eos_token_id=model.config.eos_token_id,
    pad_token=model.config.pad_token_id,
)

translation_generation_config.save_pretrained("/tmp", config_file_name="translation_generation_config.json", push_to_hub=True)

generation_config = GenerationConfig.from_pretrained("/tmp", config_file_name="translation_generation_config.json")
inputs = tokenizer("translate English to French: Configuration files are easy to use!", return_tensors="pt")
outputs = model.generate(**inputs, generation_config=generation_config)
print(tokenizer.batch_decode(outputs, skip_special_tokens=True))

흔한 선택지

generate()는 강력해서 크게 커스터마이즈할 수 있지만, 그래서 새 사용자에겐 부담스러울 수 있어요. 아래는 Transformers의 대부분 텍스트 생성 도구(generate(), GenerationConfig, pipelines, chat CLI)에서 공통으로 정의하는 인기 생성 옵션 목록이에요.

Option name Type 간단 설명
max_new_tokens int 최대 생성 길이를 조절해요. 보통 기본값이 작으니 반드시 지정해 주는 게 좋아요.
do_sample bool 다음 토큰을 샘플링할지(True), 탐욕적(greedy)으로 고를지(False)를 정해요. 대부분의 경우 True로 설정해요.
temperature float 다음으로 선택될 토큰의 예측 불가능성을 나타내요. 높은 값(>0.8)은 창의적 작업에, 낮은 값(예: <0.4)은 '생각'을 요하는 작업에 좋아요. do_sample=True가 필요해요.
num_beams int >1로 설정하면 빔 서치 알고리즘이 활성화돼요. 빔 서치는 입력에 근거한 작업에 좋아요.
repetition_penalty float 모델이 반복을 자주 한다면 >1.0으로 설정해요. 값이 클수록 패널티가 커져요.
eos_token_id list[int] 생성을 멈추게 하는 토큰이에요. 기본값이 보통 좋지만 다른 토큰을 지정할 수 있어요.

함정

텍스트 생성 중 겪는 흔한 문제와 해결법을 살펴볼게요.

출력 길이

모델의 GenerationConfig에 따로 지정하지 않으면 generate()는 기본적으로 최대 20개 토큰을 반환해요. max_new_tokens 파라미터로 생성할 토큰 수를 직접 지정하는 걸 강력히 권장해요. 디코더 전용 모델은 프롬프트와 생성된 토큰을 함께 반환해요.

model_inputs = tokenizer(["A sequence of numbers: 1, 2"], return_tensors="pt").to(model.device)
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'A sequence of numbers: 1, 2, 3, 4, 5'

max_new_tokens를 지정하지 않으면 위처럼 짧은 출력이 나와요.

디코딩 전략

generate()의 기본 디코딩 전략은 가장 가능성이 높은 다음 토큰을 고르는 greedy search예요. 전사·번역처럼 입력에 근거한 작업에는 잘 맞지만, 스토리 작성·챗 애플리케이션 같은 창의적인 용도에는 최적이 아니에요.

예를 들어 다항 샘플링(multinomial sampling) 전략을 켜면 더 다양한 출력을 만들 수 있어요. 더 많은 디코딩 전략은 Generation strategy 가이드를 참고해요.

패딩 위치

입력 길이가 서로 다르면 패딩이 필요해요. 그런데 LLM은 패딩 토큰부터 생성을 이어가도록 학습되지 않아서, padding_side 파라미터가 입력의 왼쪽으로 설정돼야 해요.

오른쪽 패딩을 쓰면 왼쪽에 패딩 토큰이 남아 모델이 패딩부터 생성하려다 잘못된 출력이 나올 수 있어요.

model_inputs = tokenizer(
    ["1, 2, 3", "A, B, C, D, E"], padding=True, return_tensors="pt"
).to(model.device)
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'1, 2, 33333333333'

프롬프트 형식

어떤 모델과 작업은 특정 입력 프롬프트 형식을 기대해서, 형식이 틀리면 최적이 아닌 출력을 돌려줘요. 예를 들어 챗 모델은 입력을 chat template으로 기대해요. 프롬프트에 누가 대화에 참여하는지 표시하는 rolecontent가 있어야 해요. 프롬프트를 단일 문자열로 넘기면 모델이 항상 기대한 출력을 주진 않아요.

from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-alpha")
model = AutoModelForCausalLM.from_pretrained(
    "HuggingFaceH4/zephyr-7b-alpha", device_map="auto", quantization_config=BitsAndBytesConfig(load_in_4bit=True)
)

chat template으로 프롬프트를 감싸면 훨씬 좋은 출력이 나와요. 형식 없이 단일 문자열로 넘기면 모델이 문맥을 제대로 파악하지 못해요.

prompt = """How many cats does it take to change a light bulb? Reply as a pirate."""
model_inputs = tokenizer([prompt], return_tensors="pt").to(model.device)
input_length = model_inputs.input_ids.shape[1]
generated_ids = model.generate(**model_inputs, max_new_tokens=50)
print(tokenizer.batch_decode(generated_ids[:, input_length:], skip_special_tokens=True)[0])
"Aye, matey! 'Tis a simple task for a cat with a keen eye and nimble paws. First, the cat will climb up the ladder, carefully avoiding the rickety rungs. Then, with"

리소스

텍스트 생성에 특화된 더 전문적인 라이브러리들을 소개할게요.

  • Optimum: 특정 하드웨어에서 학습·추론을 최적화하는 데 초점을 둔 Transformers 확장.
  • Outlines: 제약된 텍스트 생성 라이브러리(예: JSON 생성).
  • SynCode: 문맥 자유 문법으로 안내하는 생성 라이브러리(JSON, SQL, Python).
  • Text Generation Inference: LLM용 프로덕션 서버.
  • Text generation web UI: Gradio 기반 텍스트 생성 웹 UI.
  • logits-processor-zoo: 텍스트 생성을 제어하는 추가 logits 프로세서.

더 알아보기