Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

어휘 확장

미세조정의 한 가지 활용 사례입니다. 기존 토크나이저가 비효율적으로 쪼개는 언어(예: 한국어)에 새 어휘를 추가하고, 그 임베딩을 미세조정으로 학습시켜 토큰 효율과 성능을 끌어올리는 과정을 다룹니다.

1어휘와 임베딩 크기

1.1BERT

from transformers import AutoTokenizer, AutoModel

def 임베딩관찰(model_id, texts):
    print(model_id)
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    model = AutoModel.from_pretrained(model_id)
    print(model.embeddings)
    어휘수 = len(tokenizer)
    print(f"어휘수: {어휘수:,}")
    assert 어휘수 == model.embeddings.word_embeddings.num_embeddings
    print('특수 토큰:', tokenizer.special_tokens_map)
    encoded_texts = tokenizer(texts)
    for i, text in enumerate(texts):
        input_ids = encoded_texts['input_ids'][i]
        unknown_count = sum(1 for token_id in input_ids if token_id == tokenizer.unk_token_id)
        print(f"원본 텍스트: {text}")
        print(f"토큰화된 텍스트: {input_ids} (어휘밖단어: {unknown_count}/{len(input_ids) - 2})")
        print('/'.join(tokenizer.convert_ids_to_tokens(input_ids)))
texts = ['I ate an apple in the Apple Store.', '배 타고 배 멀미한다.']
임베딩관찰('google-bert/bert-base-uncased', texts)
google-bert/bert-base-uncased
BertEmbeddings(
  (word_embeddings): Embedding(30522, 768, padding_idx=0)
  ...
)
어휘수: 30,522
특수 토큰: {'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'}
원본 텍스트: I ate an apple in the Apple Store.
토큰화된 텍스트: [101, 1045, 8823, 2019, 6207, 1999, 1996, 6207, 3573, 1012, 102] (어휘밖단어: 0/9)
[CLS]/i/ate/an/apple/in/the/apple/store/./[SEP]
원본 텍스트: 배 타고 배 멀미한다.
토큰화된 텍스트: [101, 1460, 30007, 1467, ..., 1012, 102] (어휘밖단어: 0/19)
[CLS]/ᄇ/##ᅢ/ᄐ/##ᅡ/##ᄀ/##ᅩ/ᄇ/##ᅢ/ᄆ/##ᅥ/##ᆯ/##ᄆ/##ᅵ/##ᄒ/##ᅡ/##ᆫ/##ᄃ/##ᅡ/./[SEP]
texts = ['I ate an apple in the Apple Store.', '배 타고 배 멀미한다.']
임베딩관찰('google-bert/bert-base-cased', texts)
google-bert/bert-base-cased
...
어휘수: 28,996
원본 텍스트: I ate an apple in the Apple Store.
토큰화된 텍스트: [101, 146, 8756, 1126, 12075, 1107, 1103, 7302, 10422, 119, 102] (어휘밖단어: 0/9)
[CLS]/I/ate/an/apple/in/the/Apple/Store/./[SEP]
원본 텍스트: 배 타고 배 멀미한다.
토큰화된 텍스트: [101, 100, 100, 100, 100, 119, 102] (어휘밖단어: 4/5)
[CLS]/[UNK]/[UNK]/[UNK]/[UNK]/./[SEP]
texts = ['I ate an apple in the Apple Store.', '배 타고 배 멀미한다.']
임베딩관찰('google-bert/bert-base-multilingual-uncased', texts)
google-bert/bert-base-multilingual-uncased
...
어휘수: 105,879
원본 텍스트: 배 타고 배 멀미한다.
토큰화된 텍스트: [101, 1170, 26179, 1179, 67384, 1170, 26179, 1169, 84098, 22699, 14624, 119, 102] (어휘밖단어: 0/11)
[CLS]/ᄇ/##ᅢ/ᄐ/##ᅡ고/ᄇ/##ᅢ/ᄆ/##ᅥᆯ/##미/##한다/./[SEP]
texts = ['I ate an apple in the Apple Store.', '배 타고 배 멀미한다.']
임베딩관찰('google-bert/bert-base-multilingual-cased', texts)
google-bert/bert-base-multilingual-cased
...
어휘수: 119,547
원본 텍스트: 배 타고 배 멀미한다.
토큰화된 텍스트: [101, 9330, 9845, 11664, 9330, 9268, 22458, 14102, 119, 102] (어휘밖단어: 0/8)
[CLS]/배/타/##고/배/멀/##미/##한다/./[SEP]

1.2GPT 2

model_id = 'gpt2'
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModel.from_pretrained(model_id)
print(f'어휘수: {len(tokenizer):,}, 임베딩 크기: {model.wte.weight.shape}')
assert model.wte.weight.shape[0] == len(tokenizer)
print('특수 토큰:', tokenizer.special_tokens_map)

texts = ['I ate an apple in the Apple Store.', '배 타고 배 멀미한다.']
encoded_texts = tokenizer(texts)
for i, text in enumerate(texts):
    input_ids = encoded_texts['input_ids'][i]
    unknown_count = sum(1 for token_id in input_ids if token_id == tokenizer.unk_token_id)
    print(f"원본 텍스트: {text}")
    print(f"토큰화된 텍스트: {input_ids} (어휘밖단어: {unknown_count}/{len(input_ids) - 2})")
    print('/'.join(tokenizer.convert_ids_to_tokens(input_ids)))
어휘수: 50,257, 임베딩 크기: (50257, 768)
특수 토큰: {'bos_token': '<|endoftext|>', 'eos_token': '<|endoftext|>', 'unk_token': '<|endoftext|>'}
원본 텍스트: I ate an apple in the Apple Store.
토큰화된 텍스트: [40, 15063, 281, 17180, 287, 262, 4196, 9363, 13] (어휘밖단어: 0/7)
I/Ġate/Ġan/Ġapple/Ġin/Ġthe/ĠApple/ĠStore/.
원본 텍스트: 배 타고 배 멀미한다.
토큰화된 텍스트: [167, 108, 108, ..., 46695, 97, 13] (어휘밖단어: 0/22)
ë/°/°/Ġ/í/ĥ/Ģ/...        # 한국어가 바이트 단위로 잘게 쪼개짐

1.3KoGPT2

model_id = 'skt/kogpt2-base-v2'
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModel.from_pretrained(model_id)
print(f'어휘수: {len(tokenizer):,}, 임베딩 크기: {model.wte.weight.shape}')
assert model.wte.weight.shape[0] == len(tokenizer) - 1 # SKT KoGPT2는 특수 토큰을 임베딩에서 제외
print('특수 토큰:', tokenizer.special_tokens_map)

texts = ['I ate an apple in the Apple Store.', '배 타고 배 멀미한다.']
encoded_texts = tokenizer(texts)
for i, text in enumerate(texts):
    input_ids = encoded_texts['input_ids'][i]
    unknown_count = sum(1 for token_id in input_ids if token_id == tokenizer.unk_token_id)
    print(f"원본 텍스트: {text}")
    print(f"토큰화된 텍스트: {input_ids} (어휘밖단어: {unknown_count}/{len(input_ids) - 2})")
    print('/'.join(tokenizer.convert_ids_to_tokens(input_ids)))
어휘수: 51,201, 임베딩 크기: (51200, 768)
특수 토큰: {'bos_token': '<|endoftext|>', 'eos_token': '<|endoftext|>', 'unk_token': '<|endoftext|>'}
원본 텍스트: I ate an apple in the Apple Store.
토큰화된 텍스트: [415, 16642, 9574, 47562, 10397, 9610, 34626, 407, 30258, 10397, 38218, 21663, 389] (어휘밖단어: 0/11)
I/ate/an/app/le/in/the/A/pp/le/St/ore/.

영어 전용 토크나이저(BERT-uncased/cased, GPT-2)는 한국어를 자모·바이트 단위로 잘게 쪼개거나 [UNK]로 처리해 토큰이 길어지고 의미가 흩어집니다. 다국어(multilingual)·한국어 모델일수록 한국어를 더 긴 단위로 묶어 효율적으로 토큰화합니다.

2데이터셋

from datasets import load_dataset

# 공개 한국어-영어 병렬 말뭉치(AI Hub 통합본). 실습을 위해 일부만 사용합니다.
dataset_id = 'traintogpb/aihub-koen-translation-integrated-base-1m'
한영쌍 = load_dataset(dataset_id, split='train[:20000]')

3형태소 분석

import sentencepiece as spm

with open('ko.txt', 'w', encoding='utf-8') as 파일:
    for 문장 in 한영쌍['ko']:
        파일.write(문장 + '\n')
# 저장된 파일 확인
with open('ko.txt', 'r', encoding='utf-8') as 파일:
    for 줄, 문장 in zip(range(5), 파일):
        print(f'[{줄}]', 문장.strip())
        
spm.SentencePieceTrainer.train(
    input='ko.txt',
    model_prefix='spm_ko',
    vocab_size=10000,)
[0] 지방세법 개정안에 따른 종합부동산세는 부과징수권자의 경우를 제외하고는 대부분 종전 종 합부동산세와 동일하다.
[1] 실신 당시 5초 정도 의식소실이 있었고 바로 의식은 회복되으며 신경학적 이상증상은 나타나지 않았다고 한다.
[2] 문 대통령의 10월 방일이 성사될 경우 한·일 양국은 일본군 위안부 합의 이행 문제와 독도 문제 등으로 삐걱대는 양국 관계를 개선할 계기를 만들 것이라고 신문은 전망했다.
[3] 인덱스 펀드에 대한 투자에서는 장기적인 관점에서 수익을 얻을 수 있습니다.
[4] 돌봐야 하는 비용도 있지.
한국어_형태분석기 = spm.SentencePieceProcessor(model_file='spm_ko.model')

예문 = '한국에서는 한글을 사용합니다.'
한국어_형태분석기.encode_as_pieces(예문)
어휘목록 = 한국어_형태분석기
['▁한국에서', '는', '▁한', '글', '을', '▁사용', '합니다', '.']

3.1어휘 획득

어휘목록 = []
with open("spm_ko.vocab", encoding="utf-8") as f:
    for line in f:
        token, _ = line.strip().split("\t")
        if not token.startswith("<"):  # 특수 토큰 제외
            어휘목록.append(token)

print(어휘목록[:20])  # 상위 토큰 확인
['.', '▁', '의', '을', ',', '에', '를', '는', '이', '가', '은', '▁수', '한', '▁있다', '과', '로', '도', '할', '고', '에서']

4사전 훈련 모형

from transformers import GPT2LMHeadModel, PreTrainedTokenizerFast

model_id = 'skt/kogpt2-base-v2'
model = GPT2LMHeadModel.from_pretrained(model_id)
tokenizer = PreTrainedTokenizerFast.from_pretrained(
    model_id, 
    # 특수 토큰 정의
    unk_token='<unk>', # 어휘 밖(OOV; out of vocabulary) 또는 "unknown"
    bos_token='<s>', # 문장의 시작(BOS; beginning of sentence)
    eos_token='</s>', # 문장의 끝(EOS; end of sentence)
    pad_token='<pad>') # 패딩 토큰 (길이 정규화용. 빈문자열)

4.1어휘 추가

print(len(tokenizer))

tokens_to_add = [토큰 for 토큰 in 어휘목록 if 토큰 not in tokenizer.get_vocab()]
tokenizer.add_tokens(tokens_to_add)
print(len(tokenizer))
print("추가된 토큰 수:", len(tokens_to_add))
51200
54144
추가된 토큰 수: 2944
tokens_to_add[:10]
['이다', '했다', '▁있습니다', '▁것이다', '된다', '하였다', '입니다', '합니다', '▁AAA', '습니다']
text = 'AAA는 BBB 서비스를 제공합니다.'
print("원본 텍스트:", text)
print("토큰화된 입력:", tokenizer.tokenize(text))
encoded_input = tokenizer.encode(text)
print("인코딩된 입력:", encoded_input)
inputs = tokenizer(text, truncation=True, padding="max_length", max_length=10)
print('PAD 토큰:', tokenizer.pad_token_id)
print("토큰화된 입력:", inputs['input_ids'])
원본 텍스트: AAA는 BBB 서비스를 제공합니다.
토큰화된 입력: ['AAA', '▁는', '▁', 'BBB', '▁서비스를', '▁제공', '합니다', '▁.']
인코딩된 입력: [54064, 33297, 739, 53421, 13796, 10312, 51207, 36510]
PAD 토큰: 3
토큰화된 입력: [54064, 33297, 739, 53421, 13796, 10312, 51207, 36510, 3, 3]

추가한 어휘 덕분에 AAA·BBB·서비스를·제공처럼 새 토큰이 통째로 잡혀, 같은 문장이 더 적은 토큰으로 표현됩니다.

5미세 조정

5.1데이터 준비

def tokenize(example):
    tokens = tokenizer(example["ko"], truncation=True, padding="max_length", max_length=256)
    # 목표 출력을 입력과 동일하게 설정
    tokens["labels"] = tokens["input_ids"].copy()
    return tokens

tokenized_dataset = 한영쌍.map(
    tokenize, batched=True, remove_columns=['ko', 'en', 'source'])

tokenized_dataset.set_format(
    type="torch",
    columns=["input_ids", "attention_mask", "labels"]
)

5.2학습 설정

from transformers import TrainingArguments, Trainer

training_args = TrainingArguments(
    output_dir="./kogpt2-extended",
    num_train_epochs=1,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    logging_steps=100,
    learning_rate=5e-5,
    warmup_steps=500,
    weight_decay=0.01,
    fp16=True,
    save_strategy="steps",
    eval_strategy="no",
    report_to=[],
)

5.3학습

from transformers import default_data_collator

model.resize_token_embeddings(len(tokenizer))

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    processing_class=tokenizer,
    data_collator=default_data_collator,
)
trainer.train()
{'loss': 4.459, 'learning_rate': 9.9e-06, 'epoch': 0.08}
{'loss': 0.5313, 'learning_rate': 1.99e-05, 'epoch': 0.16}
...
{'loss': 0.4672, 'learning_rate': 1.007e-05, 'epoch': 0.88}
{'loss': 0.4648, 'learning_rate': 3.4e-06, 'epoch': 0.96}
TrainOutput(global_step=1250, training_loss=0.8072,
  metrics={'train_runtime': 340.9, 'train_samples_per_second': 58.67, 'epoch': 1.0})
model.save_pretrained("kogpt2-finetuned")
tokenizer.save_pretrained("kogpt2-finetuned")

6응용

from transformers import pipeline

generator = pipeline("text-generation", model="kogpt2-finetuned", tokenizer=tokenizer)
outputs = generator("AAA가 BBB에게", max_length=100)
print(outputs[0]['generated_text'])
AAA가 BBB에게 물어보고 싶습니다.

새로 추가한 AAA·BBB 어휘가 미세조정을 거쳐 자연스러운 문장 생성에 쓰입니다.