본문 바로가기

책/파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습

05 토큰화 (2) 하위 단어 토큰화

하위 단어 토큰화

현대 자연어 처리에서는 신조어의 발생, 오탈자, 축약어 등을 고려해야 하기 때문에 분석할 단어의 양이 많아져 어려움을 겪는다, 이를 해결하기 위한 방법 중 하나로 하위 단어 토큰화(Subword Tokenization)가 있다.

 

하위 단어 토큰화란 하나의 단어가 빈번하게 사용되는 하위 단어(Subword)의 조합으로 나누어 토큰화 하는 방법이다. 예를 들어 'reinforcement'라는 단어를 'rein', 'force', 'ment' 로 나눠 처리할 수 있다.

 

하위 단어 코느화를 적용하면 단어의 길이를 줄일 수 있어서 처리 속도가 빨리질 뿐만 아니라, OOV 문제, 신조어, 은어, 고유어 등으로 인한 문제를 완화할 수 있다.

 

바이트 페어 인코딩

이전 포스팅 참고

2023.10.05 - [Deep Learning/NLP] - 서브워드 토크나이저 (Subword Tokenizer)

 

서브워드 토크나이저 (Subword Tokenizer)

예를 들어 텍스트를 분류하는 문제를 다룬다고 하였을 때 토크나이저가 모든 단어에 대해서 알 필요가 없는 것은 사실이다. 그래서 vocabulary size를 정하고 빈도가 낮은 단어들은 OOV(out-of-vocabulary)

ai-junha.tistory.com

 

센텐스피스

센텐스피스(Sentencepiece) 라이브러리와 코포라(Korpora) 라이브러리를 활용해 토크나이저를 학습해본다.

 

센텐스피스 라이브러리는 구글에서 개발한 오픈소스 하위 단어 토크나이저 라이브러리이다. 바이트페어 인코딩과 유사한 알고리즘을 사용해 입력 데이터를 토큰화하고 단어 사전을 생성한다.

 

코포라 라이브러리는 국립구어원이나 AI Hub에서 제공하는 corpus 데이터를 쉽게 사용할 수 있게 제공하는 오픈소스 라이브러리다.

 

토크나이저 모델 학습

청와대 청원 데이터 다운로드

from Korpora import Korpora

corpus = Korpora.load("korean_petitions")
dataset = corpus.train
petition = dataset[0]

print("청원 시작일 :", petition.begin)
print("청원 종료일 :", petition.end)
print("청원 동의 수 :", petition.num_agree)
print("청원 범주 :", petition.category)
print("청원 제목 :", petition.title)
print("청원 본문 :", petition.text[:30])
청원 시작일 : 2017-08-25
청원 종료일 : 2017-09-24
청원 동의 수 : 88
청원 범주 : 육아/교육
청원 제목 : 학교는 인력센터, 취업센터가 아닙니다. 정말 간곡히 부탁드립니다.
청원 본문 : 안녕하세요. 현재 사대, 교대 등 교원양성학교들의 예비

코포라 라이브러리에서 제공하는 말뭉치 목록은 Korpora.corpus_list()로 확인할 수 있다.

 

학습 데이터세트 생성

petitions = corpus.get_all_texts()
with open("../corpus.txt", "w", encoding="utf-8") as f:
    for petition in petitions:
        f.write(petition + "\n")

corpus의 get_all_texts 메서드로 분문 데이터세트를 한 번에 불러온 뒤 하나의 텍스트 파일로 저장한다.

 

토크나이저 모델 학습

from sentencepiece import SentencePieceTrainer


SentencePieceTrainer.Train(
    "--input=../corpus.txt\
    --model_prefix=petition_bpe\
    --vocab_size=8000 model_type=bpe"
)
  • 토크나이저 모델 학습이 완료되면 petition_bpe.model 파일과 petition_bpe.vocab 파일이 생성된다.
  • vocab 파일을 열어보면 밑줄 문자가 포함된 데이터도 볼 수 있는데 그 이유는 센텐스피스 라이브러리가 띄어쓰기나 공백도 특수문자로 취급해 토큰화 과정에서 '_'(U+2581)로 공백을 표현하기 때문이다.
  • 예를 들어'Hello World'라는 문장은 'Hello_World'로 표현되며 '_Hello + _Wor + ld'로 토큰화 된다.

바이트 페어 인코딩 토큰화

from sentencepiece import SentencePieceProcessor


tokenizer = SentencePieceProcessor()
tokenizer.load("petition_bpe.model")

sentence = "안녕하세요, 토크나이저가 잘 학습되었군요!"
sentences = ["이렇게 입력값을 리스트로 받아서", "쉽게 토크나이저를 사용할 수 있답니다"]

tokenized_sentence = tokenizer.encode_as_pieces(sentence)
tokenized_sentences = tokenizer.encode_as_pieces(sentences)
print("단일 문장 토큰화 :", tokenized_sentence)
print("여러 문장 토큰화 :", tokenized_sentences)

encoded_sentence = tokenizer.encode_as_ids(sentence)
encoded_sentences = tokenizer.encode_as_ids(sentences)
print("단일 문장 정수 인코딩 :", encoded_sentence)
print("여러 문장 정수 인코딩 :", encoded_sentences)

decode_ids = tokenizer.decode_ids(encoded_sentences)
decode_pieces = tokenizer.decode_pieces(encoded_sentences)
print("정수 인코딩에서 문장 변환 :", decode_ids)
print("하위 단어 토큰에서 문장 변환 :", decode_pieces)
단일 문장 토큰화 : ['▁안녕하세요', ',', '▁토', '크', '나', '이', '저', '가', '▁잘', '▁학', '습', '되었', '군요', '!']
여러 문장 토큰화 : [['▁이렇게', '▁입', '력', '값을', '▁리', '스트', '로', '▁받아서'], ['▁쉽게', '▁토', '크', '나', '이', '저', '를', '▁사용할', '▁수', '▁있', '답니다']]
단일 문장 정수 인코딩 : [667, 6553, 994, 6880, 6544, 6513, 6590, 6523, 161, 110, 6554, 872, 787, 6648]
여러 문장 정수 인코딩 : [[372, 182, 6677, 4433, 1772, 1613, 6527, 4162], [1681, 994, 6880, 6544, 6513, 6590, 6536, 5852, 19, 5, 2639]]
정수 인코딩에서 문장 변환 : ['이렇게 입력값을 리스트로 받아서', '쉽게 토크나이저를 사용할 수 있답니다']
하위 단어 토큰에서 문장 변환 : ['이렇게 입력값을 리스트로 받아서', '쉽게 토크나이저를 사용할 수 있답니다']

 

어휘 사전 불러오기

tokenizer = SentencePieceProcessor()
tokenizer.load("petition_bpe.model")

vocab = {idx: tokenizer.id_to_piece(idx) for idx in range(tokenizer.get_piece_size())}
print(list(vocab.items())[:5])
print("vocab size :", len(vocab))
[(0, '<unk>'), (1, '<s>'), (2, '</s>'), (3, '니다'), (4, '▁이')]
vocab size : 8000
  • get_piece_size 메서드는 센텐스피스 모델에서 생성된 하위 단어의 개수를 반환하며, id_to_piece 메서드는 정숫값을 하위 단어로 변환하는 메서드다.
  • <unk> 토큰은 OOV 발생 시 매핑되는 토큰이며, <s>, </s>는 문장의 시작과 종료 지점을 표시하는 토큰이다.

 

워드피스

워드피스(Wordpiece) 토크나이저는 바이트 페어 인코딩과 유사한 방법으로 학습되지만 빈도 기반이 아닌 확률 기반으로 글자 쌍을 병합한다.

 

모델이 새로운 하위 단어를 생성할 때 이전 하위 단어와 함께 나타날 확률을 계산해 가장 높은 확률을 가진 하위 단어를 선택한다. 이렇게 선택된 하위 단어는 이후에 더 높은 확률로 선택될 가능성이 높으며, 이를 통해 모델이 좀 더 정확한 하위 단어로 분리할 수 있다. 각 글자 쌍에 대한 점수는 다음과 같이 계산된다.

 

$score = {f(x,y) \over f(x)f(y)}$

 

  • f는 빈도를 나타내며, x, y는 병합하려는 하위 단어를 의미한다.
  • f(x, y)는 x와 y가 조합된 글자 쌍의 빈도를 의미한다.

말뭉치에서 다음과 같은 빈도 사전과 어휘 사전이 만들어졌다고 가정해보자

  • 빈도 사전 : (l, o, w, 5), (l, o, w, e, r, 2), (n, e, w, e, s, t, 6), (w, i, d, e, s, t, 3)
  • 어휘 사전 : [d, e, i, l, n, o, r, s, t, w]

가장 빈번하게 등장한 쌍은 9번 등장한 es다. 하지만 e 17번, s는 9번 등장하므로 점수는 ${9 \over {17 * 9}} = 0.06$이 된다. id 쌍은 3번 밖에 등장하지 않았지만 i와 d가 각각 3번씩 등장하므로 점수는 ${3 \over {3*3}} = 0.33$이 된다. 따라서 es 쌍 대신 id 쌍을 병합한다.

  • 빈도 사전 : (l, o, w, 5), (l, o, w, e, r, 2), (n, e, w, e, s, t, 6), (w, i, d, e, s, t, 3)
  • 어휘 사전 : [d, e, i, l, n, o, r, s, t, w, id]

워드피스 토크나이저는 위 과정을 반복해 연속된 글자 쌍이 더 이상 나타나지 않거나 정해진 어휘 사전 크기에 도달할 때까지 학습한다.

 

토크나이저스

센텐스피스 라이브러리 대신 허깅페이스의 토크나이저스(Tokenizers) 라이브러리를 사용한다. 이는 정규화(Normalization)와 사전 토큰화(Pre-tokenization)를 제공한다.

 

정규화는 일관된 형식으로 텍스트를 표준화하고 모호한 경우를 방지하기 위해 일부 문자를 대체하거나 제거하는 등의 작업을 수행한다. 불필요한 공백 제거, 대소문자 변환, 유니코드 정규화, 구두점 처리, 특수 문자 처리 등을 제공한다. 유니코드는 인코딩 방식에 따라 동일한 글자가 여러 유니코드로 표현될 수 있다.

 

사전 토큰화는 입력 문장을 토큰화하기 전에 단어와 같은 작은 단위로 나누는 기능을 제공한다.

 

워드피스 토크나이저 학습

from tokenizers import Tokenizer
from tokenizers.models import WordPiece
from tokenizers.normalizers import Sequence, NFD, Lowercase
from tokenizers.pre_tokenizers import Whitespace


tokenizer = Tokenizer(WordPiece())
# normalizers 모듈에 포함된 클래스를 불러와 시퀀스 형식으로 인스턴스를 전달
# NFD 유니코드 정규화, 소문자 변환을 사용
tokenizer.normalizer = Sequence([NFD(), Lowercase()])
# pre_tokenizers 모듈에 포함된 클래스를 불러와 적용
# 공백과 구두점을 기준으로 분리
tokenizer.pre_tokenizer = Whitespace()

tokenizer.train(["../corpus.txt"])
tokenizer.save("../petition_wordpiece.json")

 

워드피스 토큰화

from tokenizers.decoders import WordPiece as WordPieceDecoder


tokenizer = Tokenizer.from_file("../petition_wordpiece.json")
tokenizer.decoder = WordPieceDecoder()

sentence = "안녕하세요, 토크나이저가 잘 학습되었군요!"
sentences = ["이렇게 입력값을 리스트로 받아서", "쉽게 토크나이저를 사용할 수 있답니다"]

encoded_sentence = tokenizer.encode(sentence)
encoded_sentences = tokenizer.encode_batch(sentences)

print("인코더 형식 :", type(encoded_sentence))

print("단일 문장 토큰화 :", encoded_sentence.tokens)
print("여러 문장 토큰화 :", [enc.tokens for enc in encoded_sentences])

print("단일 문장 정수 인코딩 :", encoded_sentence.ids)
print("여러 문장 정수 인코딩 :", [enc.ids for enc in encoded_sentences])

print("정수 인코딩에서 문장 변환 :", tokenizer.decode(encoded_sentence.ids))

 

바이트 페어 인코딩과 워드피스 외에도 유니코드 단위가 아닌 바이트 단위에서 토큰화하는 Byte-level Byte-Pair-Encoding이나 크기가 큰 어휘 사전에서 덜 필요한 토큰을 제거하며 학습하는 Unigram 등이 있다.