Home NLP/TA 기초
Post
Cancel

NLP/TA 기초

텍스트 분석

NLTK를 이용한 영문 텍스트 분석

NLP? 텍스트 분석?

NLP(National Language Processing)와 텍스트 분석(TA, Text Analytics)을 구분하는건 머신러닝이 보편화되면서 의미가 없어짐
NLP는 기계가 인간의 언어를 이해하고 해석하는데 중점을 두고, TA는 텍스트 마이닝(Text Mining)이라고도 불리며 비정형 텍스트에서 의미 있는 정보를 추출하는 것에 더 중점을 두고 있음

  • 텍스트 분류(Text Classification)
  • 감성 분석(Sentiment Analysis)
  • 텍스트 요약(Summarization)
  • 텍스트 군집화와 유사도 측정

텍스트 분석 이해

TA는 비정형 데이터인 텍스트를 분석하는 것
지금까지 머신러닝은 주어진 정형 데이터 기반에서 모델을 수립하고 예측을 수행했었음
-> 숫자형의 피처 기반 데이터만 입력 받을 수 있음 -> 비정형인 텍스트에 머신러닝을 적용하기 위해서는 비정형 텍스트 데이터를 어떻게 피처 형태로 추출하고 의미있는 값을 부여하는지가 중요한 요소

단어 빈도수와 같은 숫자 값을 부여 -> 단어의 조합인 벡터값으로 표현 가능 -> 피처 벡터화(Feature Vectorization) 또는 피처 추출(Feature Extraction)

대표적인 피처 벡터화 변환 방법

  • BOW(Bag of Words)
  • Word2Vec

텍스트 분석 수행 프로세스

  1. 텍스트 사전 준비 작업(텍스트 전처리)
    • 특수문자 삭제 등의 클렌징
    • 대소문자 변경
    • 단어 등의 토큰화 작업
    • 의미 없는 단어(stop word) 제거
    • 어근 추출(Stemming/Lemmatization) 등의 정규화 작업
  2. 피처 벡터화/추출
    • 벡터값 할당
    • BOW
      • count기반의 TF-IDF가 대표적
    • Word2Vec
  3. ML 모델 수립 및 학습/예측/평가

텍스트 사전 준비 작업(텍스트 전처리) - 텍스트 정규화

  1. 클렌징(Cleansing)
  2. 토큰화(Tokenization)
  3. 필터링/stop word/철자 수정
  4. Stemming
  5. Lemmatization

클렌징

텍스트에서 분석에 방해되는 불필요한 문자, 기호 등을 사전에 제거하는 작업

텍스트 토큰화

문장 토큰화(Sentence Tokenization)

문장의 마침표(.), 개행문자(\n) 등 문장의 마지막을 뜻하는 기호에 따라 분리하는 것이 일반적

1
2
3
4
5
6
7
8
9
10
from nltk import sent_tokenize
import nltk
nltk.download("punkt") # 마침표, 개행 문자들의 데이터 세트 다운

text_sample = "The Matrix is everywhere its all around us, here even in this room. \
            You can see it out your window or on your television. \
            You feel it when you go to work, or go to church or pay your taxes."
sentences = sent_tokenize(text=text_sample)
print(type(sentences), len(sentences))
print(sentences)
1
2
<class 'list'> 3
['The Matrix is everywhere its all around us, here even in this room.', 'You can see it out your window or on your television.', 'You feel it when you go to work, or go to church or pay your taxes.']
단어 토큰화(Word Tokenization)

문장을 단어로 토큰화, 기본적으로 공백, 콤마(,), 마침표(.), 개행문자 등으로 단어를 분리하지만, 정규표현식을 이용해 다양한 유형으로 토큰화를 수행할 수 있음
단어의 순서가 중요하지 않은 경우, 문장 토큰화를 사용하지 않고 단어 토큰화만 사용해도 충분
-> 일반적으로 문장 토큰화는 각 문장이 가지는 시맨틱적인 의미가 중요한 요소로 사용될 때 사용

1
2
3
4
5
6
from nltk import word_tokenize

sentence = "The Matrix is everywhere its all around us, here even in this room."
words = word_tokenize(sentence)
print(type(words), len(words))
print(words)
1
2
<class 'list'> 15
['The', 'Matrix', 'is', 'everywhere', 'its', 'all', 'around', 'us', ',', 'here', 'even', 'in', 'this', 'room', '.']

sent_tokenizword_tokenize를 조합해 문서에 대해서 모든 단어를 토큰화

1
2
3
4
5
6
def tokenize_text(text):
    # 문장별로 분리 토큰
    sentences = sent_tokenize(text)
    # 분리된 문장별 단어 토큰화
    word_tokens = [word_tokenize(sentence) for sentence in sentences]
    return word_tokens
1
2
3
word_tokens = tokenize_text(text_sample)
print(type(word_tokens), len(word_tokens))
print(word_tokens)
1
2
<class 'list'> 3
[['The', 'Matrix', 'is', 'everywhere', 'its', 'all', 'around', 'us', ',', 'here', 'even', 'in', 'this', 'room', '.'], ['You', 'can', 'see', 'it', 'out', 'your', 'window', 'or', 'on', 'your', 'television', '.'], ['You', 'feel', 'it', 'when', 'you', 'go', 'to', 'work', ',', 'or', 'go', 'to', 'church', 'or', 'pay', 'your', 'taxes', '.']]

문장을 단어별로 하나씩 토큰화 할 경우 문맥적인 의미는 무시될 수밖에 없음
-> 이런 문제를 해결하기 위해 도입된 개념이 n-gram

n-gram

연속된 n개의 단어를 하나의 토큰화 단위로 분리해 내는 것
n개 단어 크기 윈도우를 만들어 문장의 처음부터 오른쪽으로 움직이면서 토큰화 단위로 분리해 내는 것

스톱 워드 제거

스톱 워드(stop word, 불용어)는 분석에 큰 의미가 없는 단어를 지칭
문법적인 특성으로 인해 빈번하게 텍스트에 나타나지만, 사전에 제거하지 않으면 빈번함으로 인해 중요 단어로 인지할 가능성이 있음
언어별로 스톱 워드가 목록화돼 있음

1
2
# NLTK의 stopwords 목록
nltk.download("stopwords")
1
2
3
4
5
[nltk_data] Downloading package stopwords to
[nltk_data]     C:\Users\spec3\AppData\Roaming\nltk_data...
[nltk_data]   Package stopwords is already up-to-date!

True
1
2
print("영어 stop words 개수 : ", len(nltk.corpus.stopwords.words("english")))
print(nltk.corpus.stopwords.words("english")[:20])
1
2
영어 stop words 개수 :  179
['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're", "you've", "you'll", "you'd", 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his']

word_tokens에서 스톱 워드를 제거

1
2
3
4
5
6
7
8
9
10
11
stopwords = nltk.corpus.stopwords.words("english")
all_tokens = []

for sentence in word_tokens:
    filtered_words = []
    for word in sentence:
        word = word.lower()
        if word not in stopwords:
            filtered_words.append(word)
    all_tokens.append(filtered_words)
print(all_tokens)
1
[['matrix', 'everywhere', 'around', 'us', ',', 'even', 'room', '.'], ['see', 'window', 'television', '.'], ['feel', 'go', 'work', ',', 'go', 'church', 'pay', 'taxes', '.']]

Stemming과 Lemmatization (어간 추출과 표제어 추출)

많은 언어에서 문법적인 요소에 따라 단어가 다양하게 변함
-> Stemming과 Lemmatization은 문법적 또는 의미적으로 변환하는 단어의 원형알 찾는 것

Stemming과 Lemmatization 모두 원형 단어를 찾는다는 목적은 유사하지만, Lemmatization이 Stemming보다 정교하며 의미론적인 기반에서 단어의 원형을 찾음

  • Stemming: 원형 단어로 변환 시 일반적인 방법을 적용하거나 더 단순화된 방법을 적용해, 일부 철자가 훼손된 어근 단어를 추출하는 경향
    • Porter
    • Lancaster
    • Snowball Stemmer
  • Lemmatization: 품사와 같은 문법적인 요소와 더 의미적인 부분을 감안해 정확한 철자로 된 어근 단어를 찾음
    • WordNetLemmatizer
1
2
3
4
5
6
7
8
# NLTK의 LancasterStemmer
from nltk.stem import LancasterStemmer
stemmer = LancasterStemmer()

print(stemmer.stem("working"), stemmer.stem("wokrs"), stemmer.stem("worked"))
print(stemmer.stem("amusing"), stemmer.stem("amuses"), stemmer.stem("amused"))
print(stemmer.stem("happier"), stemmer.stem("happiest"))
print(stemmer.stem("fancier"), stemmer.stem("fanciest"))
1
2
3
4
work wokr work
amus amus amus
happy happiest
fant fanciest

원형 단어에서 철자가 다른 어근 단어로 인식하는 경우가 있음

1
2
3
4
5
6
7
8
9
10
# NLTK의 WordNetLemmatizer
# 동사는 v, 형용사는 a와 같이 인자를 함께 넣어줘야함
from nltk.stem import WordNetLemmatizer
nltk.download("wordnet")
nltk.download('omw-1.4')

lemma = WordNetLemmatizer()
print(lemma.lemmatize("amusing", "v"), lemma.lemmatize("amuses", "v"), lemma.lemmatize("amused", "v"))
print(lemma.lemmatize("happier", "a"), lemma.lemmatize("happiest", "a"))
print(lemma.lemmatize("fancier", "a"), lemma.lemmatize("fanciest", "a"))
1
2
3
amuse amuse amuse
happy happy
fancy fancy

Stemmer보다 정확하게 원형 단어를 추출

Bag of Words - BOW

BOW 모델은 문서가 갖는 모든 단어를 문맥이나 순서를 무시하고 일괄적으로 단어에 대해 빈도 값을 부여해 피처 값을 추출하는 모델

쉽고 빠른 구축이 장점이지만, 문맥 의미(Semantic Context) 반영 부족, 희소 행렬 문제가 있음

BOW 피처 벡터화

텍스트는 특정 의미를 갖는 숫자형 값인 벡터 값으로 변환되어야 함
-> 피처 벡터화

  • 텍스트를 단어로 추출해 피처를 할당
  • 각 단어의 발생 빈도와 같은 값을 피처에 부여해 각 문서를 이 단어 피처의 발생 빈도 값으로 구성된 벡터로 만드는 기법

BOW 피처 벡터화 방식

  1. 카운트 기반의 벡터화
  2. TF-IDF(Term Frequency - Inverse Document Frequency) 기반의 벡터화

카운트 기반의 벡터화에서는 카운트 값이 높을수록 중요한 단어로 인식
-> 문서의 특징을 나타내기보다는 언어의 특성상 문장에서 자주 사용될 수밖에 없는 단어까지 높은 값을 부여 받음
-> TF-IDF로 보완

TF-IDF는 개별 문서에서 자주 나타나는 단어에 높은 가중치를 주되, 모든 문서에서 전반적으로 자주 나타나는 단어에 대해서는 패널티를 주는 방식으로 값을 부여
-> 어떤 문자서에 특정 단어가 자주 나타나면 그 단어는 해당 문서를 특징짓는 중요한 단어일 가능성이 높음
-> 하지만 그 단어가, 다른 문서에도 자주 나타나는 단어라면 해당 단어는 언어 특성상 범용적으로 자주 사용되는 단어일 가능성이 높음

$TFIDF_i = TF_i * log{N \over DF_i}$

  • $TF_i$: 개별 문서에서의 단어 i 빈도
  • $DF_i$: 단어 i를 가지고 있는 문서 개수
  • N: 전체 문서 개수

CountVectorizer 와 TfidfVectorizer(카운트 기반의 벡터화와 TF-IDF 기반 벡터화)

  1. 모든 문자를 소문자로 변경하는 등의 전처리 작업
  2. n_gram_range를 반영해 각 단어를 토큰화
  3. 텍스트 정규화

BOW 벡터화를 위한 휘소 행렬

텍스트를 피처 단위로 벡터화해 변환하고 CSR 형태의 희소 행렬을 반환
모든 문서에 있는 단어를 추출해 이를 피처로 벡터화하는 방법은 필연적으로 많은 피처 칼럼을 만들 수 밖에 없음
-> 모든 문서에 있는 단어를 제거하고 피처로 만들면 일반적으로 수만 개에서 수십만개의 단어가 만들어짐
-> 대규모 행렬이 생선되더라고 각 문서가 갖는 단어의 수는 제한적이기에 대부분 0의 값을 갖게 됨 -> 0이 차지하는 행렬을 희소 행렬이라 함

희소 행렬을 물리적으로 적은 메모리 공간을 차지할 수 있도록 변화해야 함
-> COO 형식과 CSR 형식

COO 형식

0이 아닌 데이터만 별도의 데이터 배열에 저장하고, 그 데이터가 가리키는 행과 열의 위치를 별도의 배열로 저장하는 방식

1
2
3
4
5
import numpy as np

dense = np.array([[3, 0, 1], [0, 2, 0]])

dense
1
2
array([[3, 0, 1],
       [0, 2, 0]])
1
2
3
4
5
6
7
8
9
10
from scipy import sparse

# 0이 아닌 데이터 추출
data = np.array([3, 1, 2])

# 행 위치와 열 위치가 각각 배열로 생성
row_pos = np.array([0, 0, 1])
col_pos = np.array([0, 2, 1])

sparse_coo = sparse.coo_matrix((data, (row_pos, col_pos)))
1
sparse_coo.toarray()
1
2
array([[3, 0, 1],
       [0, 2, 0]])
CSR 형식 (Compressed Sparse Row)

COO 형식이 행과 열의 위치를 나타내기 위해 반복적인 위치 데이터를 사용해야 하는 문제점을 해결한 방식

1
2
3
4
5
6
7
# COO 형식의 문제점 확인
[[0, 0, 1, 0, 0, 5],
 [1, 4, 0, 3, 2, 5],
 [0, 6, 0, 3, 0, 0],
 [2, 0, 0, 0, 0, 0],
 [0, 0, 0, 7, 0, 8],
 [1, 0, 0, 0, 0, 0]]
1
2
3
4
5
6
[[0, 0, 1, 0, 0, 5],
 [1, 4, 0, 3, 2, 5],
 [0, 6, 0, 3, 0, 0],
 [2, 0, 0, 0, 0, 0],
 [0, 0, 0, 7, 0, 8],
 [1, 0, 0, 0, 0, 0]]

0이 아닌 데이터 값 배열 = [1, 5, 1, 4, 3, 2, 5, 6, 3, 2, 7, 8, 1]

0이 아닌 데이터 값의 행과 열 위치
(0, 2), (0, 5)
(1, 0), (1, 1), (1, 3), (1, 4), (1, 5)
(2, 1), (2, 3)
(3, 0)
(4, 3), (4, 5)
(5, 0)

행 위치 배열 = [0, 0, 1, 1, 1, 1, 1, 2, 2, 3, 4, 4, 5]
열 위치 배열 = [2, 5, 0, 1, 3, 4, 5, 1, 3, 0, 3, 5, 0]

순차적인 같은 값이 반족적으로 나타나는 모습을 볼 수 있음
-> 행 위치 배열이 0부터 순차적으로 증가하는 값으로 이뤄졌다는 특성을 고려하면 행 위치 배열의 고유한 값의 시작 위치만 표기하는 방법으로 반복을 제거 가능

행 위치 배열을 CSR로 변환하면, [0, 2, 7, 9, 10, 12]가 됨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from scipy import sparse

dense2 = [
    [0, 0, 1, 0, 0, 5],
    [1, 4, 0, 3, 2, 5],
    [0, 6, 0, 3, 0, 0],
    [2, 0, 0, 0, 0, 0],
    [0, 0, 0, 7, 0, 8],
    [1, 0, 0, 0, 0, 0]
    ]

# 0이 아닌 데이터 추출
data2 = np.array([1, 5, 1, 4, 3, 2, 5, 6, 3, 2, 7, 8, 1])
# 행 위치와 열 위치
row_pos = np.array([0, 0, 1, 1, 1, 1, 1, 2, 2, 3, 4, 4, 5])
col_pos = np.array([2, 5, 0, 1, 3, 4, 5, 1, 3, 0, 3, 5, 0])
# COO 형식
sparse_coo = sparse.coo_matrix((data2, (row_pos, col_pos)))
# 행 위치 배열의 고유한 값의 시작 위치 인덱스를 배열로 생성
row_pos_ind = np.array([0, 2, 7, 9, 10, 12, 13])
# CSR 형식
sparse_csr = sparse.csr_matrix((data2, col_pos, row_pos_ind))
1
sparse_coo.toarray()
1
2
3
4
5
6
array([[0, 0, 1, 0, 0, 5],
       [1, 4, 0, 3, 2, 5],
       [0, 6, 0, 3, 0, 0],
       [2, 0, 0, 0, 0, 0],
       [0, 0, 0, 7, 0, 8],
       [1, 0, 0, 0, 0, 0]])
1
sparse_csr.toarray()
1
2
3
4
5
6
array([[0, 0, 1, 0, 0, 5],
       [1, 4, 0, 3, 2, 5],
       [0, 6, 0, 3, 0, 0],
       [2, 0, 0, 0, 0, 0],
       [0, 0, 0, 7, 0, 8],
       [1, 0, 0, 0, 0, 0]])
This post is licensed under CC BY 4.0 by the author.

Yelp 데이터셋과 텍스트 유사도를 이용한 추천 시스템

22년 8월 1주차 주간 회고

Comments powered by Disqus.