본문 바로가기

자연어처리

텍스트 표준화, 토큰화, 어휘 사전(단어 집합) 인덱싱

■ 말뭉치(corpuis)라고 부르는 텍스트 데이터는 원시 텍스트(ASCII, UTF-8 등)와 원시 텍스트와 연관된 메타데이터를 포함하고 있다. 

■ 컴퓨터는 텍스트와 같은 비수치적 데이터를 직접 처리할 수 없다. 텍스드 데이터를 수치 데이터로 변환하는 과정이 필요하다. 

또한, 딥러닝 모델은 행렬 연산, 미분 등 수학적 계산을 통해 학습을 수행하기 때문에 입력 데이터로 원시 텍스트를 사용할 수 없다. 숫자 데이터(수치 텐서)로 처리해야 한다.

■ 텍스트를 숫자 데이터(수치 텐서)로 바꾸는 과정을 텍스트 벡터화(vectorization)이라고 한다.

■ 텍스트 벡터화 과정은 다양하고, 그 결과도 다양(원-핫 인코딩, 워드 임베딩 등)하지만, 모두 다음 그림과 같은 동일한 양식을 따른다.

[출처] https://rec-of-tech.tistory.com/28

- (1) 텍스트 데이터를 처리하기 쉽도록 표준화한다. 

- 노이즈 데이터(여기서는 자연어가 아니면서, 아무 의미를 갖지 않는 글자들(특수 문자 등)을 말함) 제거와 표현 방법이 다른 언어들을 통합시킨다.

- 대문자를 소문자로 바꾸거나, 구두점(punctuation)을 제거한다.

- 구두점은 마침표(.), 컴마(,), 물음표(?), 느낌표(!) 등과 같은 기호

- (2) 텍스트를 토큰 단위로 분할한다. 토큰 단위로 나누는 작업을 토큰화(tokenization)라고 부른다.

- 토큰의 기준을 단어(word)로 하는 경우, 단어 토큰화라고 한다. 여기서 단어는 단어구, 의미를 갖는 문자열을 포함한다.

- (3) 모든 토큰을 인덱싱하고 수치 벡터로 바꾼다. 

- 토큰 인덱싱을 통해 단어들을 고유한 정수로 맵핑하였으면, 원-핫 인코딩이나 워드 임베딩으로 각 정수를 고유한 단어 벡터로 바꿔주면 된다. 주로 워드 임베딩이 사용된다.

 

1. 텍스트 표준화

■ 이 단계는 토큰화 작업을 수행하기 전에, 토큰화 작업에 방해가 되는 부분들을 제거하는 단계이다. 

- 토큰화 작업 이후에도 여전히 노이즈 데이터가 남아있는 경우, 사용 용도에 맞게 텍스트를 다시 표준화한다.

- 경우에 따라 완벽한 표준화는 어렵기 때문에 어떤 기준을 세워 표준화를 수행하기도 한다.

■ 일반적인 표준화 방법 중 하나는 대문자를 소문자로, 구두점과 특수 문자를 삭제하는 것이다.

■ 예를 들어, 영어권 언어에서 cafe와 café, USA와 US, i와 I, staring, stared 등의 단어가 같은 의미를 지니거나, 동일한 동사라는 것을 모델은 알지 못한다. 그러므로 표준화를 하지 않고 인덱싱을 하면, 동일한 단어에 서로 다른 정수로 인덱싱이 되는 상황이 발생한다.

표준화를 통해 cafe와 café, USA와 US, i와 I가 모두 같은 의미이며, staring과 stared가 동일한 동사라는 것을 알려주기 위해 하나의 단어로 정규화할 필요가 있다. 이렇게 표기가 다른 언어들을 통합하는 방법으로 어간 추출(stemming)표제어 추출(lemmatization)이 있다.

- 표제어(lemma)는 단어의 기본형이다. 예를 들어 staring, stared 등 어미가 바뀌면서 표제어 stare에서 여러 단어로 변형된다.

- 표제어 추출은 staring, stared 등의 표제어의 변형 단어를 모두 표제어로 바꾸기 때문에 동일한 단어가 서로 다른 정수로 인덱싱이 되는 상황을 방지할 수 있다. 이렇게 하면, staring과 stared가 같은 의미라는 것을 학습하기 위해 더 많은 샘플이 필요하지 않으므로  staring과 stared과 같은 관계를 학습하기 위해 필요한 훈련 데이터가 줄어든다.

- 어간 추출은 표제어 추출 대신에 사용하는 축소 기법으로, 단어의 끝을 잘라 어간(stem, 굴절하는 단어에서 변화하지 않는 부분, 용언 활용 시 변하지 않는 부분 ex) 먹다, 먹고, 먹으며, ... 에서 '먹-'이 어간)이라는 공통 형태로 축소한다.

text = "The cats are running quickly towards the mice."

## nltk 표제어 추출
from nltk.stem import WordNetLemmatizer

l = WordNetLemmatizer()
words = text.split(' ')
for w in words:
    print(w, '->', l.lemmatize(w))
# print([l.lemmatize(w) for w in words])
```#결과#```
The -> The
cats -> cat
are -> are
running -> running
quickly -> quickly
towards -> towards
the -> the
mice. -> mice.
````````````

l.lemmatize('running', 'v')
```#결과#```
'run'
````````````

- cats는 cat으로 표제어 추출이 되었짐난, running같은 경우 표제어 추출이 수행되지 않은 것을 볼 수 있다.

- 이런 경우, 표제어 모델에 원래 단어의 품사를 지정해야 한다.

## spacy 표제어 추출
import spacy
spacy_en = spacy.load("en_core_web_sm")

text = spacy_en(u"The cats are running quickly towards the mice.")
for token in text:
    print(token,'->',token.lemma_)
```#결과#```
The -> the
cats -> cat
are -> be
running -> run
quickly -> quickly
towards -> towards
the -> the
mice -> mouse
. -> .
````````````

- 어간 추출은 다음과 같다.

## 어간 추출 - porterstemmer, LancasterStemmer
from nltk.stem import PorterStemmer, LancasterStemmer

porter = PorterStemmer()
lancaster = LancasterStemmer()
print([porter.stem(w) for w in words])
print([lancaster.stem(w) for w in words])
```#결과#```
['the', 'cat', 'are', 'run', 'quickli', 'toward', 'the', 'mice.']
['the', 'cat', 'ar', 'run', 'quick', 'toward', 'the', 'mice.']
````````````

-  두 어간 추출기의 결과를 보면, 어간을 자르는 방법이 모두 다른 것을 볼 수 있다. 어떤 규칙을 따르냐에 따라 어간 추출의 결과가 달라진다.

■ 구두점이나 특수 문자의 경우, 구두점과 특수 문자를 모두 제거하는 것이 항상 바람직한 것이 아니다. 데이터를 어떤 용도로 사용할 것인지에 따라 용도에 영향이 없게끔 제거해야 한다.

- 예를 들어, 단어 자체에 구두점을 갖고 있는 경우가 있다. Ph.D, a.k.a 등

- 그리고 마침표(.)같은 경우, 문장의 경계를 알 수 있는데 도움이 된다.

- 특수 문자의 경우 $는 $11.10과 같이 가격을 의미하기도, 슬래시(/)는 2025/01/01같이 날짜를 의미하기도 한다. 

- $11.10에서 구두점과 특수 문자를 모두 제거한다고 하면, 의미를 알 수 없는 11 10이 된다.

- 이렇게 구두점이나 특수문자를 전부 제거하면, 토큰화 결과 토큰이 의미를 잃어버리는 경우가 발생할 수 있다.

■ 그리고 아무 의미도 갖지 않는 글자들 외에 사용하려는 것에 필요하지 않는 불필요 단어들도 노이즈 데이터라고 부른다.

■ 불필요 단어들을 제거하는 방법으로 등장 빈도가 적은 단어, 길이가 짧은 단어들을 제거하는 방법과 불용어(stopwords) 제거가 있다.

■ 영어권 언어에서는 길이가 짧은 단어를 제거하는 것만으로도 어느 정도 큰 의미가 없는 단어들을 제거하는 효과를 볼 수 있다. 이는 한국어와 달리 영어 단어의 길이가 평균적으로 길기 때문이다.

- 예를 들어 '학교'라는 단어는 한국어에서는 '학'과 '교' 단어 하나 하나에 함축적인 의미(배울 학, 학교 교)를 지니고 있다.

- 반면, 영어에서는 s, c, h, o, o, l이라는 총 6개의 글자가 필요하다. 

■ 그러므로 영어 단어 중 길이가 2 이하인 단어를 제거하는 것만으로 큰 의미를 갖지 못하는 단어를 배제시킬 수 있다.

- 영어에서 불용어에 해당되는 it이나 전치사 at, on, in, by, to 등의 단어는 단어의 길이가 2이다.

- 필요에 따라 길이가 3인 단어도 제거할 수 있다. 단, 길이가 3인 명사들(car, dog, cat 등)이 존재하므로, 데이터를 사용하고자 하는 목적에 따라 어떤 단어가 불용어에 해당되는지 기준을 정할 필요가 있다.

■ 그리고 말뭉치 데이터에 너무 적게 등장해서 도움이 되지 않는 단어들도 존재한다.

- 예를 들어 100,000개의 메일로 정상과 스팸을 분류하는 문제에서 분류 기준으로 자주 등장하는 단어를 사용할 수 있다.

- 이때, 100,000개 메일 중에서 1~2번 혹은 몇 개 이하밖에 등장하지 않은 단어가 있다면, 직관적으로 해당 단어들이 풀고자 하는 문제에 도움이 되지 않을 것임을 알 수 있다.

■ 토큰화 작업 후, 유의미한 토큰만 선별하기 위해 큰 의미가 없는(자주 등장하지만, 의미 분석에 큰 도움이 되지 않는) 토큰을 제거하는 작업이 필요할 수 있다. 

예를 들어 조사, 접미사같은 것들은 문장에 자주 등장하지만, 의미 분석에 큰 도움이 되지 않는다. 이러한 단어들을 불용어라고 한다.

- 불용어는 사용자가 직접 정의할 수도 있다.

■ NLTK에서는 다음과 같은 영어 단어들을 불용어로 정의하고 있다.

- stowords.words('english')를 통해 NLTK가 정의한 영어 불용어 리스트를 확인할 수 있다.

from nltk.corpus import stopwords

stopwords_list = stopwords.words('english')
print('nltk 불용어 영어 단어 개수: ', len(stopwords_list))
print('nltk 불용어 5개: ', stopwords_list[:5])
```#결과#```
nltk 불용어 영어 단어 개수:  179
nltk 불용어 5개:  ['i', 'me', 'my', 'myself', 'we']
````````````

- i, me, my와 같은 단어들을 불용어로 정의하고 있음을 볼 수 있다.

■ NLTK의 word_tokenize를 통해 토큰화를 수행하고, 다음과 같이 토큰화 결과에서 NLTK가 정의하고 있는 불용어를 제외할 수 있다.

from nltk.tokenize import word_tokenize

stop_words = set(stopwords.words('english'))
text = "This is an example sentence, showing how stopwords can be removed from a text."
word_tokens = word_tokenize(text)
print('토큰화 결과: ', word_tokens)

result = []
for word in word_tokens: # 토큰화 결과 중에서(토큰 중에서)
    if word not in stop_words: # nltk에서 정의한 불용어가 아닌 단어만
        result.append(word) # 결과에 추가
        
print('불용어 제거 후 토큰화 결과: ', result)     
```#결과#```
토큰화 결과:  ['This', 'is', 'an', 'example', 'sentence', ',', 'showing', 'how', 'stopwords', 'can', 'be', 'removed', 'from', 'a', 'text', '.']
불용어 제거 후 토큰화 결과:  ['This', 'example', 'sentence', ',', 'showing', 'stopwords', 'removed', 'text', '.']
````````````

 

2. 텍스트 토큰화

텍스트 표준화 후, 다음 단계는 텍스트를 벡터화할 단위(토큰)으로 나누는 단계이다. 이 단계를 토큰화라고 부른다.

■ 텍스트 토큰화는 3가지 방법으로 수행할 수 있다.

- (1) 단어 수준의 토큰화

- 공백이나 구두점으로 토큰을 나누는 방법이다. 

cf) 비슷한 다른 방법으로 단어(word)를 부분 단어(subword)로 더 나누는 방법이 있다. 예를 들어 staring \( \rightarrow \) star + ing

from nltk.tokenize import word_tokenize, sent_tokenize, TweetTokenizer, RegexpTokenizer

text = "The cats are running quickly towards the mice."
sentence = "The weather is nice today. However, it might \
rain in the evening. Let's go for a walk now!"
tweet = "#hastag cat @midnight, :)"

word_tokenize = word_tokenize(text)
sent_tokenize = sent_tokenize(sentence)
tweet_tokenizer = TweetTokenizer()

print('단어 토큰화: ', word_tokenize)
print('문장 토큰화: ', sent_tokenize)
print(tweet_tokenize.tokenize(tweet.lower()))
print(re_tokenizer.tokenize(("This is 12 a test sentence 123 with numbers").lower()))
```#결과#```
단어 토큰화:  ['The', 'cats', 'are', 'running', 'quickly', 'towards', 'the', 'mice', '.']
문장 토큰화:  ['The weather is nice today.', 'However, it might rain in the evening.', "Let's go for a walk now!"]
['#hastag', 'cat', '@midnight', ',', ':)']
['this is ', ' a test sentence ', ' with numbers']
````````````

-  NLTK에서는 영어 '문장'의 토큰화를 수행하는 sent_tokenize를 지원한다. 이외에 이모티콘을 인식하는 기능이 있는 토큰화, 정규 표현식을 이용한 토큰화 등이 있다.

- 정규 표현식 토큰화에 사용한 \d+는 숫자가 1개 이상인 경우를 의미하며, gaps = True는 해당 정규 표현식을 기준으로 토큰을 나눈다는 의미이다.

## spacy 토큰화
import spacy

spacy_en = spacy.load("en_core_web_sm")

def tokenize(text):
    return [token.text for token in spacy_en.tokenizer(text)]
print(tokenize(text))
```#결과#```
['The', 'cats', 'are', 'running', 'quickly', 'towards', 'the', 'mice', '.']
````````````

- (2) N-그램(N-gram) 토큰화

- N개의 연속된 단어 그룹으로 나누는 방법이다.

- 바이그램(bigram)은 토큰 두 개, 유니그램(unigram)은 토큰 한 개로 이루어진다.

def n_grams(text, n):
    result = []
    for i in range(len(text) - n+1):
        result.append(text[i:i+n])
    return result
    
print(n_grams(word_tokenize, 3))
```#결과#```
[['The', 'cats', 'are'], 
['cats', 'are', 'running'], 
['are', 'running', 'quickly'], 
['running', 'quickly', 'towards'], 
['quickly', 'towards', 'the'], 
['towards', 'the', 'mice'],
['the', 'mice', '.']]
````````````

- 예시에서 사용한 word_tokenize의 길이는 9이다. 3개의 연속된 단어 그룹으로 나눌 경우, text[0:3], text[1:4], text[2:5], .... 순으로 나눠지는 것을 볼 수 있다.

- (3) 문자 수준 토큰화

- 각 문자가 하나의 토큰이다. 

- 텍스트 생성이나 음성 인식같은 작업에서만 사용한다.

■ 위의 예시처럼 영어는 띄어쓰기 단위로 잘라도 단어 토큰이 분리되지만, 한국어는 띄어쓰기만으로 토근화를 하기에는 어려움이 있다. 그 이유는 한국어가 영어와는 달리, 교착어이기 때문이다.

- 교착어는 조사, 어미 등을 붙여서 말을 만드는 언어를 말한다.

- 예를 들어 한국어의 '그'가 주어나 목적어로 사용될 경우, 영어는 he/him이 있지만, 

- 한국어는 '그가', '그를', '그와', '그는', '그에게' 등 '그'라는 글자 뒤에 다양한 조사가 띄어씌기 없이 바로 붙게된다.

- 이렇게 한국어는 어절이 독립적인 단어로 이루어진 경우보다 다양한 요소(조사 등)가 결합된 형태가 많아, 이를 모두 분리해 주어야 한다.

■ 영어에서의 단어 토큰화와 유사한 형태를 얻으려면 한국어는 형태소(morpheme) 토큰화를 수행해야 한다.

■ 형태소란 뜻을 가진 가장 작은 말의 단위를 의미한다. 더 이상 쪼갤 수 없는 의미를 가진 단위로 볼 수 있다. 그리고 형태소를 자립 형태소의존 형태소. 2가지 기준으로 분류한다.

- 자립 형태소는 접사, 어미, 조사와 상관없이 자립하여 사용할 수 있는 형태소이며, 그 자체로 단어가 된다.

- 체언(명사, 대명사, 수사), 수식언(관형사, 부사), 감탄사 등이 있다.

- 의존 형태소는 다른 형태소와 결합하여 사용되는 형태소이며 접사, 어미, 조사, 어간을 말한다.

- 즉, 자립 형태소는 그 자체로 자립해서 사용할 수 있는 경우, 의존 형태소는 자립할 수 없으므로 의존하는 경우라고 볼 수 있다.

예를 들어, "강물이 매우 파랗다"라는 문장을  띄어쓰기로 토큰화를 수행했다면 ['강물이", "매우", "파랗다"]라는 결과를 얻을 것이다.

형태소 단위로 분해한다면 다음과 같이 분해될 것이다.

- 자립 형태소: 강, 물, 매우

- 의존 형태소: '-이', '파랗-', '-다'

"the river water is very blue"라는 영어 문장을 띄어쓰기로 분해하면 ["the", "river", "water", "is", "very", "blue"]가 될 것이다.

■ 형태소 단위로 분해하면, 위의 영어 문장 분해처럼 '강'과 '물'이라는 명사를 얻어낼 수 있다. 즉, 영어에서의 단어 토큰화와 유사한 결과를 얻으려면 어절 토큰화가 아니라 형태소 토큰화를 수행해야 한다.

■ 그리고 단어의 표기는 같지만, 품사에 따라 단어의 의미가 달라지는 경우가 있다. 영어도 마찬가지이다.

- 예를 들어, 영어 단어 'fly'는 '날다'라는 의미를 갖지만, 명사로 사용할 경우 '파리'라는 의미가 된다.

- 한국어에서 '못'이라는 명사는 목재 따위의 접합이나 고정에 쓰는 물건이라는 의미를 갖지만, 부사로 사용할 경우 동사가 나타내는 동작을 할 수 없거나 어떤 상태가 이루어지지 않았다는 부정의 뜻을 나타낸다.

■ 그러므로 단어 의미를 제대로 파악하기 위해, 해당 단어가 어떤 품사로 쓰였는지 확인이 필요할 경우가 있다.

■ 단어 토큰화 과정에서 단어가 어떤 품사로 쓰였는지 구분하는 작업이 있는데, 이 작어블 품사 태깅(part-of-speech tagging, POS tagging)이라고 한다.

■ NLTK에서는 Penn Treebank POS Tags라는 기준을 사용하여 품사를 태깅하고,

KoNLPy(코엔엘파이)에서는 형태소 분석기 Okt(Open Korea Text), 메캅(Mecab), 꼬꼬마(Kkma), 코모란(komoran) 등에서 품사 태깅을 지원한다.

- KoNLPy의 형태소 분석기는 품사 태깅뿐만 아니라 형태소 분석, 명사 추출 등을 지원한다.

- KoNLPy의 형태소 분석기에서 공통적으로 제공하는 메서드로, morphs 메서드는 형태소 추출, pos 메서드는 품사 태깅, nouns 메서드는 명사 추출을 수행한다. 

- morphs로 형태소 토큰화를 수행할 수 있다.

from nltk.tag import pos_tag
from nltk.tokenize import word_tokenize

word_tokenize = word_tokenize(text)
print(f'단어 토큰화: {word_tokenize}\n 품사 태깅 결과: {pos_tag(word_tokenize)}')
```#결과#```
단어 토큰화: ['The', 'cats', 'are', 'running', 'quickly', 'towards', 'the', 'mice', '.']
품사 태깅 결과: [('The', 'DT'), ('cats', 'NNS'), ('are', 'VBP'), ('running', 'VBG'), 
('quickly', 'RB'), ('towards', 'IN'), ('the', 'DT'), ('mice', 'NN'), ('.', '.')]
````````````

- Penn Treebank POS Tags에서 DT는 한정사(관사, 지시/소유격 형용사), NN은 단수 명사, NNS는 복수 명사, VBP는 동사, VBG는 현재분사, IN은 전치사, RB는 부사를 의미한다.

- 참고로 'I'같은 인칭 대명사는 PRP,  'were'같은 과거 동사는 VBD로 태깅된다. 

from konlpy.tag import Okt, Kkma, Hannanum

okt = Okt()
kkma = Kkma()
han = Hannanum()

kor_text = "하늘에 비구름이 끼었다"

print('OKT 형태소 토큰화 :',okt.morphs(kor_text))
print('OKT 품사 태깅 :',okt.pos(kor_text))
print('OKT 명사 추출 :',okt.nouns(kor_text))
```#결과#```
OKT 형태소 토큰화 : ['하늘', '에', '비구름', '이', '끼었다']
OKT 품사 태깅 : [('하늘', 'Noun'), ('에', 'Josa'), ('비구름', 'Noun'), ('이', 'Josa'), ('끼었다', 'Verb')]
OKT 명사 추출 : ['하늘', '비구름']
````````````

print('Kkma 형태소 토큰화 :',kkma.morphs(kor_text))
print('Kkma 품사 태깅 :',kkma.pos(kor_text))
print('Kkma 명사 추출 :',kkma.nouns(kor_text))
```#결과#```
Kkma 형태소 토큰화 : ['하늘', '에', '비구름', '이', '끼', '었', '다']
Kkma 품사 태깅 : [('하늘', 'NNG'), ('에', 'JKM'), ('비구름', 'NNG'), ('이', 'JKS'), ('끼', 'VV'), ('었', 'EPT'), ('다', 'EFN')]
Kkma 명사 추출 : ['하늘', '비구름']
````````````

print('Hannanum 형태소 토큰화 :',han.morphs(kor_text))
print('Hannanum 품사 태깅 :',han.pos(kor_text))
print('Hannanum 명사 추출 :',han.nouns(kor_text))
```#결과#```
Hannanum 형태소 토큰화 : ['하늘', '에', '비구름', '이', '끼', '이', '었다']
Hannanum 품사 태깅 : [('하늘', 'N'), ('에', 'J'), ('비구름', 'N'), ('이', 'J'), ('끼', 'N'), ('이', 'J'), ('었다', 'E')]
Hannanum 명사 추출 : ['하늘', '비구름', '끼'
````````````

- 각 한국어 형태소 분석기마다 결과가 다르게 나오는 것을 볼 수 있다. 그러므로, 어떤 형태소 분석기가 사용 목적에 적절한지 판단하고 사용해야 한다.

■ 한국어도 다음과 같은 불용어가 있다.

Korean Stopwords

 

Korean Stopwords

 

www.ranks.nl

- 단, 여기에 있는 불용어가 절대적인 기준은 아니다.

■ 한국어에서 불용어를 제거하는 간단한 방법은 토큰화 후에 조사, 접속사 등을 제거하는 것이다. 

■ 만약, 불용어가 아닌 명사, 형용사와 같은 단어들 중에서 제거하고 싶은 단어가 있다면, 해당 단어들을 불용어로 정의해서 제거할 수 있다. 다음 코드는 형태소 토큰화를 진행한 다음, 제거하고 싶은 단어를 불용어로 정의해서 불용어 제거를 수행하는 예이다.

han = Hannanum()
kor_text = "하늘에 비구름이 끼었다"

## 불용어 정의
stop_words = "에 비구름 이 끼" 
stop_words = set(stop_words.split(' '))
print(stop_words)
```#결과#```
{'끼', '에', '비구름', '이'}
````````````

kor_word_token = han.morphs(kor_text) # 형태소 토큰화 수행
result = [token for token in kor_word_token if not token in stop_words] # 토큰화 후, 불용어 제거
result
```#결과#```
['하늘', '었다']
````````````

 

3. 어휘 사전(Vocabulary) 인덱싱

■ 텍스트를 토큰으로 나눈 후, 다음 단계는 각 토큰에 고유한 정수(=중복을 제거하고)를 할당하는 것이다.

■ 이렇게 중복을 제거한 모든 토큰의 집합(set)을 단어 집합 또는 어휘 사전(Vocabulary)이라고 부른다. 

Vocabulary는 토큰의 저장과 관리뿐 아니라 토큰에 부여된 고유한 정수 인덱스를 이용해 문자를 숫자로 바꾸는데 사용한다.

■ 예를 들기 위해, 다음 깃허브 주소에서 원시 텍스트 데이터인 네이버 영화 리뷰 데이터를 사용

https://github.com/e9t/nsmc/

 

GitHub - e9t/nsmc: Naver sentiment movie corpus

Naver sentiment movie corpus. Contribute to e9t/nsmc development by creating an account on GitHub.

github.com

import pandas as pd
import numpy as np
import urllib.request

urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings.txt", 
                           filename="ratings.txt")
data = pd.read_table('ratings.txt') 
data.head()

data['label'].value_counts()
```#결과#```
label
1    100000
0    100000
Name: count, dtype: int64
````````````

sample_data = data[:100]

- 리뷰 데이터는 총 20만 개이며 긍정 리뷰는 1, 부정 리뷰는 0으로 레이블링된 데이터이다.

- 그 중에서 100개의 리뷰 데이터만 사용

■ 정규 표현식을 이용해 한글과 공백을 제외하고 모두 제거한 다음, 형태소 분석기로 형태소 토큰화를 수행하고 불용어를 제거하면 다음과 같다.

sample_data['document'] = sample_data['document'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","", regex=True)

[ㄱ-ㅎㅏ-ㅣ가-힣]은 한글을 의미한다.

- [ㄱ-ㅎ]는 ㄱ부터 ㅎ까지 자음 전체, [ㅏ-ㅣ]는 ㅏ부터 ㅣ까지 모음 전체, [가-힣]은 사전순으로 '가'부터 마지막 '힣'까지 글자 전체

# 불용어 정의
stopwords=['의','가','이','은','들','는','좀','잘','걍','과','도','를','으로','자','에','와','한','하다']

kkma = Kkma()

result = []
for sentence in sample_data.document:
    temp = kkma.morphs(sentence) # 형태소 토큰화 수행
    temp = [word for word in temp if not word in stopwords] # 불용어 제거
    result.append(temp) # 불용어 제거한 것만
    
print(result[:1])    
```#결과#```
[['어리', 'ㄹ', '때', '보고', '지금', '다시', '보', '아도', '재밌', '어요', 'ㅋㅋ']]
````````````

■ 이제, 어휘 사전을 만들어야 한다. 훈련 데이터로 가장 많이 등장하는 단어로만 어휘 사전을 만드는 것이 일반적이다.

■ 위의 result[:1]과 같이 텍스트 데이터를 토큰화하면, 고유한 토큰이 굉장히 많다. 그리고 한 번 또는 두 번 등장하는 토큰들이 대부분일 가능성이 높다.

드물게 등장하는 토큰들로 어휘 사전을 만들 경우 공간은 많이 차지하지만, 대부분은 거의 아무런 정보가 없어 도움이 되지 않는다.

■ NLTK에서는 빈도수 계산 도구인 FreqDist( )를 지원한다. 이를 이용하여 어휘 사전을 만들면

from nltk import FreqDist

vocabulary = FreqDist(np.hstack(result))
print(f'단어 집합의 크기 : {len(vocabulary)}')
```#결과#```
단어 집합의 크기 : 671
````````````

■ 단어를 키(key)로, 단어에 대한 빈도수가 값(value)로 저장된다. 

vocabulary['재밌']
```#결과#```
9
````````````

- '재밌'이라는 단어가 총 9번 등장했다.

■ 어휘 사전에서 빈도수 상위 100개의 단어만 어휘 사전으로 저장하기 위해 다음과 같이 most_common( )을 사용한다.

- FreqDist를 이용해서 어휘 사전을 만들고, 어휘 사전에 most_common( )을 사용하여 높은 빈도수를 가지는 단어들만 어휘 사전으로 제한할 수 있다.

vocabulary_size = 100
vocabulary = vocabulary.most_common(vocabulary_size) # 빈도수 상위 100개의 단어만
print(f'단어 집합의 크기 : {len(vocabulary)}')
```#결과#```
단어 집합의 크기 : 100
````````````

■ 다음 단계는, 각 토큰에 고유한 정수를 부여하는 단계이다. 

■ 어휘 사전에서 새로운 토큰을 찾을 때, 해당 토큰이 존재하지 않을 수도 있다. 토큰을 저장해뒀던 어휘 사전(Vocabulary)에 토큰이 없어서 처음 보는 토큰이 나오는 현상을 Out-of-Vocabulary(OOV)라고 한다.

이렇게 토큰을 저장해뒀던 어휘 사전에 토큰이 없는 경우를 다루기 위해 OOV 인덱스를 사용한다. 

■ 이 경우에 인덱스는 일반적으로 숫자 1을 사용한다. 1은 <unk>토큰을 의미하며, <unk> 토큰 같은 것을 OOV 토큰이라고 부르기도 한다.

<unk> 외에 일반적으로 사용하는 특별한 토큰이 하나 더 있다. 바로 <pad> 토큰이다.

■ <pad> 토큰을 마스킹(masking) 토큰이라고도 부른다. 이 토큰은 특히 시퀀스 데이터를 패딩하기 위해 사용한다. 이 토큰의 인덱스는 일반적으로 숫자 0을 사용한다.

■ 패딩은 모든 샘플들의 길이를 동일하게 맞추기 위한 작업이다.

시퀀스 데이터를 사용하는 경우 병렬 연산을 위해 배치 데이터가 동일해야 한다. 그러므로, 배치에 있는 모든 시퀀스 데이터의 길이를 동일하게 맞춰주기 위해 패딩을 수행한다.

■ <unk> 토큰은 '인식할 수 없는 단어', '모르는 단어'라는 의미를 가지고, <pad> 토큰은 '단어가 아니라 무시할 수 있는 토큰'이라는 의미를 갖는다. 보통 <unk> 토큰의 인덱스는 1, <pad> 토큰의 인덱스는 0으로 사용한다.

- 패딩 시, 숫자 0을 사용하면 제로 패딩(zero padding)이라고도 부른다.

word_to_index = {word[0] : index+2 for index, word in enumerate(vocabulary)}
word_to_index['pad'] = 0
word_to_index['unk'] = 1

word_to_index['재밌']
```#결과#```
22
````````````

■ 단어와 단어에 대응하는 고유한 정수를 가진 딕셔너리 word_to_index를 이용해, 다음과 같이 어휘 사전의 각 단어에 고유한 정수를 부여할 수 있다.

encoded = []
for line in result: # 토큰화 결과에서 1줄씩 리뷰를 읽음
    temp = []
    for word in line: # 리뷰 1줄마다 단어 1개씩 읽음
        try: 
            temp.append(word_to_index[word]) # word가 word_to_index에 있으면, word에 대응하는 정수로 
        except KeyError: # 어휘 사전에 없는 단어는 <unk> 토큰에 대응되는 정수 1로 대체
            temp.append(word_to_index['unk'])
        encoded.append(temp)
print(result[:1])
print(encoded[:1])
```#결과#```
[['어리', 'ㄹ', '때', '보고', '지금', '다시', '보', '아도', '재밌', '어요', 'ㅋㅋ']]
[[50, 8, 31, 1, 51, 38, 5, 65, 22, 23, 52]]
````````````

- 각 토큰들이 고유한 정수로 변환된 것을 볼 수 있다.

■ 패딩을 하려면, 먼저 어떤 리뷰가 토큰이 가장 많은지(어떤 리뷰가 길이가 최대인지) 확인해야 한다. 리뷰 길이가 최대인 리뷰를 기준으로 패딩해야 모든 리뷰들의 길이를 동일하게 맞춰줄 수 있기 때문이다.

print('최대 길이:', max(len(l) for l in encoded)) # 인코딩된 리뷰 데이터들의 길이 중 max
```#결과#```
최대 길이: 70
````````````

■ 이 예에서 가장 길이가 긴 리뷰의 길이는 70이다. 그러므로 패딩을 적용할 것이라면, 모든 리뷰의 길이를 70으로 맞춰야 한다.

max_len = max(len(l) for l in encoded)

for line in encoded: # 인코딩된 리뷰 데이터들 1개씩 읽음
    if len(line) < max_len: # 현재 읽은 리뷰 데이터의 길이가 max_lne보다 짧으면
        line += [word_to_index['pad']] * (max_len - len(line)) # 부족한 길이만큼 <pad> 토큰에 대응되는 정수 0을 채움
        
print('최대 길이:', max(len(l) for l in encoded))
print('최소 길이:', max(len(l) for l in encoded))
```#결과#```
최대 길이: 70
최소 길이: 70
````````````
print(encoded[:1])
```#결과#```
[[50, 8, 31, 1, 51, 38, 5, 65, 22, 23, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
````````````

- 빈 자리가 pad = 0으로 채워진 것을 볼 수 있다.

■ 단어들을 고유한 정수로 맵핑한 다음 단계는, 각 정수를 고유한 단어 벡터로 만드는 단계이다. 이런 정수를 모델이 처리할 수 있도록 원-핫 인코딩이나 워드 임베딩을 사용한다.

'자연어처리' 카테고리의 다른 글

유사도  (0) 2025.02.28
Bag of Words, DTM, TF-IDF  (0) 2025.02.27
언어 모델(Language Model)  (0) 2025.02.25
텐서(Tensor)  (0) 2025.01.20
기본 용어 (1)  (0) 2025.01.01