'Machine Learning'에 해당하는 글 17건

전처리와 정수 인코딩으로 각 문장들을 수치화 한 후에 각 문장들을 하나의 행렬로 보고 가장 긴 문장의 길이로 모든 문장들의 길이를 맞춰주는 것을 패딩(Padding) 이라 한다. 빠른 병렬 연산을 위해 필요한 기능이며, 보통 숫자 0으로 채워넣는 제로 패딩(zero padding) 을 사용한다. 패딩 길이를 지정해서 긴 문장을 자를 수도 있다.


Numpy 패딩


import numpy as np
 
encoded = [[15], [185], [135], [92], [2432], [32], [146], [146], [142], [773210111], [112313]]
 
max_len = max(len(item) for item in encoded)  # 정수 인코딩 된 각 문장의 가장 많은 요소 개수
 
for item in encoded:
    while len(item) < max_len:
        item.append(0)  # 제로 패딩
 
print(np.array(encoded))
 
 
''' 출력
[[ 1  5  0  0  0  0  0]
 [ 1  8  5  0  0  0  0]
 [ 1  3  5  0  0  0  0]
 [ 9  2  0  0  0  0  0]
 [ 2  4  3  2  0  0  0]
 [ 3  2  0  0  0  0  0]
 [ 1  4  6  0  0  0  0]
 [ 1  4  6  0  0  0  0]
 [ 1  4  2  0  0  0  0]
 [ 7  7  3  2 10  1 11]
 [ 1 12  3 13  0  0  0]]
'''
cs



Keras 패딩


from tensorflow.keras.preprocessing.sequence import pad_sequences
 
encoded = [[15], [185], [135], [92], [2432], [32], [146], [146], [142], [773210111], [112313]]
 
print(pad_sequences(encoded, padding='post', maxlen=7, value=0))
cs


pad_sequences 한 줄로 위와 동일한 결과를 출력한다.




WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

토큰화, 정제 및 정규화 등의 전처리 작업을 마친 후에, 컴퓨터가 데이터 처리를 더 빠르게 하도록 텍스트를 수치화 하는 것을 정수 인코딩(Integer Encoding) 이라 한다.


결론 : keras 의 Tokenizer 를 사용하면 쉽게 해결



정수 인코딩 과정

  1. 문장 토큰화, 정제 및 일반화, 단어 토큰화.
  2. 빈도수를 기록하여 빈도수가 높은 순서대로 정렬 
  3. 자연어 처리에서 빈도수가 낮은 단어는 의미를 가지지 않을 가능성이 높으므로 제외
  4. 빈도수 순서로 정렬된 단어에 순차적으로 인덱스 부여
  5. 각 단어를 부여된 인덱스로 맵핑.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from nltk.tokenize import sent_tokenize
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
 
text = "A barber is a person. a barber is good person. a barber is huge person. he Knew A Secret! The Secret He Kept is huge secret. Huge secret. His barber kept his word. a barber kept his word. His barber kept his secret. But keeping and keeping such a huge secret to himself was driving the barber crazy. the barber went up a huge mountain."
text = sent_tokenize(text)  # 문장 토큰화
 
vocab = {}
sentences = []
stop_words = set(stopwords.words('english'))
 
for i in text:
    sentence = word_tokenize(i)  # 단어 토큰화
    result = []
 
    for word in sentence:
        word = word.lower()  # 단어 중복제거를 위해 소문자화
        if word not in stop_words:
            if len(word) > 2:  # 3자 이상의 단어만 수집
                result.append(word)
                if word not in vocab:  # 단어 중복 제거
                    vocab[word] = 0
                vocab[word] += 1  # 빈도수 계산
 
    sentences.append(result)  # 문장별 정제된 단어 리스트
 
vocab_sorted = sorted(vocab.items(), key=lambda x: x[1], reverse=True)  # 빈도수 높은 순으로 단어 정렬
word_to_index = {}
= 0
for (word, frequency) in vocab_sorted:
    if frequency > 1:  # 빈도수 높은 순으로 정렬된 단어에 1부터 인덱스 부여
        i = i + 1
        word_to_index[word] = i
 
vocab_size = 5
words_frequency = [w for w, c in word_to_index.items() if c >= vocab_size + 1]
for w in words_frequency:  # 빈도수 상위 5개를 제외한 단어 제거
    del word_to_index[w]
 
word_to_index['OOV'= len(word_to_index) + 1  # OOV(Out-Of-Vocabulary) : 단어 집합에 없는 단어
encoded = []
for s in sentences:
    temp = []
    for w in s:
        try:
            temp.append(word_to_index[w])  # 단어를 인덱스로 치환
        except:
            temp.append(word_to_index['OOV'])
    encoded.append(temp)
 
print(word_to_index)
print(sentences)
print(encoded)
 

''' 출력 
{'barber'1'secret'2'huge'3'kept'4'person'5'OOV'6}
[['barber''person'], ['barber''good''person'], ['barber''huge''person'], ['knew''secret'], ['secret''kept''huge''secret'], ['huge''secret'], ['barber''kept''word'], ['barber''kept''word'], ['barber''kept''secret'], ['keeping''keeping''huge''secret''driving''barber''crazy'], ['barber''went''huge''mountain']]
[[15], [165], [135], [62], [2432], [32], [146], [146], [142], [6632616], [1636]]
'''
cs


뭔가 상당히 복잡해 보이는데 25줄까지는 전처리, 그 뒤로는 빈도수 계산하고 인덱스 부여한 뒤 단어 위치에 인덱스로 치환하는 과정이다.



27~38줄의 과정에 collection.Counter 를 사용할 수도 있다.


from collections import Counter
words = sum(sentences, [])  # 리스트 요소들을 하나의 리스트로 합치기
vocab = Counter(words)  # 중복 요소 제거 및 빈도수 계산하여 정렬
vocab = vocab.most_common(vocab_size)  # 빈도수 상위 개수만큼 추출
cs


마찬가지로 nltk.FreqDist 도 사용할 수 있다.


from nltk import FreqDist
import numpy as np
words = np.hstack(sentences)  # 리스트 요소들을 하나의 리스트로 합치기
vocab = FreqDist(words)   # 중복 요소 제거 및 빈도수 계산하여 정렬
cs


이 모든 것들을 한번에 해결하는 keras...


from tensorflow.keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer(6)  # 빈도수 상위 5개
tokenizer.fit_on_texts(sentences)  # 빈도수를 기준으로 단어 집합 생성
encode = tokenizer.texts_to_sequences(sentences)  # 정수 인코딩
cs


생성된 tokenizer 로 인덱스 부여 확인(word_index), 빈도수 확인(word_counts) 도 가능하다. 기본적으로 OOV 는 제거되므로 OOV 를 보존하려면 Tokenizer 에 oov_token 인자를 사용한다. 이 경우 OOV 의 default 인덱스 값은 1이며, 나머지 상위 5개 단어의 인덱스는 2부터 시작된다.


tokenizer = Tokenizer(num_words = vocab_size + 2, oov_token = 'OOV')
cs




WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

전처리에서 정규 표현식 모듈(re) 을 토큰화와 정제, 정규화 등에 사용할 수 있으며, NLTK 를 통한 정규 표현식 사용도 가능하다.



re 모듈을 사용한 토큰화


import re
 
pattern = re.compile("a+")
result = pattern.match("abc")
# equal : pattern.match("a+", "abc)
# result : <re.Match object; span=(0, 1), match='a'>
 
text = "Let's write RegEx! Won't that be fun? I sure think so. Can you find 4 sentences?"
 
# 단어별로 분리 (특수문자 제외)
print(re.findall(r"\w+", text))
# 공백으로 분리
print(re.split(r"\s+", text))
# 문장 끝 부호로 분리
print(re.split(r"[.!?]\s+", text))
# 기호와 1~2자 단어 제거
print(re.sub(r"\W*\b\w{1,2}\b"'', text))
 
""" 출력
['Let', 's', 'write', 'RegEx', 'Won', 't', 'that', 'be', 'fun', 'I', 'sure', 'think', 'so', 'Can', 'you', 'find', '4', 'sentences']
["Let's", 'write', 'RegEx!', "Won't", 'that', 'be', 'fun?', 'I', 'sure', 'think', 'so.', 'Can', 'you', 'find', '4', 'sentences?']
["Let's write RegEx", "Won't that be fun", 'I sure think so', 'Can you find 4 sentences?']
Let write RegEx! Won that fun sure think. Can you find sentences?
"""
cs



NLTK 의 RegexpTokenizer 를 사용한 토큰화


from nltk.tokenize import RegexpTokenizer
from nltk.tokenize import regexp_tokenize
 
text = "Good muffins cost $3.88\nin New York. Please buy me\ntwo of them.\n\nThanks."
tokenizer = RegexpTokenizer(r"[\w]+")
print(tokenizer.tokenize(text))
print(regexp_tokenize(text, r"[\s]+", gaps=True))
# 위의 gaps 는 정규표현식을 토큰으로 나눌 것인지 여부
 
""" 출력
['Good', 'muffins', 'cost', '3', '88', 'in', 'New', 'York', 'Please', 'buy', 'me', 'two', 'of', 'them', 'Thanks']
['Good', 'muffins', 'cost', '$3.88', 'in', 'New', 'York.', 'Please', 'buy', 'me', 'two', 'of', 'them.', 'Thanks.']
"""
cs



WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

코퍼스 토큰화(tokenization) 를 마쳤다면, 불필요한 토큰들을 없애거나 표현 방법이 다른 단어들을 하나로 통합하여 코퍼스의 복잡성을 줄여야 한다.



정제(cleaning)


언어 및 데이터의 특징에 따라 규칙을 정하여 함수나 정규 표현식(Regular Expression)으로 의미없는 기호나 단어(불용어: Stopword)를 제거하는 일이다. 불용어는 필요한 경우 사용자 정의로 추가하는 것이 가능하다.


# NLTK 로 불용어 제거하기
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
 
# 불용어 코퍼스 다운로드
nltk.download('stopwords')
 
# 영어 불용어 가져오기
stop_words = stopwords.words('english')
 
# 영어 불용어 개수와 10개 미리보기
print(len(stop_words))
print(stop_words[:10])
 
eng_stop_words = set(stop_words)
text = "There is a tree near the river"
word_tokens = word_tokenize(text)
 
for w in word_tokens:
    if w not in eng_stop_words:
        print(w)
 
''' 출력
179
['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're"]
There
tree
near
river 
'''
cs


위와 같이 불용어 제거는 불용어 리스트를 이용해 for 문을 돌려 제거하면 되겠다. 한국어도 마찬가지로 txt / csv 파일 등으로 불용어를 관리하며 불러와 사용하는 것이 일반적이다. (불용어 리스트 예 - https://www.ranks.nl/stopwords/korean)



정규화(normalization)


정규화는 표제어 추출과 어간 추출을 사용하여 다르게 표현한, 같은 뜻의 단어들을 하나의 단어로 통합한다. 표제어 추출(lemmatization) 은 형태소(morphology) 종류인 어간(stem) 과 접사(affix) 로 분리하며 기본형으로 바꾸고 품사 태그는 보존된다. (예, am/are/is -> be). 어간 추출(stemming) 은 적당히 어간을 잘라내므로 품사 태그가 보존되지 않는다. (예, changing/changed -> chang). Porter Stemmer, Lancaster Stemmer, ... 등 각각의 알고리즘을 이해하고 적합한 스태머를 사용해야 한다. 한국어의 어간 추출에는 [어간+어미] 에서 어간이 변하지 않는 규칙 활용과 어간이 변하는 불규칙 활용이 있다.


# 표제어 추출 / 어간 추출
import nltk
from nltk.stem import WordNetLemmatizer
from nltk.stem import PorterStemmer
from nltk.stem import LancasterStemmer
 
nltk.download('wordnet')
lemmatizer = WordNetLemmatizer()
porter_stemmer = PorterStemmer()
lancaster_stemmer = LancasterStemmer()
 
words = ['rocks''corpora''better''are''has''swimming']
 
# 표제어 추출 (lemmatization)
print([lemmatizer.lemmatize(w) for w in words])
print(lemmatizer.lemmatize('better''a'))
 
# 어간 추출 (stemming)
print([porter_stemmer.stem(w) for w in words])
print([lancaster_stemmer.stem(w) for w in words])
 
 
''' 출력
['rock', 'corpus', 'better', 'are', 'ha', 'swimming']
good
['rock', 'corpora', 'better', 'are', 'ha', 'swim']
['rock', 'corpor', 'bet', 'ar', 'has', 'swim']
'''
cs




WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

토큰화 : 코퍼스를 의미 있는 단위로 쪼갬 (단어/어절/문장/...)

- ['아버지', '가', '방', '에', '들어가', '신다']


코퍼스를 수집하고 가장 먼저 해야할 것은, 데이터들을 의미 있는 작은 단위(Token)로 쪼개는 토큰화(tokenization) 작업을 해야 한다. 띄어쓰기 별로 나눈다던지, 어절별로 나눈다던지, 형태소별로 나누던지, 이러한 토큰화 작업을 하면 토큰들에 대한 분석이 가능해 진다.



언어별 토큰화의 어려움


의미있는 단어들을 추출하기 위한 첫번째 단계인 토큰화는 언어별로 섬세한 알고리즘을 필요로 한다. 예로 들면, 영어권 언어들은 띄어쓰기 단위의 어절 토큰화가 단어 토큰화와 같아 합성어(New York)나 줄임말(I'm) 정도의 예외처리만 잘하면 단어(word) 토큰화가 가능하다. 한국어의 경우 띄어쓰기를 규칙적으로 사용하지도 않을 뿐더러, 교착어이기 때문에 대부분의 단어에 조사 등이 붙어 어절 토큰화가 아닌 형태소 토큰화가 필요하다. 형태소는 그 자체로 뜻이 있는 자립 형태소와, 다른 형태소와 결합해야지만 사용할 수 있는 의존 형태소가 있는데 이 형태소들을 각각 분리하는 것이 형태소(morpheme) 토큰화이다. 또한 여러 뜻을 포함하는 단어의 의미를 파악하기 위해 품사 태깅(part-of-speech tagging)도 필요하다. 문장(sentence) 토큰화를 먼저 수행할 수도 있는데 이 경우 구두점이 단어의 일부분이 아닌 문장의 끝임을 어떻게 판단할 것인지, 이러한 규칙들이 필요하다.


이런 것들을 어떻게 만들어야 하나... 걱정할 필요 없다. 이미 공개된 자연어 처리 패키지들을 잘 활용하기만 하면 된다.


◈ 영문 자연어 처리: NLTK(Natural Language Toolkit)


◈ 한글 자연어 처리: KoNLPy

- 한글 형태소 분석기: Mecab, Komoran, Hannanum, Kkma, Okt(Open Korean Text, 예전이름 Twitter),... 


◈ 한글 문장 분리기: kss(Korean Sentence Splitter)



NLTK 를 사용한 영어권 언어 토큰화


import nltk
from nltk.tokenize import word_tokenize
from nltk.tokenize import sent_tokenize
from nltk.tag import pos_tag
 
# corpus download
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')
 
text = "At eight o'clock on Thursday morning. Arthur didn't feel very good."
tokens = word_tokenize(text)
 
print(tokens) # 단어 토큰화
print(pos_tag(tokens)) # 품사 태깅
print(sent_tokenize(text)) # 문장 토큰화
 

""" 결과
['At', 'eight', "o'clock", 'on', 'Thursday', 'morning', '.', 'Arthur', 'did', "n't", 'feel', 'very', 'good', '.']
[('At', 'IN'), ('eight', 'CD'), ("o'clock", 'NN'), ('on', 'IN'), ('Thursday', 'NNP'), ('morning', 'NN'), ('.', '.'), ('Arthur', 'NNP'), ('did', 'VBD'), ("n't", 'RB'), ('feel', 'VB'), ('very', 'RB'), ('good', 'JJ'), ('.', '.')]
["At eight o'clock on Thursday morning.", "Arthur didn't feel very good."]
"""
cs


NLTK 를 사용하여 단어/문장 토큰화 및 토큰화 데이터에 품사를 태깅하였다. NLTK 는 Penn Treebank Tokenization 의 규칙에 따라 토큰화 하고, Penn Treebank POS Tags 라는 품사 태깅 기준을 사용한다.(패키지마다 기준이나 규칙은 다를 수 있다.)



KoNLPy 를 사용한 한글 토큰화


KoNLPy 을 사용하기 위해서는 JDK 1.7 이상이 설치되어 있어야 한다. 그리고 pip 로 konlpy 를 설치하고, JAVA 와 Python 을 연결해 주는 JPype 를 본인의 버전에 맞게 다운로드 하여 설치한다. (JPype 다운로드 - https://www.lfd.uci.edu/~gohlke/pythonlibs/#jpype)


> pip install konlpy
> pip install JPype1-1.1.2-cp38-cp38-win_amd64.whl
cs


from konlpy.tag import *
 
okt = Okt()
hannanum = Hannanum()
kkma = Kkma()
komoran = Komoran()
mecab = Mecab()
 
text = "아버지가방에들어가신다"
 
print(okt.morphs(text))
print(okt.pos(text))
print('-----')
print(mecab.morphs(text))
print(mecab.pos(text))
 
 
""" 결과
['아버지', '가방', '에', '들어가신다']
[('아버지', 'Noun'), ('가방', 'Noun'), ('에', 'Josa'), ('들어가신다', 'Verb')]
-----
['아버지', '가', '방', '에', '들어가', '신다']
[('아버지', 'NNG'), ('가', 'JKS'), ('방', 'NNG'), ('에', 'JKB'), ('들어가', 'VV'), ('신다', 'EP+EC')]
"""
cs


한글 토큰화 방법도 형태소를 분석하고 품사를 태깅하는 순서로 토큰화 과정은 대부분 비슷하다. 형태소 분석기에 따라 토큰화된 결과가 조금씩 다르며, 상황별 문장에 따라 각각의 장단점이 있다. 저중에는 아버지가 제대로 방에 들어가는 분석기도 mecab 뿐이고 성능도 가장 낫다고는 하지만... 윈도우 플랫폼은 지원하지 않아 추가 설치 및 설정 등이 필요하다.




WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,