어휘 확장
미세조정의 한 가지 활용 사례입니다. 기존 토크나이저가 비효율적으로 쪼개는 언어(예: 한국어)에 새 어휘를 추가하고, 그 임베딩을 미세조정으로 학습시켜 토큰 효율과 성능을 끌어올리는 과정을 다룹니다.
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
추가된 토큰 수: 2944tokens_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 어휘가 미세조정을 거쳐 자연스러운 문장 생성에 쓰입니다.