Text 데이터의 전처리 (1)

용가리·2024년 9월 4일

반갑습니다.
텍스트데이터의 전처리에 대해 설명해볼까 합니다.

데이터에는 다양한 종류가 있죠.
이미지데이터, 동영상데이터, 텍스트데이터, 수치형데이터, 범주형데이터 등등,,,

비정형 데이터들은 각자마다 처리 방법이 다르고 어렵습니다.
하지만 AI모델을 구축하기 위해서는 전처리가 필수적이죠.
제가 배운 내용들을 설명해보겠습니다.

텍스트를 어떻게 훈련 가능한 데이터로 바꿔 ???

AI 모델은 글자를 입력으로 받지 않습니다. 무조건 숫자로 받아야 하죠.
그렇다면, 우리가 해야 할 일은 텍스트 데이터를 아주 알맞는 숫자들의 배열로 바꾸는 것이 되겠습니다.

Tokenization

일단, 단어들을 알맞게 쪼개야 합니다.
왜 단어를 쪼개냐
숫자의 형태로 바꾸려면 각각의 단어들을 숫자로 1:1 매핑해야 합니다.
그니까 Key와 Value로 구성된 딕셔너리를 만들고 싶은거죠.
그렇다면 Key들의 집합을 구성해야 Value를 만들 수 있겠습니다.

그러면 어떠한 기준으로 단어를 쪼갤까요?
크게
Word-level-Tokenization
Character-level Tokenization
Subword-level Tokenization
이 있겠습니다.
간단한 예시 문장을 만들어보겠습니다.

ex) 썩을 예비군 때문에 블로그를 못썼다.

  1. Word-level Tokenization
    ['썩을', ' ', '예비군', ' ', '때문에', ' ', '블로그를', ' ', '못썼다', '.']

  2. Character-level Tokenization
    ['썩', '을', ' ', '예', '비', '군',' ', '때', '문', '에', ' ', '블', '로', '그', '를',' ', '못', '썼', '다', '.']

  3. Subword-level Tokenization
    ['썩', '을', ' ', '예비', '군', ' ', '때문', '에', ' ', '블로그', '를', ' ', '못', '썼다', '.']

이 중에서, Subword-level Tokenization에서는 Subword의 단위를 다양하게 정합니다.
뭐 어간별로 나눌 수도 있고, 표제어로 나눌 수도 있습니다.

나누는 방법들중 하나인 Byte-Pair Encoding에 대해 설명해보겠습니다.

Byte-Pair Encoding?

바이트 페어 인코딩은, 일단 Character-level Encoding을 합니다.
그 다음으로 많이 나오는 단어들을 차근차근 더해가는 방식입니다.
왜 이렇게 할까요?
['abc', 'abb', 'abcc', 'abbd', 'abf']라는 문장이 있다고 해봅시다.
만약 Character-level Encoding을 한다면,
단어들 리스트는 ['a','b','c','d','f']가 나오겠죠?
하지만 이 캐릭터 하나하나들은 의미를 많이 담지 못합니다. 만약 'ab'라는 단어가 문장 중 매우 많이 나왔다고 하더라도, 'ab'를 'a' 와 'b' 로 표현합니다.
이것은 단어의 의미를 많이 담지 못하겠죠.
그렇다면 'ab'를 추가하는겁니다.
그 다음 또 많이 나오는 철자들을 점차 추가해가며 본인이 지정한 길이가 될 때까지 반복하는것이죠.
즉, character 단위 인코딩을 하되 많이 나오는 단어들은 따로 모아서 더 추가하겠다
라는 의미입니다.

import re
import collections
from collections import Counter
from typing import Dict, List

corpus = ['abc', 'abb', 'abcc', 'abbd', 'abf']
WORD_END = '_'
max_vocab_size = 8

# 두 단어쌍들을 합친 것의 빈도를 찾는다.
def get_stats(vocab):
    pairs = collections.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 pairs
  
# 이 정규 표현식은 특정 단어 쌍(bigram)을 독립된 단어로 매칭
# bigram이 다른 단어에 붙어 있지 않고 정확히 단어 경계에서만 존재하는 경우에만 매칭
# ex) "hello world"는 매칭되지만 "hello-world"나 "helloworld"는 매칭되지 않음.
def merge_vocab(pair : tuple, v_in: Dict[str, int]) -> Dict[str, int]:
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out
    
# 말뭉치들에 '_' 단어를 추가하고, 각 단어들의 빈도수를 체크
# 하나의 단위의 알파벳들은 final_vocab에 넣음
corpus = [word + WORD_END for word in corpus]
vocab = Counter([' '.join(word) for word in corpus])
final_vocab = set()
for word in vocab:
    for char in word:
        if char != ' ':
            final_vocab.add(char)
          

while len(final_vocab) < max_vocab_size:
    pairs = get_stats(vocab)
    if not pairs:
        break
    most = sorted(pairs.items(), key = lambda x: (-x[1], x[0]), reverse = True)[-1][0]
    vocab = merge_vocab(most, vocab)
    final_vocab.add(''.join(most))
    
print(final_vocab)

그르니까
{('a', 'b'): 5, ('b', 'c'): 2, ('c', ''): 2, ('b', 'b'): 2, ('b', ''): 1, ('c', 'c'): 1, ('b', 'd'): 1, ('d', ''): 1, ('b', 'f'): 1, ('f', ''): 1})
와 같이 단어를 구성하고 있는 character 두 쌍에 대해서 빈도수를 체크 해봅니다.
가장 빈도가 많은건 final_vocab에 추가하겠다는 것이죠.

finalvocab = {'', 'a', 'ab', 'abb', 'b', 'c', 'd', 'f'}
같이 나왔다면, 이제 다 한겁니다.
각 final_vocab의 단어들을 key로, 숫자를 value로 매핑하면 되는 것입니다.

Byte-pair Encoding 외에도 파이썬에서 사용할 수 있는 다양한 라이브러리들이 있습니다. spacy, konoply, Bert 등이 여기에 해당됩니다.

자연어처리에서의 전처리는 알고리즘 문제들처럼 어려운 것 같습니다.

다음 글에는 숫자로 인코딩 된 문자들을 학습 데이터로 사용하는 방법들에 대해 적어보겠습니다.
감사합니다.

0개의 댓글