본문 바로가기
  • 머킹이의 머신로그
프로젝트/개인 프로젝트

[자연어 개인 프로젝트] 한국어를 활용한 Byte-Pair Encoding

by 머킹 2023. 9. 21.
728x90

형태소 없이 Byte-Pair Encoding 하기

 

안녕하세요 머킹입니다.

오늘은 형태소 없이 Byte-Pair encoding 하는 코드를 적어보고자 합니다!

개념 정리를 제대로 하고 싶어서 개인 프로젝트 외에 

자연어 / 음성 / 이미지 알아야 할 용어나 개념들을 따로 적어두려고 합니다.

 

개인적으로 지금 배우고 있는 NLP에서 많이 헷갈리는 부분들이 많은데

특히 모르는 용어가 많다 보니까 굉장히 어려운 것 같습니다.

LSTM 정의를 배우는 순간부터 좀 무너진 것 같지만..

차근차근 정리하고 코드를 많이 쳐봐야겠습니다!

 

시간이 된다면 제가 읽은 책들도 정리해 두겠습니다.

밑바닥부터 시작하는 딥러닝, StatQuest 머신러닝 강의, 혼공머신 등...

머신러닝 강의 외에는 거의 코드위주라 코드만 잔뜩 적힌 글이 올라갈 것 같기도 합니다 ㅎㅎ


아무튼 오늘은 자연어 처리를 배우다가

형태소 없이 한국어를 처리하는 방법을 배웠습니다.

 

일단 Byte-Pair Encoding에 대해서 설명드리겠습니다.

 

BPE 자체는 1994년에 제안된 데이터 압축 알고리즘입니다. (NLP 알고리즘이 아님)
• 자주 등장하는 Byte Pair는 새로운 하나의 Byte가 된다. 
• 이를 단어 분리(Word segmentation)에 도입한다. (NLP 알고리즘이 됨.) 
• Bottom-up 방식의 클러스터링 
• 데이터의 모든 글자(character) 단위의 유니그램 단어 사전에서 시작한다. 
• 자주 등장하는 바이그램을 유니그램으로 통합한다.
• 모든 바이그램이 선택되거나 정해진 단어 집합의 크기에 도달할 때까지 반복한다.

 

이렇게 보면 이해가 좀 쉬운데요.

단어들을 끊어서 Vocabulary에 넣는 방법입니다.

시간이 오래 걸리지만 다른 언어들에도 활용하기 쉬워서 한국어에도 꽤 잘 적용됩니다.

또한 희귀 단어, OOV(Out-Of-Vocabulary)에 강건해진다는 특징이 있습니다.

# 한국어를 활용한 Byte-Pair Encoding

2015년 Rico Sennrich가 작성한 논문 [Neural Machine Translation of Rare Words with Subword Unit*](https://arxiv.org/abs/1508.07909)에서는OOV (Out-Of-Vocabulary)문제를 해결하기 위해 정보 이론에서의 **Byte-Pair Encoding** 기법을 토크나이즈에 접목시킨 BPE Embedding이 고안되었습니다.

해당 알고리즘 덕분에 자연어 처리의 많은 모델들은 OOV 문제를 해결할 수 있게 되었고, 뿐만 아니라 성능 개선까지 얻게 되었습니다.

그리고 BPE를 활용한 토크나이즈 기법은 Transformer 기반의 여러 최신 모델들에서 아직까지 차용되고 있습니다.

본 노트북에서는 이러한 BPE 임베딩을 한국어에 적용해보며, 그 특성과 구현 방법에 대해 알아보도록 하겠습니다.

먼저 정규표현식을 활용해 한국어 문장을 전처리 해줄 수 있는 `preprocess` 함수를 구현해 보도록 합시다.
한국어 전처리를 위한 정규표현식을 어떻게 구성할지는 프로젝트의 목적에 따라 다를 수 있습니다.

영어, 숫자 등이 의미를 큰 지니지 않는 코퍼스에 대해서는 해당 캐릭터들을 모두 제거할 수도 있겠지만,
영어, 숫자 등이 의미를 지니는 경우 해당 캐릭터들을 제거하지 않을 수도 있습니다.

본 예제에서는 한국어Byte-Pair가 어떻게 형성되는지 보기 위해 불필요한 특수문자와 숫자, 영어를 모두 제거하도록 하겠습니다.

import re
from collections import defaultdict
from typing import Dict, List, Tuple
from tqdm import tqdm


SPECIALS = "".join([".", ",", ";", ":", "!", "?", '"', "'", " "])


def preprocess(text: str, only_kor: bool=True):
    """한국어 문장을 옵션에 맞게 전처리"""
    # 한국어 모음과 특수 문자, 숫자 및 영어 제거
    if only_kor:
        text = re.sub(f"[^가-힣| |]+", "", text)
    else:
        text = re.sub(f"[^가-힣|ㄱ-ㅎ|0-9|{SPECIALS}|a-zA-Z|]+", "", text)
    
    # 연속 공백 제거
    text = re.sub(" +", " ", text)
    
    # 좌우 불필요한 공백 제거
    return text.strip()
sent = "ㅋㅋㅋㅋㅋㅋ 안녕하세요 ! \"저는\" cola를 좋아합니다."
preprocess(sent)
preprocess(sent, only_kor=False)
>>> 'ㅋㅋㅋㅋㅋㅋ 안녕하세요 ! "저는" cola를 좋아합니다.'

 

Byte-Pair Encoding 구현

이제 전처리를 거친 문장들로 구성된 코퍼스를 활용해 Byte-Pair를 구성할 수 있는 Encoding로직을 구현해 보겠습니다.

3개의 핵심 스텝으로 구현될 수 있습니다.

1. get_vocab

2. get_stats

3. merge_vocab

 

 

해당 코드는 Lei Mao 포스트에서 차용 및 수정하였습니다.

get_vocab 함수에 대해서 먼저 살펴보겠습니다.

해당 함수가 수행하는 로직은 간단합니다.

1. 코퍼스 파일을 라인 단위로 읽어옵니다.

2. 각 라인을 공백 단위로 스플릿해 토큰 리스트를 구성합니다.

3. 토큰 리스트를 순회하며, 토큰을 캐릭터 단위로 자른 후 사전에 postfix <'/w>'와 함께 등록합니다.

 

def get_vocab(f_name: str) -> Dict[str, int]:
    """코퍼스 파일을 읽어와 단어 사전 구축"""
    vocab = defaultdict(int)
    with open(f_name, "r", encoding="utf-8") as corpus:
        for line in corpus:
            tokens = preprocess(line).strip().split()
            for token in tokens:
                vocab[" ".join(list(token)) + "</w>"] += 1
    return dict(vocab)
 

전체 코퍼스를 활용해 사전을 구축하기 전에,

간단한 더미 데이터로 실험을 하기 위한 'pseudo_get_vocab' 함수를 구현하겠습니다.

def pseudo_get_vocab(corpus: List[str]) -> Dict[str, int]:
    """더미 데이터를 읽어와 단어 사전 구축"""
    vocab = defaultdict(int)
    for line in corpus:
        tokens = preprocess(line).strip().split(" ")
        for token in tokens:
            vocab[" ".join(list(token)) + " </w>"] += 1
    return dict(vocab)
corpus = [
    "나는 나를 사랑해",
    "나는 너를 사랑해",
    "콜라가 좋아",
    "딸기는 사랑하지 않아",
    "펩시는 사랑하지 않아",
    "네가 사랑하는 나",
    "내가 사랑하는 너"
]

여기에 원하는 문장을 넣으시면 됩니다.

vocab = pseudo_get_vocab(corpus)
vocab

>>>{'나 는 </w>': 2,
 '나 를 </w>': 1,
 '사 랑 해 </w>': 2,
 '너 를 </w>': 1,
 '콜 라 가 </w>': 1,
 '좋 아 </w>': 1,
 '딸 기 는 </w>': 1,
 '사 랑 하 지 </w>': 2,
 '않 아 </w>': 2,
 '펩 시 는 </w>': 1,
 '네 가 </w>': 1,
 '사 랑 하 는 </w>': 2,
 '나 </w>': 1,
 '내 가 </w>': 1,
 '너 </w>': 1}

이제 구축된 사전을 순회하며 사전 내 등록된 캐릭터 토큰등장 횟수를 반환해 주는 함수 `get_tokens`를 구현해 줍니다.
이후, **Byte-Pair Encoding** 로직을 거치기 이전의 사전을 확인합니다.

def get_tokens(vocab: Dict[str, int]):
    """사전 내 등록된 토큰을 확인"""
    result = defaultdict(int)
    for word, freq in vocab.items():
        tokens = word.split()
        for token in tokens:
            result[token] += freq

    return dict(result)
tokens = get_tokens(vocab)
tokens
>>>
{'나': 4,
 '는': 6,
 '</w>': 20,
 '를': 2,
 '사': 6,
 '랑': 6,
 '해': 2,
 '너': 2,
 '콜': 1,
 '라': 1,
 '가': 3,
 '좋': 1,
 '아': 3,
 '딸': 1,
 '기': 1,
 '하': 4,
 '지': 2,
 '않': 2,
 '펩': 1,
 '시': 1,
 '네': 1,
 '내': 1}

마지막 스텝인 `merge_vocab`은 가장 자주 등장했던 바이그램 페어를 엮어주는 함수여야 하므로,
가장 자주 등장한 페어를 구할 수 있는 로직 `get_stats` 함수가 필요합니다.

def get_stats(vocab: Dict[str, int]):
    """사전을 활용한 바이그램 페어 구축"""
    pairs = defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols)-1):
            pairs[symbols[i], symbols[i+1]] += freq
    return dict(pairs)
pairs = get_stats(vocab)
pairs
>>>
{('나', '는'): 2,
 ('는', '</w>'): 6,
 ('나', '를'): 1,
 ('를', '</w>'): 2,
 ('사', '랑'): 6,
 ('랑', '해'): 2,
 ('해', '</w>'): 2,
 ('너', '를'): 1,
 ('콜', '라'): 1,
 ('라', '가'): 1,
 ('가', '</w>'): 3,
 ('좋', '아'): 1,
 ('아', '</w>'): 3,
 ('딸', '기'): 1,
 ('기', '는'): 1,
 ('랑', '하'): 4,
 ('하', '지'): 2,
 ('지', '</w>'): 2,
 ('않', '아'): 2,
 ('펩', '시'): 1,
 ('시', '는'): 1,
 ('네', '가'): 1,
 ('하', '는'): 2,
 ('나', '</w>'): 1,
 ('내', '가'): 1,
 ('너', '</w>'): 1}

이제 3개 스텝 중 마지막 함수인 `merge_vocab` 함수를 구현해 줍니다.

`merge_vocab`의 로직 역시 간단합니다.

1. `get_stats` 함수를 통해 얻어진 바이그램 중 가장 자주 등장한 페어기존 사전을 인자로 받습니다.
2. 기존 사전에서 캐릭터 단위로 떨어져 있던 바이그램을 하나의 페어로 엮어줍니다.
3. 새로이 구축한 사전을 반환합니다.

 

def merge_vocab(pair: Tuple[str, str], vocab: Dict[str, int]):
    """가장 자주 등장한 바이그램 페어를 엮어줌"""
    result = defaultdict(int)
    for word in vocab:
        paired = word.replace(" ".join(pair), "".join(pair))
        result[paired] = vocab[word]
    return dict(result)

지금까지 구현한 함수들을 활용해 자주 등장한 Byte-Pair가 합쳐져, 새로운 사전이 구축되는 과정을 살펴보도록 합시다.

num_merges = 5

for i in range(num_merges):
    pairs = get_stats(vocab)
    if not pairs:
        break
    best = max(pairs, key=pairs.get)
    vocab = merge_vocab(best, vocab)
    tokens = get_tokens(vocab)
    print(f"Iter: {i+1}\n"
          f"Best pair: {best}\n"
          f"Tokens: {tokens}\n"
          f"Number of tokens: {len(tokens)}\n")
          
  >>>
  Iter: 1
Best pair: ('는', '</w>')
Tokens: {'나': 4, '는</w>': 6, '를': 2, '</w>': 14, '사': 6, '랑': 6, '해': 2, '너': 2, '콜': 1, '라': 1, '가': 3, '좋': 1, '아': 3, '딸': 1, '기': 1, '하': 4, '지': 2, '않': 2, '펩': 1, '시': 1, '네': 1, '내': 1}
Number of tokens: 22

Iter: 2
Best pair: ('사', '랑')
Tokens: {'나': 4, '는</w>': 6, '를': 2, '</w>': 14, '사랑': 6, '해': 2, '너': 2, '콜': 1, '라': 1, '가': 3, '좋': 1, '아': 3, '딸': 1, '기': 1, '하': 4, '지': 2, '않': 2, '펩': 1, '시': 1, '네': 1, '내': 1}
Number of tokens: 21

Iter: 3
Best pair: ('사랑', '하')
Tokens: {'나': 4, '는</w>': 6, '를': 2, '</w>': 14, '사랑': 2, '해': 2, '너': 2, '콜': 1, '라': 1, '가': 3, '좋': 1, '아': 3, '딸': 1, '기': 1, '사랑하': 4, '지': 2, '않': 2, '펩': 1, '시': 1, '네': 1, '내': 1}
Number of tokens: 21

Iter: 4
Best pair: ('가', '</w>')
Tokens: {'나': 4, '는</w>': 6, '를': 2, '</w>': 11, '사랑': 2, '해': 2, '너': 2, '콜': 1, '라': 1, '가</w>': 3, '좋': 1, '아': 3, '딸': 1, '기': 1, '사랑하': 4, '지': 2, '않': 2, '펩': 1, '시': 1, '네': 1, '내': 1}
Number of tokens: 21

Iter: 5
Best pair: ('아', '</w>')
Tokens: {'나': 4, '는</w>': 6, '를': 2, '</w>': 8, '사랑': 2, '해': 2, '너': 2, '콜': 1, '라': 1, '가</w>': 3, '좋': 1, '아</w>': 3, '딸': 1, '기': 1, '사랑하': 4, '지': 2, '않': 2, '펩': 1, '시': 1, '네': 1, '내': 1}
Number of tokens: 21
 

이제 제대로 된 훈련 파일을 읽어와 'vocab'을 구축합시다.

 

vocab = get_vocab("C:/Users/user/Desktop/vs/patois/jeju/Training/DZJD21002407.txt")
vocab
>>>
{'이 렇 게</w>': 5,
 '교 수 님 들 이 랑</w>': 1,
 '있 는</w>': 6,
 '느 낌 이 라 부 난 서 이 이</w>': 1,
 '뭔 가</w>': 9,
 '멀 게</w>': 1,
 '느 껴 져 부 러 부 러</w>': 1,
 '겅 그 렇 게 행 해 서</w>': 1,
 '별</w>': 2,
 '말</w>': 1,
 '안</w>': 45,
 '하 맨 해</w>': 2,
 '웃 음</w>': 40,
 '나</w>': 18,
 '같 은</w>': 15,
 '다</w>': 22,
 '같 이</w>': 5,
 '있 을</w>': 4,
 '줄</w>': 4,
 '알 았 냐</w>': 1,
 '내 가</w>': 9,
 '내 성 적 인</w>': 1,
 '사 람 은</w>': 2,
 '굳 이</w>': 1,
 '왜</w>': 7,
...
 '있 어 도</w>': 1,
 '많 아 부 난 서</w>': 1,
 '가 는</w>': 4,
 '똑 같 댄 다 고</w>': 1,
 ...}

저는 제주도 사투리 데이터 하나를 가져왔습니다.

구축한 사전에 `merge_vocab` 로직을 적용합니다. 간단한 실험을 위해 `num_merges`는 1000으로 설정하겠습니다.

 

num_merges = 1000

for i in tqdm(range(num_merges)):
    pairs = get_stats(vocab)
    if not pairs:
        break
    best = max(pairs, key=pairs.get)
    vocab = merge_vocab(best, vocab)
    >>>
    100%|██████████| 1000/1000 [00:01<00:00, 636.10it/s]
vocab
>>>
{'이렇게</w>': 5,
 '교수님들이랑</w>': 1,
 '있는</w>': 6,
 '느낌이라부난서이이</w>': 1,
 '뭔가</w>': 9,
 '멀게</w>': 1,
 '느껴져부러부러</w>': 1,
 '겅그렇게행해서</w>': 1,
 '별</w>': 2,
 '말</w>': 1,
 '안</w>': 45,
 '하맨해</w>': 2,
 '웃음</w>': 40,
 '나</w>': 18,
 '같은</w>': 15,
 '다</w>': 22,
 '같이</w>': 5,
 '있을</w>': 4,
 '줄</w>': 4,
 '알았냐</w>': 1,
 '내가</w>': 9,
 '내성적인</w>': 1,
 '사람은</w>': 2,
 '굳이</w>': 1,
 '왜</w>': 7,
...
 '있어 도</w>': 1,
 '많 아 부난서</w>': 1,
 '가는</w>': 4,
 '똑 같 댄다고</w>': 1,
 ...}

BPE를 활용한 토크나이즈는 최장 길이 토큰의 매칭을 우선적으로 적용하기 때문에,
사전을 단어 길이 기준내림차순으로 정렬해줘야 합니다. 이를 위해 `get_token_len` 함수를 구현해줍니다.

def get_token_len(token: str):
    """토큰 길이 계산 : </w> 는 하나의 토큰 취급"""
    if token.endswith("</w>"):
        # 구성 캐릭터 + </w>
        return len(token[:-4]) + 1
    return len(token)

이제 구축된 사전을 활용해 새로운 입력 값을 토크나이즈 할 수 있는 `tokenize` 함수를 구현하도록 합니다.

BPE를 활용한 문장의 인코딩은 재귀적으로 구성되기 때문에, 직접 결과 값을 찍어보며 이해하는 것이 가장 좋습니다.
`tokenize`의 기본적인 로직은 다음과 같습니다.

1. 훈련을 통해 구축된 단어 리스트를 순회하며, 입력 문장 내 해당 단어가 존재하는지 파악합니다.
2. 순회하던 단어가 입력 문장 내에 존재하지 않는다면, 계속 순회를 진행합니다.
    - 만약 단어 리스트를 끝까지 순회했음에도 매치되는 토큰이 없었다면, `</u>`의 OOV 토큰을 반환합니다.
2. 순회하던 단어가 입력 문장 내에 존재한다면, 해당 토큰을 기준으로 재귀적인 토크나이즈를 수행합니다.
    - 먼저, 매치 토큰의 좌측 스팬을 서브 스트링으로 설정해 토크나이즈 함수에 입력합니다. 그리그 그 결과를 전체 결과 리스트에 추가합니다.
    - 이제, 매칭된 토큰을 결과 값에 추가합니다.
    - 매치 토큰이 한 문장 내에서 여러 개일 수 있으므로, 위 과정을 여러 번 반복합니다.
    - 이후 매치 토큰 우측 스팬을 서브 스트링으로 설정해 토크나이즈 함수에 입력합니다. 그리그 그 결과를 전체 결과 리스트에 추가합니다.

 

def tokenize(text: str, sorted_tokens: List[str], unknown_token="</u>"):
    """구축된 사전을 활용한 BEP 토크나이즈"""
    text = text.strip()

    if text == "":
        return list()
    if len(sorted_tokens) == 0:
        return [unknown_token]
    
    result = list()
    # 사전 내 등록 단어 순회
    for i in range(len(sorted_tokens)):
        token = re.escape(sorted_tokens[i])

        # 현재 순회 중인 단어가 입력 텍스트에 포함되는지 확인
        matched = [(m.start(0), m.end(0)) for m in re.finditer(token, text)]

        ## 단순히 포함되지 않은 것이라면, continue
        ## 토큰 리스트를 다 돌았음에도 포함되지 않은 것이라면, [unk] 변환
        if len(matched) == 0:
            if i == (len(sorted_tokens) - 1):
                return [unknown_token]
            else:
                continue

        ## 포함되면 해당 토큰의 시작점(들)을 저장
        ends = [m[0] for m in matched]
        print(f"[{text}] 매치 토큰: {token} / 인덱스: {ends}")

        start = 0
        for end in ends:
            # 매치 토큰 이전에 위치한 서브 스트링에 대한 토크나이즈 진행 및 결과 추가
            substring = text[start:end]
            print(f"[{text}] 서브 스트링: {substring} ({start}~{end})")
            result += tokenize(substring, sorted_tokens[i+1:])

            # 매치 토큰 추가
            result += [token]
            print(f"[{text}] 현재 토크나이즈 결과: {result}")

            # 매치 토큰 길이 만큼 start 인덱스 값 증가
            start = end + len(token)

        # 매치 토큰 이후에 위치한 서브 스트링에 대한 토크나이즈 진행 및 결과 추가
        remainder = text[start:]
        result += tokenize(remainder, sorted_tokens[i+1:])
        break
    return result

구현한 `tokenize` 함수를 활용해 새로운 입력 값에 대한 인코딩을 진행해 보도록 합시다.
앞서 언급한 대로 입력 값을 바꾸어 가며, 결과 값을 찍어보는 것이 이해에 가장 도움이 됩니다.

tokens = get_tokens(vocab)

# 사전 내 토큰들 길이 순으로 정렬 후, 단어만 저장
sorted_tokens = sorted(tokens.items(), key=lambda x: (get_token_len(x[0]), x[1]), reverse=True)
sorted_tokens = [token for (token, _) in sorted_tokens]

print(f"사전 내 등록 단어: \n{sorted_tokens}\n")

words = ["한한구갹국겨계껴ㅖ", "사랑합니다</w>", "야식</w> 안먹고</w> 참아보기</w>"]

for word in words:
    print(f"입력 단어: {format(word)}\n")
    print(f"입력 단어 토큰화 결과: {tokenize(word, sorted_tokens)}\n")
사전 내 등록 단어: 
['경그렇게행해서그넹그넹</w>', '겅그렇게행해서기넹기넹</w>', '모르게신디모르겠는데</w>', '생각해신고했을까이이</w>', '찾아보고이신디있는데</w>', '느낌이라부난서이이</w>', '필요행해서그넹그넹</w>', '생각해사주게해야지</w>', '생각해부난해버려서</w>', '해외배송이라부난서</w>', '이서있어부난서이이</w>', '생각해신디했는데</w>', '닮아같아그넹그넹</w>', '들어와부난서이이</w>', '했어신디했었는데</w>', '경그렇게하믄하면</w>', '걱정하는거라거야</w>', '생각함신지는하는</w>', '싸질렀저질렀댄대</w>', '고르라말해라게게</w>', '지원해신디했는데</w>', '생각해사주해야지</w>', '되엉어서그넹그넹</w>', '고민하는디하는데</w>', '게난그니까이이</w>', '기구나그렇구나</w>', '이시난있으니까</w>', '엉어서그넹그넹</w>', '느껴져부러부러</w>', '겅그렇게행해서</w>', '생모양이라이야</w>', '생각하멍하면서</w>', '만들잰만들려고</w>', '이서있어부난서</w>', '불편하지않않아</w>', '고이신디있는데</w>', '경그렇게하니까</w>', '행해서그넹그넹</w>', '닮은디같은데</w>', '하는디하는데</w>', '햄신디하는데</w>', '팔아부러버려</w>', '기는한디한데</w>', '교수님들이랑</w>', '나오드라더라</w>', '소리인디인데</w>', '보믄보면이이</w>', '가졌져가졌지</w>', '꽂혀가지고서</w>', '고민하당다가</w>', '이신디있는데</w>', '것보단보다는</w>', '가죽인디인데</w>', '시키믄시키면</w>', '가부런버렸어</w>', '쌌져많지게게</w>', '삼성페이여야</w>', '걸어가고이이</w>', '해신디했는데</w>', '어서그넹그넹</w>', '신디했었는데</w>', '게난그니까</w>', '아니아니야</w>', '가지고이이</w>', '부난서이이</w>', '루미큐브도</w>', '붙엉붙어서</w>', '않안않았어</w>', '어떵어떻게</w>', '오난오니까</w>', '층인디인데</w>', '있언있었어</w>', '사람들이랑</w>', '전이랑이이</w>', '나서랑이이</w>', '닮다같다고</w>', '어색하니까</w>', '인문학적인</w>', '근데그런데</w>', '비과학적인</w>', '그치그렇지</w>', '게지그렇지</w>', '요즘엔에는</w>', '붙어그넹서</w>', '아니라니야</w>', '문과랑라서</w>', '넘언넘었어</w>', '자신감으로</w>', '질런질렀어</w>', '초급이라고</w>', '보기엔에는</w>', '사겠다야야</w>', '그츄그렇지</w>', '되클되겠어</w>', '옷이네잖아</w>', '되부난돼서</w>', '만들어서주</w>', '원인디인데</w>', '밀려부난서</w>', '실험실에서</w>', '중단했다는</w>', '사이트까지</w>', '시작해볼까</w>', '한거라거야</w>', '가지않않아</w>', '사람들이이</w>', '보드게임도</w>', '잘도매우</w>', '이서있어</w>', '닮아같아</w>', '하고이이</w>', '경그렇게</w>', '영이렇게</w>', '사람들이</w>', '거라거야</w>', '보드게임</w>', '겅그렇게</w>', '근디근데</w>', '그넹그넹</w>', '심사위원단', '있고이이</w>', '있네잖아</w>', '회사에서</w>', '있으니까</w>', '있네이이</w>', '이여이야</w>', '적혀있지</w>', '기라그래</w>', '돌아오라</w>', '어서없어</w>', '잘핸했어</w>', '도전하는</w>', '고럼그럼</w>', '한디한데</w>', '보여주고</w>', '해부난서</w>', '카리스마</w>', '송민호도</w>', '사람들도</w>', '고정싶어</w>', '서울에서</w>', '가믄가면</w>', '극적으로</w>', '루미큐브</w>', '내성적인</w>', '되지라고</w>', '대화하는</w>', '시작하기</w>', '선생님이</w>', '시간에도</w>', '자기소개</w>', '분위기도</w>', '해지더라</w>', '치우쳐져</w>', '생각하는</w>', '쓸데없는</w>', '거냐면서</w>', '건그거는</w>', '붙어봐야</w>', '친구들이</w>', '하도많이</w>', '골아말해</w>', '가이걔가</w>', '아이고야</w>', '쓸데없이</w>', '생각하면</w>', '좋주좋지</w>', '가방부터</w>', '시작해도</w>', '초급이라</w>', '다닙니다</w>', '불가능한</w>', '옷이니까</w>', '사면이이</w>', '거닮같아</w>', '잘도겁나</w>', '시키면은</w>', '경그런한</w>', '들어오는</w>', '제품인데</w>', '제품으로</w>', '미국에서</w>', '공장에서</w>', '않안않아</w>', '싸고많고</w>', '근데이이</w>', '지갑부터</w>', '핸드폰만</w>', '들어가면</w>', '뭐여뭐야</w>', '아이폰은</w>', '그냥이이</w>', '있었거든</w>', '니까이이</w>', '나서이이</w>', '인디인데</w>', '가지고서</w>', '고민하고</w>', '려부난서</w>', '부러버려</w>', '지않않아</w>', '기엔에는</w>', '보면이이</w>', '그리고</w>', '가지고</w>', '그래서</w>', '요즘에</w>', '이렇게</w>', '그런데</w>', '어쨌든</w>', '하니까</w>', '네잖아</w>', '때마다</w>', '닮같아</w>', '회사에</w>', '부난서</w>', '인스타</w>', '기그래</w>', '고이이</w>', '하는데</w>', '사람들</w>', '댄다고</w>', '마피아</w>', '하맨해</w>', '거거야</w>', '그렇게</w>', '있거든</w>', '영수증</w>', '경그렇게', '행해서</w>', '빨래가</w>', '세제를</w>', '용으로</w>', '당근에</w>', '걍그냥</w>', '하다고</w>', '난니까</w>', '나오는</w>', '사람들이', '트렌디</w>', '맛있는</w>', '지게게</w>', '저녁에</w>', '게임도</w>', '점심시간', '일부러</w>', '사람은</w>', '끝나고</w>', '거그거</w>', '나중에</w>', '싶은데</w>', '가이걔</w>', '괜찮은</w>', '송아지</w>', '만들고</w>', '이름는</w>', '때문에</w>', '받아야</w>', '다니지</w>', '지갑도</w>', '카드를</w>', '도시락</w>', '하루에</w>', '해야지</w>', '오래된</w>', '거거든</w>', '랑이이</w>', '조바심나', '그래도</w>', '유기농</w>', '다게게</w>', '면이이</w>', '보다가</w>', '보맨봐</w>', '심사는</w>', '재미도</w>', '아이이</w>', '못하네</w>', '사람도</w>', '생각해</w>', '방송국</w>', '있기는</w>', '안았어</w>', '오히려</w>', '있다고</w>', '자기가</w>', '보겠어</w>', '먹방을</w>', '맛있게</w>', '먹방은</w>', '아니고</w>', '맛있어</w>', '이름이</w>', '대전만</w>', '요즘도</w>', '저번에</w>', '그때가</w>', '성산일출', '베이킹</w>', '오라고</w>', '갑자기</w>', '케이크</w>', '아침에</w>', '집에서</w>', '걸리는</w>', '보드게임', '사회자</w>', '히틀러</w>', '식으로</w>', '그걸로</w>', '그거를</w>', '거짓말</w>', '마시러</w>', '알았냐</w>', '걸어야</w>', '해이이</w>', '자체가</w>', '얘기도</w>', '나누고</w>', '시간을</w>', '주거든</w>', '긴장한</w>', '어른들</w>', '어려워</w>', '소양이</w>', '필요한</w>', '쪽으로</w>', '막아진</w>', '생각할</w>', '친구가</w>', '나한테</w>', '붙으면</w>', '어떡할</w>', '건지는</w>', '되냐고</w>', '옆에서</w>', '번호가</w>', '가방에</w>', '세트를</w>', '어차피</w>', '구멍도</w>', '뜯어져</w>', '않않아</w>', '단계는</w>', '브랜드</w>', '만들면</w>', '가방을</w>', '보느라</w>', '컹게사</w>', '이름가</w>', '이름만</w>', '뭐하면</w>', '얼마나</w>', '사사로</w>', '그거는</w>', '사사가</w>', '중요해</w>', '괜찮을</w>', '배송도</w>', '것들은</w>', '걸리고</w>', '미국에</w>', '중단해</w>', '소리도</w>', '크로스</w>', '써질쓸</w>', '가방은</w>', '지갑을</w>', '아이고</w>', '카드만</w>', '모으기</w>', '지더라</w>', '같은데</w>', '조바심</w>', '어떻게</w>', '네이이</w>', '핸했어</w>', '것보단</w>', '서이이</w>', '얘기가</w>', '알았어</w>', '장에서</w>', '적으로</w>', '하면서</w>', '제주도</w>', '으니까</w>', '가이이</w>', '었거든</w>', '않았어</w>', '재밌어</w>', '겅그렇게', '못하고</w>', '있어서</w>', '들어서</w>', '아니야</w>', '이라고</w>', '웃음</w>', '그런</w>', '근데</w>', '것도</w>', '약간</w>', '하는</w>', '이런</w>', '같은</w>', '하고</w>', '맞아</w>', '나는</w>', '그거</w>', '나도</w>', '너무</w>', '요즘</w>', '거든</w>', '뭔가</w>', '내가</w>', '이거</w>', '는데</w>', '이제</w>', '많이</w>', '그냥</w>', '들고</w>', '회사</w>', '다고</w>', '뭐냐</w>', '카드</w>', '누가</w>', '현금</w>', '먹고</w>', '으로</w>', '려고</w>', '게임</w>', '있는</w>', '보는</w>', '해야</w>', '있어</w>', '라고</w>', '해서</w>', '이이</w>', '같이</w>', '시간</w>', '사람</w>', '가죽</w>', '아니</w>', '진짜</w>', '되는</w>', '오래</w>', '다녀</w>', '에서</w>', '니까</w>', '었어</w>', '있을</w>', '네가</w>', '거기</w>', '가방</w>', '하면</w>', '있고</w>', '따로</w>', '지금</w>', '지는</w>', '까는</w>', '되게</w>', '계속</w>', '서로</w>', '없어</w>', '순서</w>', '사실</w>', '완전</w>', '이랑</w>', '면서</w>', '가는</w>', '우리</w>', '집에</w>', '조금</w>', '그럴</w>', '밖에</w>', '가고</w>', '먼저</w>', '좋게</w>', '겠다</w>', '한두</w>', '해외</w>', '지갑</w>', '좋아</w>', '겠어</w>', '기도</w>', '하러</w>', '에는</w>', '기는</w>', '으면</w>', '다가</w>', '알아</w>', '끝이</w>', '심사</w>', '하지</w>', '들이</w>', '별로</w>', '기가</w>', '더라</w>', '수도</w>', '거는</w>', '놀러</w>', '에도</w>', '많아</w>', '오븐</w>', '재밌어', '말을</w>', '왠지</w>', '쓰고</w>', '뭐야</w>', '걸로</w>', '사는</w>', '가짜</w>', '모델</w>', '간다</w>', '무슨</w>', '다음</w>', '중국</w>', '거의</w>', '물건</w>', '것들</w>', '아예</w>', '원래</w>', '다른</w>', '너네</w>', '찾다</w>', '없이</w>', '가서</w>', '까지</w>', '시켜</w>', '이사</w>', '가지고', '근할</w>', '소리</w>', '뭐해</w>', '세제</w>', '드럼</w>', '일반</w>', '되나</w>', '건데</w>', '거면</w>', '인스타', '당근</w>', '그럼</w>', '겠지</w>', '주지</w>', '이름이', '지만</w>', '말해주', '엄청</w>', '주로</w>', '보면</w>', '댄대</w>', '던데</w>', '위원</w>', '가이걔', '다들</w>', '구나</w>', '재미서', '안돼</w>', '티비</w>', '어서</w>', '오는</w>', '것만</w>', '그게</w>', '흠뻑</w>', '졌어</w>', '잖아</w>', '사람들', '끼리</w>', '들도</w>', '최근</w>', '이지</w>', '맨날</w>', '표정</w>', '생각</w>', '있으</w>', '맞는</w>', '있지</w>', '리고</w>', '그걸</w>', '야지</w>', '먹는</w>', '하다</w>', '보잰보', '거지</w>', '음식</w>', '가게</w>', '와도</w>', '서울</w>', '말고</w>', '그때</w>', '대전</w>', '훨씬</w>', '제주도', '중에</w>', '여러</w>', '히틀러', '나가</w>', '할리갈', '작은</w>', '멀게</w>', '굳이</w>', '걸어</w>', '좋을</w>', '얘기</w>', '처음</w>', '말이</w>', '앞에</w>', '이론</w>', '수업</w>', '모르</w>', '이과</w>', '그것</w>', '들을</w>', '미친</w>', '고민</w>', '상담</w>', '자기</w>', '아는</w>', '걱정</w>', '어제</w>', '대기</w>', '번이</w>', '붙을</w>', '아까</w>', '라탄</w>', '공예</w>', '결국</w>', '재단</w>', '적혀</w>', '인해</w>', '얼마</w>', '할게</w>', '빼라</w>', '좁게</w>', '사사</w>', '키스</w>', '만날</w>', '들어</w>', '이만</w>', '달에</w>', '쉽네</w>', '쉬는</w>', '배송</w>', '걸려</w>', '발송</w>', '쓰던</w>', '바꾼</w>', '특히</w>', '만든</w>', '생산</w>', '미국</w>', '현지</w>', '백을</w>', '갖고</w>', '폰만</w>', '제일</w>', '폰에</w>', '싸서</w>', '가그넹', '라서</w>', '주게</w>', '닮다같', '어디</w>', '보드</w>', '뭐하맨', '찾아보', '아니라', '그치</w>', '시작하', '않아</w>', '재미</w>', '마다</w>', '번에</w>', '되지</w>', '보여주', '거야</w>', '느낌</w>', '행해서', '부난</w>', '한데</w>', '생각하', '많고</w>', '모르겠', '먹방</w>', '조바심', '좋지</w>', '았어</w>', '아무</w>', '면은</w>', '가까</w>', '싶어</w>', '은데</w>', '없는</w>', '여행</w>', '보여</w>', '했어</w>', '행해</w>', '리는</w>', '가지</w>', '그래</w>', '거</w>', '어</w>', '안</w>', '응</w>', '막</w>', '다</w>', '그</w>', '아</w>', '도</w>', '나</w>', '가</w>', '해</w>', '는</w>', '고</w>', '에</w>', '잘</w>', '못</w>', '이</w>', '게</w>', '을</w>', '난</w>', '또</w>', '면</w>', '로</w>', '지</w>', '은</w>', '수</w>', '야</w>', '서</w>', '왜</w>', '돼</w>', '뭐</w>', '게임', '더</w>', '좀</w>', '걸</w>', '네</w>', '데</w>', '인</w>', '기</w>', '재밌', '를</w>', '재미', '때</w>', '것</w>', '살</w>', '만</w>', '할</w>', '명</w>', '들</w>', '한</w>', '리</w>', '줄</w>', '건</w>', '시켜', '동</w>', '오</w>', '갈</w>', '취미', '요</w>', '될</w>', '봐</w>', '올</w>', '음</w>', '돈</w>', '먹으', '히</w>', '의</w>', '들어', '러</w>', '간</w>', '운</w>', '든</w>', '랑</w>', '신디', '원</w>', '까</w>', '고고', '끝나', '배</w>', '본</w>', '주</w>', '붙이', '그러', '좋아', '부난', '아니', '긴</w>', '모여', '은디', '다니', '구워', '다음', '티</w>', '별</w>', '말</w>', '사</w>', '일</w>', '소득', '괜찮', '해야', '주게', '집</w>', '베이', '가까', '온</w>', '적</w>', '와려', '내</w>', '세탁', '드럼', '그래', '스</w>', '새</w>', '귀찮', '당근', '좋은', '살아', '론</w>', '반짝', '했져', '자</w>', '올리', '크라', '아무', '날</w>', '아파', '래</w>', '투표', '프로', '텐</w>', '던</w>', '사람', '지않', '브</w>', '와</w>', '화재', '모르', '올라', '늘어', '빠져', '열</w>', '꼭</w>', '순서', '냐</w>', '뭐하', '나오', '어색', '취향', '녀석', '유튜', '우</w>', '같아', '몇</w>', '내려', '어디', '만들', '얘기', '미루', '분위', '아발', '뱅</w>', '에서', '물</w>', '두</w>', '여행', '만한', '맨</w>', '하</w>', '넌</w>', '백</w>', '뭔</w>', '저</w>', '옷</w>', '뼈</w>', '달</w>', '써</w>', '밥</w>', '씁</w>', '모으', '합</w>', '세</w>', '삼</w>', '터</w>', '그런', '분</w>', '씩</w>', '걸어', '려</w>', '겸</w>', '됐</w>', '앱</w>', '단</w>', '입</w>', '시작', '걱정', '친구', '사이', '겨</w>', '튼</w>', '경</w>', '남</w>', '꼴</w>', '밀</w>', '져</w>', '션</w>', '램</w>', '이라', '잼</w>', '껏</w>', '방</w>', '저녁', '시난', '짤</w>', '쩔</w>', '실</w>', '푹</w>', '방송', '짜</w>', '벌</w>', '이렇', '연</w>', '순</w>', '떤</w>', '중요', '욕</w>', '보여', '영</w>', '위원', '웅</w>', '미</w>', '볼</w>', '느껴', '라</w>', '룰</w>', '쎄</w>', '있</w>', '바</w>', '악</w>', '둘</w>', '곡</w>', '나한', '란</w>', '먹방', '느낌', '생각', '맛</w>', '사주', '리스', '트</w>', '곳</w>', '있어', '설</w>', '쯤</w>', '월</w>', '마</w>', '청</w>', '것보', '그렇', '편하', '아이', '제주', '질</w>', '떡</w>', '중</w>', '객</w>', '글</w>', '말해', '창</w>', '문</w>', '시</w>', '산</w>', '봉</w>', '장</w>', '진</w>', '봄</w>', '꽃</w>', '술</w>', '참</w>', '대</w>', '자기', '렌</w>', '희</w>', '그넹', '먹</w>', '찬</w>', '차</w>', '마시', '국</w>', '업</w>', '어서', '근</w>', '가지', '류</w>', '릿</w>', '드</w>', '편</w>', '마피', '석</w>', '회사', '번</w>', '거짓', '하는', '아보', '길</w>', '씨</w>', '킬</w>', '쟤</w>', '린</w>', '닌</w>', '심</w>', '구</w>', '죠</w>', '식</w>', '느</w>', '용</w>', '커</w>', '알았', '깔</w>', '전</w>', '시키', '닐</w>', '그거', '가', '하', '이', '보', '아', '그', '있', '사', '인', '주', '지', '거', '먹', '리', '어', '기', '나', '게', '난', '라', '자', '다', '없', '진', '고', '들', '만', '서', '해', '했', '로', '잘', '구', '오', '한', '원', '장', '저', '무', '네', '미', '여', '는', '되', '영', '내', '야', '터', '시', '클', '갔', '때', '같', '와', '수', '요', '놓', '팔', '부', '치', '선', '유', '언', '정', '알', '대', '뭐', '바', '많', '적', '할', '실', '종', '온', '물', '으', '출', '려', '산', '긴', '샀', '디', '러', '신', '좋', '경', '까', '마', '민', '우', '당', '래', '애', '중', '찾', '졌', '집', '었', '싶', '을', '글', '줬', '안', '매', '작', '먼', '돌', '스', '갈', '길', '동', '놀', '죽', '현', '모', '씨', '드', '머', '니', '합', '세', '택', '배', '봠', '엘', '힘', '운', '퇴', '도', '즈', '상', '태', '햄', '런', '전', '였', '됐', '일', '살', '뺄', '쪄', '웃', '소', '단', '추', '잰', '져', '노', '싱', '새', '떴', '않', '트', '간', '편', '판', '방', '빵', '녀', '론', '분', '문', '평', '볼', '걸', '브', '비', '조', '속', '똑', '달', '막', '더', '회', '의', '피', '누', '행', '블', '쏠', '쬐', '돈', '써', '금', '손', '투', '식', '확', '괜', '춘', '쓰', '넷', '앞', '베', '켱', '삼', '왔', '십', '몽', '캐', '뭉', '닮', '땐', '렴', '키', '닌', '핸', '프', '팩', '준', '겸', '믄', '던', '건', '돼', '플', '앱', '깔', '벽', '켓', '롱', '혼', '뭔', '차', '낫', '암', '튼', '올', '랑', '뵈', '싫', '르', '함', '탑', '맞', '본', '월', '봐', '핵', '심', '순', '꺼', '화', '틈', '빠', '특', '별', '응', '빼', '악', '국', '팀', '듣', '향', '껄', '댓', '꾸', '력', '표', '몰', '랬', '연', '갖', '럴', '짬', '음', '듀', '퍼', '포', '럽', '느', '끼', '렇', '말', '억', '티', '법', '얹', '체', '족', '곳', '생', '군', '너', '초', '앙', '댕', '겨', '청', '훨', '워', '탄', '약', '발', '불', '호', '관', '광', '공', '항', '랜', '봔', '봤', '잖', '입', '검', '된', '증', '았', '봉', '옆', '변', '욕', '찍', '났', '채', '꽃', '벚', '펴', '박', '람', '처', '임', '최', '근', '에', '킹', '또', '잇', '몇', '계', '번', '곤', '둘', '강', '케', '익', '겠', '콘', '외', '참', '뤄', '젼', '앤', '짧', '귀', '역', '재', '히', '크', '뻉', '명', '통', '은', '년', '받', '직', '넣', '켠', '켰', '젠', '제', '숨', '황', '감', '예', '릴', '코', '린', '큐', '것', '개', '색', '반', '댄', '끄', '용']

입력 단어: 한한구갹국겨계껴ㅖ

[한한구갹국겨계껴ㅖ] 매치 토큰: 구 / 인덱스: [2]
[한한구갹국겨계껴ㅖ] 서브 스트링: 한한 (0~2)
[한한] 매치 토큰: 한 / 인덱스: [0, 1]
[한한] 서브 스트링:  (0~0)
[한한] 현재 토크나이즈 결과: ['한']
[한한] 서브 스트링:  (1~1)
[한한] 현재 토크나이즈 결과: ['한', '한']
[한한구갹국겨계껴ㅖ] 현재 토크나이즈 결과: ['한', '한', '구']
[갹국겨계껴ㅖ] 매치 토큰: 국 / 인덱스: [1]
[갹국겨계껴ㅖ] 서브 스트링: 갹 (0~1)
[갹국겨계껴ㅖ] 현재 토크나이즈 결과: ['</u>', '국']
[겨계껴ㅖ] 매치 토큰: 겨 / 인덱스: [0]
[겨계껴ㅖ] 서브 스트링:  (0~0)
[겨계껴ㅖ] 현재 토크나이즈 결과: ['겨']
[계껴ㅖ] 매치 토큰: 계 / 인덱스: [0]
[계껴ㅖ] 서브 스트링:  (0~0)
[계껴ㅖ] 현재 토크나이즈 결과: ['계']
입력 단어 토큰화 결과: ['한', '한', '구', '</u>', '국', '겨', '계', '</u>']

입력 단어: 사랑합니다</w>
...
[참아보] 현재 토크나이즈 결과: ['참', '아보']
[참아보기</w>] 현재 토크나이즈 결과: ['참', '아보', '기</w>']
입력 단어 토큰화 결과: ['야', '식</w>', '안', '먹고</w>', '참', '아보', '기</w>']

 

이상으로 Byte-Pair Encoding을 한국어에 적용했을 때, 사전이 어떤 식으로 구축되는지에 대해 알아보았습니다.

실제 프로젝트에 BPE를 적용하고자 한다면, 저자의 [공식 구현체](https://github.com/rsennrich/subword-nmt)를

사용하시는 것이 좋을 것 같습니다.

 

 

이렇게 형태소를 사용하지 않고 해 봤는데요.

나름 재밌는 것 같습니다.

지금도 돌아가고 있는 저의 형태소 분석기 코드가.. 오늘 안에는 끝나주기를 바라며

머킹이었습니다.