[Text Mining] Text Preprocessing, Text Transformation

Joo·2024년 7월 31일

Data Analytics 101

목록 보기
14/15
post-thumbnail

🐼 목 차 🐼
1. 텍스트 마이닝의 프로세스
2. 텍스트 마이닝의 데이터 전처리
3. 텍스트 변환 (수치화)
4. (추가) 그 외의 프로세스



1. 텍스트 마이닝의 프로세스

데이터 수집 → 데이터 전처리 → 텍스트 변환 → EDA → 모델링 → 평가 및 개선 → 배포 및 모니터링

보다시피 다른 분석과 유사한 프로세스를 갖고있지만,
다루는 데이터의 형태가 다르다보니 데이터 전처리 및 데이터(텍스트) 변환 과정에서 다른 분석 기법이 적용된다.

2. 텍스트 마이닝의 데이터 전처리

컴퓨터가 텍스트를 분석할 수 있도록 적합한 형태로 정제하는 단계 (NLTK, SpaCy, KoNLPy 등 사용 가능)
아래에 정리되어있는 텍스트 정제, 토큰화, 품사 태깅, 어간 및 표제어 추출뿐만 아니라,
오탈자 제거 및 띄어쓰기 교정, 정규화(표현 방법이 다른 단어들을 통합시켜 같은 단어로 만들기) 또한 전처리의 한 과정임

✔️ 텍스트 정제(Cleaning) 및 불용어 제거(Removla of Stopwords)
: 불필요한 문자(반복되는 문자-ㅋ, ㅎ, 이모지, 기호, 숫자 등)를 제거하고 대소문자 통일 (정규표현식으로도 가능)

import re, emoji

# 정규표현식을 사용해 텍스트 정제
p = re.compile("[ㅋㅎㄷㅇ~!?.\\-ㅡ0-9a-z]+") # ㅋ, ㅎ, ㄷ, ㅇ, ~, 등 모두 삭제

cleaned_text = [] # 전처리된 전체 텍스트를 담을 리스트

for doc in sentence['문장']:
    temp = [] # 하나의 문장을 전처리한 후 저장할 리스트
    for token in doc.split(" "): # 한 문장을 띄어쓰기 기준으로 토큰화
    							 # NLTK의 word_tokenize()를 통해서도 가능함
        if len(token) < 2: # 글자수가 하나인 단어는 무시
            continue 

        if p.search(token): # 정규표현식 패턴을 포함하는 단어는 무시
            continue

        temp.append(token) # 그 외의 단어 토큰은 임시 리스트에 추가

    cleaned_text.append(" ".join(temp)) # 정제된 토큰들을 다시 문장으로 결합하여 결과 리스트에 추가
  
# 이모지 삭제
cleaned_text2 = emoji.replace_emoji(tc) for tc in cleaned_text]

✔️ 토큰화(Tokenization)
: 주어진 문서(말뭉치, corpus)에서 토큰이라 불리는 단위로 나누는 작업(공백, 형태소, 명사 기준)
문서를 단어(NLTK), 형태소 단위로 분할 (Khaiii, KoNLPy, Mecab, Hannanum 등)
Sentiment Analysis의 경우, 감성을 나타내는 품사가 동사, 형용사에 가깝기 때문에 형태소 분석기를 사용해 동사, 형용사 위주로 추출함
형태소 분할은 띄어쓰기에 영향을 많이 받음

🟢 단어 단위 토큰화

from nltk import word_tokenize
import nltk
nltk.download('punkt')

sentence = "텍스트 마이닝 작업 수행 시, 아래와 같은 NLP 기술이 적용될 수 있음"
words = word_tokenize(sentence) // NLTK의 word_tokenize()는 단순 split(" ")와 달리 구두점(., ?, !)과 같은 특수 문자 또한 따로 토큰화함
print(words) 
# ['텍스트', '마이닝', '작업', '수행', '시', ',', '아래와', '같은', 'NLP', '기술이', '적용될', '수', '있음']

🟢 형태소 단위 토큰화

from konlpy.tag import Kkma, Okt, Mecab

kkma = kkma()
okt = Okt()
mecab = Mecab()

sentence = "형태소 분석은 자연어 처리의 중요한 작업이다."

kkma_morphs = kkma.morphs(sentence)
okt_morphs = okt.morphs(sentence)
mecab_morphs = mecab.morphs(sentence)

print("Kkma 형태소 분석:", kkma_morphs)
print("Okt 형태소 분석:", okt_morphs)
print("Mecab 형태소 분석:", mecab_morphs)

# Kkma 형태소 분석: ['형태소', '분석', '은', '자연어', '처리', '의', '중요', '하', 'ㄴ', '작업', '이', '다']
# Okt 형태소 분석: ['형태소', '분석', '은', '자연어', '처리', '의', '중요한', '작업', '이다']
# Mecab 형태소 분석: ['형태소', '분석', '은', '자연어', '처리', '의', '중요', '한', '작업', '이다']

# .tagset을 통해 나눈 형태소도 확인 가능함!

※ KoNLPy 패키지 내에 다양한 형태소 분류기(Hannanum, Kkma, Komoran, Mecab, OpenKoreanText 등)가 포함되어 있음. 형태소 분석 뿐만 아니라 품사 태깅, 구문 분석 등도 가능함!
※ Okt - 트위터 기반 형태소 분류기라 신조어에 강함
※ Kkma - 속도가 좀 느리지만, 품사 태깅이 디테일하게 가능
※ Mecab - 처리 속도가 빠름 (일본어도 가능)


✔️ 품사 태깅(Part-of-Speech Tagging)
: 텍스트 데이터의 각 단어에 대해 해당 언어 품사를 식별하고 태그를 부여하는 작업
규칙 기반 접근법, 통계적 접근법(대규모 태그가 달린 corpus에서 학습한 통계 모델), ML 접근법(HMM, CRF, SVM 등의 지도학습 알고리즘), DL 접근법(LSTM, BiLSTM, Transformer)이 있음

okt_pos = okt.pos(sentence)
mecab_pos = mecab.pos(sentence)
kkma_pos = kkma.pos(sentence)

print("Okt 품사 태깅:", okt_pos)
print("Mecab 품사 태깅:", mecab_pos)
print("Kkma 품사 태깅:", kkma_pos)

# Okt 품사 태깅: [('형태소', 'Noun'), ('분석', 'Noun'), ('은', 'Josa'), ('자연어', 'Noun'), ('처리', 'Noun'), ('의', 'Josa'), ('중요한', 'Adjective'), ('작업', 'Noun'), ('이다', 'Josa'), ('.', 'Punctuation')]
# Mecab 품사 태깅: [('형태소', 'NNG'), ('분석', 'NNG'), ('은', 'JX'), ('자연어', 'NNG'), ('처리', 'NNG'), ('의', 'JKG'), ('중요', 'NNG'), ('한', 'XSA+ETM'), ('작업', 'NNG'), ('이다', 'VCP+EC'), ('.', 'SF')]
# Kkma 품사 태깅: [('형태소', 'NNG'), ('분석', 'NNG'), ('은', 'JX'), ('자연어', 'NNG'), ('처리', 'NNG'), ('의', 'JKG'), ('중요', 'NNG'), ('하', 'XSV'), ('ㄴ', 'ETD'), ('작업', 'NNG'), ('이', 'VCP'), ('다', 'EFN'), ('.', 'SF')]

✔️ 어간 추출(Stemming) 및 표제어 추출(Lemmatization)
: 단어의 기본 형태로 변환 (영어는 PorterStemmer, SnowballStemmer, 한국어에는 KoNLPy 사용 가능)
• 어간 추출 - 접미사, 접두사, 어미 제거
• 표제어 추출 - 추출한 어간을 문법적, 의미적으로 올바른 형태로 변환

🟢 영어 어간, 표제어 추출

import nltk
nltk.download('wordnet')
nltk.download('omw-1.4')

from nltk.stem import PorterStemmer
from nltk.stem import SnowballStemmer
from nltk.stem import WordNetLemmatizer
from nltk.corpus import wordnet

sentence = "running runs runner easily fairly"

porter = PorterStemmer()
porter_stems = [porter.stem(word) for word in sentence.split()]
print("Porter Stemmer:", porter_stems)
# Porter Stemmer: ['run', 'run', 'runner', 'easili', 'fairli'] <- 어간 추출

snowball = SnowballStemmer('english')
snowball_stems = [snowball.stem(word) for word in sentence.split()]
print("Snowball Stemmer:", snowball_stems)
# Snowball Stemmer: ['run', 'run', 'runner', 'easili', 'fair'] <- 어간 추출

lemmatizer = WordNetLemmatizer()
lemmatized_words = [lemmatizer.lemmatize(word, pos=wordnet.VERB) for word in sentence.split()]
print("Lemmatized Words:", lemmatized_words)
# Lemmatized Words: ['run', 'run', 'run', 'run', 'fair'] <- 표제어 추출

🟢 한국어 어간, 표제어 추출

from konlpy.tag import Okt

okt = Okt()
sentence = "달리고 있는 중입니다. 그는 달리고 있었습니다."
stems = [okt.morphs(word, stem=True) for word in sentence.split()]
print(stems)
# ["달리고", "있는", "중입니다.", "그는", "달리고", "있었습니다."]

3. 텍스트 변환 (수치화)

컴퓨터가 텍스트 데이터를 인식할 수 있도록 숫자 벡터로 변환하는 단계 (단어 벡터화)

✔️ Bag of Words(BoW)
: 문서를 단어 출현 빈도로 표현하는 방법 (희소 행렬 반환)

• 단어 순서를 무시하므로, 문맥이 반영되지 않음
• 문서는 전체 단어 사전 크기만큼의 벡터로 표현되고, 벡터의 각 요소는 해당 단어의 출현 빈도를 나타냄
• 그러나 대부분의 단어들은 특정 문서(문장)에 등장하지 않기 때문에, 벡터의 많은 요소가 0이 됨 → 고차원의 희소 행렬
• 특정 단어들이 자주 등장하면(불용어) 벡터의 특정 요소가 자주 1 이상의 값을 가짐
데이터 부피가 매우 커질 수 있으며, 상대적으로 중요한 단어의 중요도가 낮아질 수 있음
• 문서(문장) 수가 많아질수록 고유 단어의 수가 증가해 사전 크기도 커짐
데이터 부피가 매우 커질 수 있음
• 전체 문장은 많은데 각 문장의 길이가 짧을 경우, 각 문서 벡터에서 대부분의 요소가 0됨 (전체 데이터에서 의미있는 정보가 적다는 뜻!)
• 따라서, BoW는 간단한 자연어 처리 작업에서 사용됨

from sklearn.feature_extraction.text import CountVectorizer

corpus = [
    "나는 자연어 처리를 공부합니다",
    "자연어 처리는 재미있습니다",
    "나는 머신러닝도 공부합니다"
]
vectorizer = CountVectorizer() # 고유 단어 리스트 생성하는 메소드
X = vectorizer.fit_transform(corpus) # BoW 방식으로 변환
print(X.toarray()) # 각 문장에 단어 사전의 단어가 몇 번씩 나왔는가
print(vectorizer.get_feature_names_out()) # 단어 사전의 단어 목록
# 
# [[0 1 0 1 1 1 0]  # 나는 자연어 처리를 공부합니다
#  [0 0 1 0 1 0 1]  # 자연어 처리는 재미있습니다
#  [1 1 0 0 1 1 0]] # 나는 머신러닝도 공부합니다
# 
# ['공부합니다' '나는' '재미있습니다' '처리' '자연어' '머신러닝도' '처리는']

✔️ TF-IDF(Term Frequency-Inverse Document Frequency)
: BoW 대안으로, 단어 빈도와 역문서 빈도 결합해 단어의 중요도를 벡터화 (희소 행렬 반환하지만 toarray() 메소드를 통해 밀집 행렬로 나타냄)

• 단순히 단어의 빈도(TF)와 희소성(IDF)을 통해 단어의 중요도를 계산하는거라서 단어의 순서나 문맥 관계를 파악할 수는 없음

TF-IDF : 특정 단어가 문서에서 얼마나 중요한가 (TF와 IDF의 곱으로 계산됨)

TF-IDF(t,d)=TF(t,d)×IDF(t)=해당 단어 t의 문서 d에서의 빈도문서 d의 총 단어 수×log(NDF(t)+1)\text{TF-IDF}(t, d) = \text{TF}(t, d) \times \text{IDF}(t) = \frac{\text{해당 단어 } t \text{의 문서 } d \text{에서의 빈도}}{\text{문서 } d \text{의 총 단어 수}} \times \log \left( \frac{N}{\text{DF}(t) + 1} \right)

TF : 특정 문서에서의 단어 빈도

TF(t,d)=해당 단어 t의 문서 d에서의 빈도문서 d의 총 단어 수\text{TF}(t, d) = \frac{\text{해당 단어 } t \text{의 문서 } d \text{에서의 빈도}}{\text{문서 } d \text{의 총 단어 수}}

DF : 특정 단어가 전체 문서에서 등장하는 문서의 수

DF(t)=단어 t가 등장한 문서의 수전체 문서 수\text{DF}(t) = \frac{\text{단어 } t \text{가 등장한 문서의 수}}{\text{전체 문서 수}}

IDF : 단어의 희소성을 반영하며, 전체 문서 수를 DF로 나눈 값을 로그변환한 것 (DF의 역수에 비례함)
→ 특정 단어가 전체 문서에서 얼마나 자주 등장하는지를 역으로 나타낸 것
+1 = 단어가 어떠한 문서에서도 등장하지 않을 경우(DF=0)에 대비하기 위한 것
→ 단어가 자주 등장할수록 IDF 값은 감소함

IDF(t)=log(전체 문서 수DF(t)+1)\text{IDF}(t) = \log \left( \frac{\text{전체 문서 수}}{\text{DF}(t) + 1} \right)

• TF-IDF 값의 해석
높은 TF-IDF 값 : 특정 단어가 해당 문서에서 많이 등장하고(높은 TF), 전체 문서에서 드물게 등장하는(높은 IDF) 경우 → 해당 문서를 다른 문서와 구별짓는 특징
낮은 TF-IDF 값 : 특정 단어가 해당 문서에서 적게 등장하고(낮은 TF), 전체 문서에서 자주 등장하는(낮은 IDF) 경우 → 정보량이 저거나, 모든 문서에서 흔히 등장하는 단어(예, 불용어)

from sklearn.feature_extraction.text import TfidfVectorizer

corpus = [
    "나는 자연어 처리를 공부합니다",
    "자연어 처리는 재미있습니다",
    "나는 머신러닝도 공부합니다"
]

vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus)

print(X.toarray()) # 희소 행렬을 밀집 행렬로 반환
print(vectorizer.get_feature_names_out())

# TF-IDF 결과를 밀집 행렬로 나타낸 것
# [[0.         0.47435029 0.         0.47435029 0.33517574 0.         0.47435029]
#  [0.         0.         0.69010824 0.         0.48546061 0.         0.48546061]
#  [0.60534851 0.60534851 0.         0.         0.         0.60534851 0.        ]]

['공부합니다', '나는', '재미있습니다', '처리', '자연어', '머신러닝도', '처리는']

✔️ 단어 임베딩(Word Embedding)
: 단어를 고정된 크기의 밀집 벡터로 변환해, 단어의 의미적 유사성을 반영하는 방법 (밀집 벡터 반환)
• 단어의 의미적 유사성을 반영해, 의미적으로 유사한 단어들이 벡터 공간에서 가깝게 위치함
• 따라서 문서의 문맥이 반영됨
• 대규모 말뭉치를 사용해 사전훈련된 임베딩을 사용하거나, 새로운 데이터셋에 대해 직접 임베딩 수행 가능
• Word2Vec, GloVe, FastText, BERT 등으로 구현 가능

from gensim.models import Word2Vec
from konlpy.tag import Okt
import pprint

sentences = [
    "고양이는 귀엽다",
    "강아지는 귀엽다",
    "고양이와 강아지는 친구다",
    "나는 고양이를 좋아한다",
    "나는 강아지를 좋아한다"
]

okt = Okt() # 형태소 분석기 생성

tokenized_sentences = [okt.morphs(sentence) for sentence in sentences] # 형태소 단위로 문장 분할

model = Word2Vec(sentences=tokenized_sentences, vector_size=10, window=2, min_count=1, workers=4) # Word2Vec 모델 학습

print("고양이 벡터:") # 단어 벡터 확인
pprint.pprint(model.wv['고양이'])

print("\n'고양이'와 유사한 단어:") # 가장 유사한 단어 확인
pprint.pprint(model.wv.most_similar('고양이'))
pprint.pprint(model.wv.similarity('고양이', '강아지'))
pprint.pprint(model.wv.most_similar('고양이')) # 벡터 간 코사인 유사도를 계산해 단어간 유사성 측정 (1에 가까울수록 좋음)
#
# 고양이 벡터: <- 10차원 벡터 공간에 표현한 고양이의 의미
# 				각 차원은 고양이와 관련된 다양한 의미적 정보를 캡슐화하고 있음
#				벡터의 각 요소(차원)는 각 특성들의 강도를 나타냄
# array([ 0.00849383,  0.00452056, -0.00309813,  0.00397072,  0.00871472,
#        -0.0099122 ,  0.00344669, -0.00581288, -0.00415291, -0.00663748],
#       dtype=float32)
# 
# '고양이'와 유사한 단어:
# [('강아지', 0.30124518275260925),
#  ('친구', 0.2501252293586731),
#  ('좋아한다', 0.20316757261753082),
#  ('귀엽다', 0.182562917470932)]

추가로, 특정 단어의 의미를 변환시켜 그 결과값과 비슷한 단어를 반환하는 메소드도 있음

king_vector = model.wv['왕'] - model.wv['남자'] + model.wv['여자']
print(model.wv.similar_by_vector(king_vector)) # "여왕"과 유사한 단어 반환할 것



4. (추가) 그 외의 프로세스

EDA

  • 단어 빈도 분석, N그램 분석, 워드클라우드, 상관 분석, 트렌드 분석 등

모델링

  • Classification, Clustering, Syntax Parsing, Topic Modeling, Sentiment Analysis
    • Naive Bayes, SVM, LDA(Latent Dirichlet Allocation), LSTM, BERT 등


profile
적당히 공부한 거 정리하는 곳

0개의 댓글