임베딩 모델에 새로운 단어 가르치기
Hugging Face sentence-transformer 모델에 새 어휘를 추가하고 fine-tuning한 뒤 LangChain에서 쓰는 단계별 가이드.
Hugging Face에 있는 사전학습 임베딩 모델들은 대단히 강력하다. 방대한 어휘의 의미와 맥락을 이해한다. 그런데 모델이 한 번도 본 적 없는 단어가 들어간 텍스트를 다뤄야 한다면 어떻게 될까?
이런 것들을 떠올려 보자:
- 기술 전문 용어: “Gecko-Embeddings”
- 틈새 용어: “DeepLearningGenius”
모델이 이 단어들을 본 적이 없다면, 의미 없는 subword로 쪼갤 가능성이 크다 (예: “XIGN” + “CODE” 또는 “xig” + “##nco” + “##de”). 이러면 용어의 고유한 의미가 사라진다.
해법은 새 단어를 모델의 어휘에 추가하고, 그 의미를 학습하도록 fine-tuning하는 것이다. 이 글에서는 sentence-transformers 라이브러리를 예로 그 방법을 보인다.
새 단어를 추가하는 두 단계
새 단어를 추가하는 건 한 단계가 아니다. 모델의 “사전”(tokenizer)과 “뇌”(모델 가중치)를 둘 다 갱신해야 한다.
- tokenizer 갱신: 먼저 새 단어가 하나의 완결된 단위로 존재한다고 tokenizer에게 알려준다. 이러면 subword로 쪼개지지 않는다.
- 모델 갱신 및 fine-tuning: 단어를 추가하면 모델이 임베딩 행렬에 “자리”를 만들지만, 이 새 임베딩 벡터는 그저 랜덤 노이즈다. 아무 의미가 없다. 그 단어가 들어간 새 문장들로 모델을 반드시 fine-tuning해서, 맥락으로부터 단어의 의미를 배우게 해야 한다.
새 토큰 추가하기 (코드)
sentence-transformers로 모델을 불러오고 내부 tokenizer와 임베딩 행렬을 수정해 보자.
from sentence_transformers import SentenceTransformer
import torch
# 베이스 모델 로드
model_name = 'sentence-transformers/all-MiniLM-L6-v2'
model = SentenceTransformer(model_name)
# 1. 내부 tokenizer와 모델 얻기
# 수정하려면 하위의 'transformers' 모델에 접근해야 한다
tokenizer = model.tokenizer
transformer_model = model.auto_model
print(f"Original vocabulary size: {len(tokenizer)}")
# 2. 새 단어 정의
# 중요: 모델이 'cased'인지 'uncased'인지 먼저 확인할 것!
# 이 모델('all-MiniLM-L6-v2')은 uncased라서 전부 소문자로 바꾼다.
# 소문자 버전만 추가하면 된다.
# 'cased' 모델을 쓴다면 이렇게 추가할 수도 있다: ['XIGNCODE', 'xigncode', 'Xigncode']
new_words = ['xigncode']
# 3. tokenizer에 새 토큰 추가
num_added_toks = tokenizer.add_tokens(new_words)
if num_added_toks > 0:
print(f"Added {num_added_toks} new tokens.")
# 4. 모델의 토큰 임베딩 크기 조정
# 새 토큰(들)을 위해 무작위로 초기화된 벡터가 추가된다
transformer_model.resize_token_embeddings(len(tokenizer))
print(f"New vocabulary size: {len(tokenizer)}")
else:
print("Tokens already exist in the vocabulary.")
# tokenizer 테스트
test_sentence = "This is a test of xigncode and XIGNCODE3."
tokenized = tokenizer.tokenize(test_sentence)
print(f"Tokenization test: {tokenized}")
# 기대 출력: ['this', 'is', 'a', 'test', 'of', 'xigncode', 'and', 'xigncode', '3']
위 출력에서 두 가지를 눈여겨보자:
-
xigncode가 단일 토큰으로 인식됐다. -
XIGNCODE3는['xigncode', '3']으로 토큰화됐다. “최장 일치” 규칙이 작동한 것이고, 우리가 원하던 결과다!
fine-tuning (의미 가르치기)
이제 모델이 “xigncode”를 인식 하니, 그것이 무엇을 의미하는지 가르쳐야 한다. 그 단어가 맥락 속에서 쓰인 문장들로 fine-tuning하면 된다.
from torch.utils.data import DataLoader
from sentence_transformers import InputExample, losses, models
# 1. 학습 예제 만들기
# 새 단어에 맥락을 제공하는 문장이 필요하다.
train_examples = [
# 맥락 부여: "xigncode"는 "anti-cheat"과 관련이 있다
InputExample(texts=['xigncode is an anti-cheat solution.', 'This software prevents cheating in online games.']),
# 또 다른 맥락 부여:
InputExample(texts=['Many gamers are familiar with xigncode.', 'It is a well-known security program.'])
# 더 잘 배우려면 예제를 훨씬 많이 추가할 것...
]
# 2. 데이터로더와 손실 함수 설정
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
train_loss = losses.MultipleNegativesRankingLoss(model=model)
# 3. 모델 fine-tuning
num_epochs = 1
warmup_steps = int(len(train_dataloader) * num_epochs * 0.1)
print("Starting fine-tuning...")
model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=num_epochs,
warmup_steps=warmup_steps,
output_path='./my-finetuned-model',
show_progress_bar=True
)
print("Fine-tuning complete. Model saved to './my-finetuned-model'.")
새 모델 사용하기 (LangChain과 함께)
더 똑똑해진 새 모델이 ./my-finetuned-model 디렉토리에 저장됐다. sentence-transformers에서 직접 불러올 수도 있고, 저장 경로만 가리키면 LangChain에서도 쓸 수 있다.
from langchain_huggingface import HuggingFaceEmbeddings
# 로컬에 fine-tuning된 모델 디렉토리를 가리킨다
model_path = "./my-finetuned-model"
# LangChain이 모델과 새 tokenizer를 함께 로드한다
embeddings = HuggingFaceEmbeddings(
model_name=model_path,
model_kwargs={'device': 'cpu'}, # 가능하면 'cuda'
encode_kwargs={'normalize_embeddings': True}
)
# 이제 이 텍스트가 더 정확한 임베딩을 만든다
text = "Tell me about the xigncode anti-cheat."
query_embedding = embeddings.embed_query(text)
print("Successfully created embedding with the new word!")
print(f"Vector dimension: {len(query_embedding)}")
놓치기 쉬운 지점: 대소문자와 subword
기억해둘 만한 중요한 지점 몇 가지:
XIGNCODE vs. xigncode (대소문자 구분)
- Uncased 모델 (
all-MiniLM-L6-v2같은): 전부 자동으로 소문자화한다. 소문자xigncode만 추가하면 된다.XIGNCODE,xigncode,XignCode모두 매칭된다. - Cased 모델 (
bert-base-cased같은):XIGNCODE와xigncode를 서로 다른 단어로 취급한다.XIGNCODE를 추가하면 정확히 그 대소문자 형태만 매칭된다. cased 모델에서는 등장할 만한 변형들을 모두 추가해두는 편이 안전하다.
[UNK] vs. subword
- 애초에 왜 이걸 하나? 새 단어(예:
xigncode)가['xig', '##nco', '##de']로 쪼개지는 걸 막기 위해서다. 이렇게 쪼개지면 용어의 고유한 의미가 사라진다. -
[UNK]가 되지는 않나? 현대 모델(BERT, RoBERTa 등)에서는 거의 절대 그렇지 않다. 이 모델들은 subword 기반이고, 어떤 미지의 단어든 구성 조각으로 쪼개도록 설계됐다.[UNK](Unknown) 토큰은 대체로 단어 기반의 옛 모델 (Word2Vec 같은)의 유물이거나, 입력에 tokenizer의 문자 집합을 완전히 벗어난 문자가 들어 있을 때만 쓰인다.
토큰을 추가하고 fine-tuning하는 것은 모델에게 이렇게 말하는 것이다. “이 단어를 그만 쪼개라! 하나로 취급하고, 이것 이 그 의미다.”