2024년 6월 19일 수요일

NLP의 핵심. 토큰, 임베딩 모델 파인튜닝

이 글은 LLM NLP처리의 핵심인 토큰, 임베딩 모델 파인튜닝에 대한 내용을 간략히 다룬다. 여기서 토큰은 문장을 구성하는 단어로 가정하면 이해하기 쉽다. 토큰과 임베딩은 입력 시퀀스에 대한 출력을 학습, 예측할 때 훈련의 전제가 되는 LLM의 기본조건이다. 이에 대해 좀 더 깊게 이해해 보자.

TIKTOKEN 라이브러리

도메인 의존 정보와 토큰
의학과 같은 특별한 분야에서는 환각 현상 등으로 인해 ChatGPT와 같은 범용 LLM이 제대로 정보를 생성하지 못하는 경우가 많다. 이런 문제를 해결하기 위해, 전문 분야의 지식을 기존 LLM 모델을 이용해 재학습하는 방법이 생겨났는 데, 파인튜닝은 그 중에 한 방법이다. 

임베딩은 학습 모델이 입력되는 문장의 토큰 패턴을 통계적으로 계산하기 전, 토큰을 수치화시키는 함수이다. 임베딩 모델은 다양한 종류가 있다. 임베딩 모델은 토큰을 수치화하여 모델 학습에 사용하는 데 필요한 입력값을 출력한다. 이런 이유로, 토큰 사전과 임베딩 모델이 다르면, 제대로 된 모델 학습, 예측, 패턴 계산 결과를 얻기 어렵다. 다음 그림은 토큰이 수치화된 결과를 보여준다. 참고로, 토큰이 숫자로 표현되지 못하는 문제를 OOV(Out-Of-Vocabulary라 한다.
숫자 토큰화 결과

이런 이유로, LLM을 재학습하기 전에 어떤 토큰이 사용되었는 지, 임베딩 모델이 무엇인지 확인해야 한다. 일반적으로, 모델을 파인 튜닝하려면, LLM 토큰 확인 및 개발, 임베딩 모델의 적절한 사용이 필요하다. 

개발환경
실습을 위해 다음을 설치한다.
pip install transformers torch

참고로, 다음은 파인튜닝에 사용하는 도구를 보여준다.
  • Torch: 텐서 계산 및 딥 러닝을 위한 핵심 라이브러리이다.
  • peft: 낮은 순위의 적응 기술을 사용하여 대규모 언어 모델을 효율적으로 미세 조정할 수 있다. 특히 리소스가 제한된 장치에서 학습 가능한 매개 변수의 수를 줄여 모델을 압축하고 더 빠르게 미세 조정할 수 있다.
  • bitsandbytes: 신경망에 대한 양자화 및 이진화 기술을 제공하여 모델 압축을 지원한다. 모델 압축에 도움이 되므로 메모리와 계산 능력이 제한된 에지 장치에 모델을 보다 실현 가능하게 만들 수 있다.
  • Transformers: 대규모 언어 모델 작업을 간소화하여 사전 학습된 모델 및 학습 파이프라인을 제공한다.
  • trl: 대규모 언어 모델의 경우 효율적인 모델 학습 및 최적화에 중점을 둔다.
  • accelerate: 다양한 하드웨어 플랫폼에서 학습 및 추론을 가속화한다.
  • dataset: 기계 학습 작업을 위한 데이터 세트 로드 및 준비를 간소화한다.
  • pipeline: 사용자 지정 학습 없이 일반적인 NLP 작업에 대해 사전 학습된 모델의 사용을 간소화한다.
  • pyarrow: 효율적인 데이터 로드 및 처리를 위해 사용될 수 있다.
  • LoraConfig: LoRA 기반 미세 조정을 위한 구성 매개변수를 보유한다.
  • SFTTrainer: 모델 학습, 최적화 및 평가를 처리한다.
토큰 사전 
LLM 파인튜닝이나 RAG 시 토큰 사전이 없으면, 제대로 학습되지 않는다. 입력 시퀀스의 토큰이 사전에 없으면, 토큰은 분리된다. 분리된 토큰들은 각자 다른 맥락을 가지도록 학습된다. 다음 코드를 실행하면, 그 내용을 확인할 수 있다. 이런 문제는 유전자 해석 등 다양한 문제에서 발생된다. 
from transformers import DistilBertTokenizerFast, DistilBertModel

tokenizer = DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased")
tokens = tokenizer.encode('This is a IfcBuilding.', return_tensors='pt')
print("These are tokens!", tokens)
for token in tokens[0]:
    print("This are decoded tokens!", tokenizer.decode([token]))

model = DistilBertModel.from_pretrained("distilbert-base-uncased")
print(model.embeddings.word_embeddings(tokens))
for e in model.embeddings.word_embeddings(tokens)[0]:
    print("This is an embedding!", e)

다음 코드를 실행해보면, 좀 더 많은 문제점을 확인할 수 있다.
from transformers import BertTokenizer, BertModel
bert_tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

example_sen = (
    """
    The United States and Russia sought to lower the temperature in a 
    heated standoff over Ukraine,even as they reported no breakthroughs 
    in high-stakes talks on Friday aimed at preventing a feared Russian invasion
    """
)
print(bert_tokenizer.tokenize(example_sen))

결과는 다음과 같다. 
['the', 'united', 'states', 'and', 'russia', 'sought', 'to', 'lower', 'the', 'temperature', 'in', 'a', 'heated', 'stand', '##off', 'over', 'ukraine', ',', 'even', 'as', 'they', 'reported', 'no', 'breakthrough', '##s', 'in', 'high', '-', 'stakes', 'talks', 'on', 'friday', 'aimed', 'at', 'preventing', 'a', 'feared', 'russian', 'invasion']

이 경우, 토큰을 추가하거나, 하나로 합치는 것을 고려할 필요가 있다.

토큰 추가와 임베딩 공간
BERT를 이용해 토큰 사전과 임베딩을 실습해본다.
일반적으로 허깅페이스 라이브러리에서 LLM모델에 대한 토큰 추가가 가능한 tokenizer를 제공해 준다. 토큰을 추가하면, 임베딩 차원에 영향을 주므로, 해당 크기를 수정해야 한다. 다음은 이를 고려한 코드를 보여준다. 
from transformers import AutoTokenizer, AutoModelForCausalLM

# 사전 학습모델 및 토크나이저 로딩
model = AutoModelForCausalLM.from_pretrained('model-name')
tokenizer = AutoTokenizer.from_pretrained('model-name')

# 토큰 추가
new_tokens = ['newword1', 'newword2']
tokenizer.add_tokens(new_tokens)

# 임베딩 공간 리사이즈 
model.resize_token_embeddings(len(tokenizer))

# 추가된 토큰과 함께 파인튜닝. 
# (fine-tuning code here)

BPE(Byte Pair Encoding) 토큰 압축
BPE는 바이트 쌍 인코딩(Byte pair Encoding)을 의미하며, 데이터의 가장 일반적인 연속 바이트 쌍을 해당 데이터 내에 발생하지 않는 바이트로 대체하는 데이터 압축 형태이다. 결과 데이터에 대해 프로세스가 반복적으로 반복된다. 자연어 처리(NLP) 및 기계 학습의 맥락에서 BPE는 하위 단어 토큰화 방법으로 사용된다. 

단어를 보다 관리하기 쉬운 하위 단어나 기호로 분할하여 대규모 어휘를 효율적으로 인코딩할 수 있다. 이 접근 방식은 어휘의 크기를 크게 줄이고 희귀 단어나 OOV(어휘에서 벗어난) 용어를 처리하는 모델의 능력을 향상시킬 수 있다.

NLP에 BPE를 적용하는 기본 단계는 다음과 같다.
  • 텍스트를 단어로 나눈 다음 문자로 나누고 각 문자(또는 문자 시퀀스)의 빈도를 계산한다.
  • 인접한 문자 또는 문자 시퀀스의 가장 빈번한 쌍을 반복적으로 찾아, 이를 새로운 문자 시퀀스로 병합한다.
  • 원하는 어휘 크기에 도달할 때까지 또는 더 이상 병합으로 인해 어휘 크기가 크게 줄어들 때까지 병합 프로세스를 반복한다.
BPE는 언어 모델링, 텍스트 생성, 특히 BERT(Bidirection Encoder Representations from Transformers)와 같은 변환기 기반 모델과 같은 다양한 NLP 모델 및 작업에 널리 채택되어 광범위한 어휘를 효율적으로 처리하는 데 도움이 된다.

다음은 그 예제를 보여준다.
from tokenizers import Tokenizer, models, pre_tokenizers, trainers

tokenizer = Tokenizer(models.BPE()) # 토큰화 얻기

tokenizer.pre_tokenizer = pre_tokenizers.Whitespace() # 사용자 토큰 처리 객체
def custom_pre_tokenizer(sequence): # 사용자 토큰 정의
    # Define rules to combine tokens, e.g., "new word" -> "newword"
    combined_sequence = sequence.replace("new word", "newword")
    return combined_sequence

# 토큰 훈련. custom pre-tokenizer 활용함.
trainer = trainers.BpeTrainer()
tokenizer.train(files=["path/to/training/data.txt"], trainer=trainer, pre_tokenizer=custom_pre_tokenizer)

# 훈련된 토큰 저장
tokenizer.save("path/to/customized_tokenizer.json")

임베딩 모델 파인튜닝
다음은 토큰을 추가하고, 임베딩 모델을 파인튜닝하는 보여준다.

from transformers import BertTokenizerFast, BertModel
import torch
from torch import nn

# BERT 토크나이저 사전학습모델 로딩
tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')
print(tokenizer.tokenize("[CLS] Hello world, how are you?"))

print(tokenizer.tokenize("[newtoken] Hello world, how are you?"))
tokenizer.add_tokens(['[newtoken]'])

다음과 같이, [newtoken] 토큰 추가 전 테스트. 토큰이 한단어가 아닌 분할 출력된 것 확인
['[',
 'newt',
 '##oke',
 '##n',
 ']',
 'hello',
 'world',
 ',',
 'how',
 'are',
 'you',
 '?']

토큰을 추가하고 다시 토큰화를 한다.
tokenizer.add_tokens(['[newtoken]'])
tokenizer.tokenize("[newtoken] Hello world, how are you?")

제대로 토큰화가 된다. 
['[newtoken]', 'hello', 'world', ',', 'how', 'are', 'you', '?']

토큰값을 확인해 본다.
tokenized = tokenizer("[newtoken] Hello world, how are you?", add_special_tokens=False, return_tensors="pt")
print(tokenized['input_ids'])

tkn = tokenized['input_ids'][0, 0]
print("First token:", tkn)
print("Decoded:", tokenizer.decode(tkn))

다음과 같이, 토큰값이 잘 할당된 것을 알 수 있다.
tensor([[30522,  7592,  2088,  1010,  2129,  2024,  2017,  1029]])
First token: tensor(30522)
Decoded: [newtoken]

임베딩 모델 학습을 위한 BERT 로딩하고, 앞의 토큰 리스트를 모델에 입력한다.
model = BertModel.from_pretrained('bert-base-uncased')
print(model.embeddings)

try:
    out = model(**tokenized)
    out.last_hidden_state
except Exception as e:
    print(e)

임베딩 모델이 추가된 토큰을 학습하지 않았으므로, out of range 에러가 출력될 것이다. 
다음 코드로 BERT 모델의토큰 공간 크기를 확인해 본다.
weights = model.embeddings.word_embeddings.weight.data
print(weights.shape)

출력은 다음과 같이 30522이다.
torch.Size([30522, 768])

이제 [CLS] 토큰을 임베딩 모델에 추가해보자. 
new_weights = torch.cat((weights, weights[101:102]), 0)
new_emb = nn.Embedding.from_pretrained(new_weights, padding_idx=0, freeze=False)
print(new_emb)

다음과 같이 30523으로 토큰 크기가 증가되었다. 
Embedding(30523, 768, padding_idx=0)

새 레이어를 모델 마지막에 추가한다.
model.embeddings.word_embeddings = new_emb
print(model.embeddings)

그 결과로 임베딩 모델의 word_embeddings가 업데이트된다.
BertEmbeddings(
  (word_embeddings): Embedding(30523, 768, padding_idx=0)
  (position_embeddings): Embedding(512, 768)
  (token_type_embeddings): Embedding(2, 768)
  (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
  (dropout): Dropout(p=0.1, inplace=False)
)

앞의 토큰 시퀀스 리스트를 입력한다. 그럼, 제대로 결과가 출력될 것이다.
out = model(**tokenized)
print(out.last_hidden_state)

다음 코드를 실행하면, 추가된 모델이 동일한 결과를 가지는 것을 알 수 있다.
model = BertModel.from_pretrained('bert-base-uncased')
out2 = model(
    **tokenizer("[CLS] Hello world, how are you?", add_special_tokens=False, return_tensors="pt")
)

out3 = torch.all(out.last_hidden_state == out2.last_hidden_state)
output(out3)

마무리
LLM 파인튜닝이나 RAG 시 학습 데이터에 포함된 토큰에 대한 적절한 사전이 없으면, 제대로 학습되지 않는다. 이 글은 LLM NLP처리의 핵심인 토큰, 임베딩 모델 파인튜닝에 대한 내용을 간략히 다룬다. 

레퍼런스

댓글 없음:

댓글 쓰기