한국어 LLM 개발 (1) - 토크나이저 확장

nebchi·2024년 6월 16일
0

LLM

목록 보기
10/11

토크나이저 확장

  • 최근 한국어를 잘하는 LLM을 개발하기 위해, 생각했던 방법 중 하나로, 토크나이저 확장을 시도해보았다.
  • 토크나이저는 LLM이 텍스트 데이터를 처리하는데, 모델이 이해할 수 있도록, 효율적으로 나눠주는 역할을 수행하는데, 기존 LLM이 모두 영어다 보니, 한국어를 효율적으로 토큰화를 하지 못해 이번에 토크나이저 확장을 시도해보았다.

SentencePiece

  • SentencePiece는 기존에 존재하던 unigram, BPE와 같은 Tokenizer들을 개발할 수 있도록 도움을 주는 다국어 라이브러리이다.
  • 이를 통해 BPE 토크나이저를 생성한 후, 기존 Llama2 토크나이저와 병합하여 한국어 특화 토크나이저를 만들어보았다.
# 기존 학습
import os

options = dict(
  # input spec
  input="filtered_tokens2.txt", # 파일 경로
  input_format="text", # 파일 포맷
  # output spec
  model_prefix="llama_kor_tokenizer", # 토크나이저 모델명
  # algorithm spec
  # BPE alg
  model_type="bpe", # bpe 알고리즘 사용
  vocab_size=10000, # 학습된 토크나이저의 단어 집합 갯수

  # normalization
  normalization_rule_name="identity", # turn off normalization
  remove_extra_whitespaces=False, # 불필요한 공백 제거 여부 설정
  input_sentence_size=200000000, # 학습에 사용 될 전체 문장의 최대 개수
  max_sentence_length=1073741824, # 학습에 사용 할 개별 문장의 최대 바이트수
  seed_sentencepiece_size=1000000,
  shuffle_input_sentence=True,

  # rare word treatment
  character_coverage=1.0, # 모든 문자를 최소 한 번 포함하도록 사전을 만듦
  byte_fallback=True, # 사전에 없는 단어는 개별 바이트 단위로 토큰화하도록 설정(BBPE)

  # 위 파라미터 연구 예정
  # merge rules(토큰을 생성하는 방식을 제어하는 규칙 집합)
  split_digits=True,  # 숫자를 별도의 토큰으로 분리
  split_by_unicode_script=False, # 유니코드 스크립트에 따라 분리하도록 설정(한국어는 False가 더 좋음)
  split_by_whitespace=True, # 공백 문자를 기준으로 분리
  split_by_number=False, # 숫자 구분 기호를 기준으로 분리/ 이를 False로 지정한 것은 바로, 한국어는 쉼표 같은 조사를 사용하여 천,만 단위를 구분하는데, 이를 True로 하면 12,345 를 1,2,3,,,4,5 이렇게 분리함.
  max_sentencepiece_length=16, # 서브워드의 최대길이 지정
  add_dummy_prefix=True , #
  allow_whitespace_only_pieces=True,

  # special tokens(llama 특수 토큰의 id  지정)
  unk_id=3, # the UNK token MUST exist
  bos_id=1, # the others are optional, set to -1 to turn off
  eos_id=2,

  # systems # 사용 가능한 모든 cpu 코어를 사용하여 학습 속도 향
  num_threads=os.cpu_count(), # use ~all system resources
)

# 토크나이저 학습
import sentencepiece as spm

def main():
    spm.SentencePieceTrainer.train(
      **options
    )

main()
  • BPE는 반복적인 병합과정을 통해 점진적으로, 토큰을 생성하는데, 드문단어나 새로운 단어에 대해서도 Subword를 통해 더 작은 단위로 분할하여 처리를 하여 OOV(Out Of Vocab) 문제를 해결할 수 있다.
  • 그 후, merge_rules를 통해 토큰화를 수행 할 규칙을 정해준다.
  • 하지만 위의 규칙은 영어에 특화되어 있어 한국어의 특성을 제대로 반영하지 못할 수 있습니다. 이 경우, 형태소 분석기와 결합하여 한국어의 특성을 반영한 토크나이저를 개발할 수 있습니다.
  • 이번에는 단순하게 한국어 토큰만 추가할 목적으로 위와 같은 단순한 방식의 토큰화를 수행하였다.

토크나이저 확장 결과

Length of kor text:  18
--------------
NEW TOKENIZER
--------------
Length of encoded IDs:  8
---
Compression ratio:  0.44
---
Encoded token IDs:  [359, 4949, 47, 966, 7906, 397, 1842, 49]
---
Decoded text:  안녕하세요, 오늘 날씨가 좋네요.
--------------
llama TOKENIZER
--------------
Length of encoded IDs:  30
---
Compression ratio:  1.67
---
Encoded token IDs:  [1, 29871, 31734, 238, 136, 152, 30944, 31578, 31527, 29892, 29871, 31346, 238, 141, 155, 29871, 238, 133, 163, 31781, 30903, 29871, 239, 165, 142, 238, 135, 167, 31527, 29889]
---
Decoded text:  <s> 안녕하세요, 오늘 날씨가 좋네요.
  • 결과적으로 기존 Llama2 토크나이저에 비해 좀 더 효율적으로 텍스트를 처리할 수 있음을 볼 수 있다.
  • 최근 Llama3와 Qwen2, Gemma의 경우, Vocab_size가 10만 이상이므로, 비영어권 언어에 대해 텍스트 처리 효율이 높아, 이 경우에는 토크나이저 확장을 하지 않는게 좋다.
  • 그 이유는 토크나이저를 확장하면 파라미터도 커지게 되는데, 텍스트 처리 효율이 좋은 모델에 굳이 토큰을 추가하면 오히려 학습시간만 늘어나기 때문이다.

기존 Llama2 토크나이저 병합

import os
import re
from transformers import LlamaTokenizer

os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
from huggingface_hub import hf_hub_download
from sentencepiece import sentencepiece_model_pb2 as sp_pb2_model

# 기존 토크나이저 로드하기
original_tokenizer_path = hf_hub_download(repo_id="meta-llama/Llama-2-7b-hf",token='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', filename="tokenizer.model", local_dir="original_tokenizer")
original_tokenizer_spm = sp_pb2_model.ModelProto()
original_tokenizer_spm.ParseFromString(open(original_tokenizer_path, "rb").read())

# 확장된 토크나이저 로드하기
new_tokenizer_spm = sp_pb2_model.ModelProto()
new_tokenizer_spm.ParseFromString(open("llama_kor_tokenizer.model", "rb").read())

# 기존 Llama2 토크나이저의 중복 토큰을 제외한 새로운 단어 추가하기
def contains_eng(text):
    eng_pattern = re.compile(r"[\u0020-\u007E]+")
    return True if eng_pattern.search(text) else False


original_tokenizer_tokenset = set(p.piece for p in original_tokenizer_spm.pieces)
print(f"Number of tokens before merge: {len(original_tokenizer_tokenset)}")
for p in new_tokenizer_spm.pieces:
    piece = p.piece
    if piece not in original_tokenizer_tokenset and not contains_eng(piece):
        new_p = sp_pb2_model.ModelProto().SentencePiece()
        new_p.piece = piece
        new_p.score = 0
        original_tokenizer_spm.pieces.append(new_p)
print(f"Number of tokens after merge: {len(original_tokenizer_spm.pieces)}")

# 병합한 토크나이저 저장
extended_tokenizer_save_path="llama-kor-tokenizer"
os.makedirs(extended_tokenizer_save_path, exist_ok=True)
with open(os.path.join(extended_tokenizer_save_path, "tokenizer.model"), "wb") as f:
    f.write(original_tokenizer_spm.SerializeToString())
    
# 모델의 임베딩 레이어 크기를 조정
model.resize_token_embeddings(len(tokenizer)) # 모델은 따로 로드해야합니다.
  • 위와 같이 토크나이저를 병합한 뒤 기존 모델에 확장된 결과를 반영하면 된다.

참고자료

profile
NLP Developer

0개의 댓글