https://github.com/nalinzip/ml_study/blob/main/ML_Week11.ipynb
NLP 는 머신이 인간의 언어를 이해하고 해석하는 데 더 중점을 두고 기술이 발전해 왔음
텍스트 마이닝 (Text Mining) 이라고도 불리는 텍스트 분석 (Text Analytics, 이하 TA) 은 비정형 텍스트에서 의미 있는 정보를 추출하는 것에 좀 더 중점을 두고 기술이 발전해 왔음
예를 들어 NLP 의 영역에는 언어를 해석하기 위한 기계 번역 , 자동으로 질문을 해석하고 답을 해주는 질의응답 시스템 등의 영역 등에서 텍스트 분석과 차별점이 있음
NLP 는 텍스트 분석을 향상하게 하는 기반 기술이라고 볼 수도 있음
NLP 기술이 발전함에 따라 텍스트 분석도 더욱 정교하게 발
전할 수 있었음
NLP 와 텍스트 분석의 발전 근간에는 머신러닝이 존재함
예전의 텍스트를 구성하는 언어적인 룰이나 업무의 룰에 따라 텍스트를 분석하는 룰 기반 시스템에서 머신러닝의 텍스트 데이터를 기반으로 모델을 학습하고 예측하는 기반으로 변경되면서 많은 기술적 발전이 가능해졌음
텍스트 분석은 머신러닝 , 언어 이해 , 통계 등을 활용해 모델을 수립하고 정보를 추출해 비즈니스 인텔리전스 (Business Intelligence)나 예측 분석 등의 분석 작업을 주로 수행함
머신러닝 기술에 힘입어 텍스트 분석은 크게 발전하고 있음
주로 다음과 같은 기술 영역에 집중
텍스트 분류 (Text Classification) / Text Categorization
감성 분석(Sentiment Analysis)
텍스트 요약 (Summarization)
텍스트 군집화 (Clustering) 와 유사도 측정
머신러닝 기반의 텍스트 분석 프로세스
텍스트 사전 준비작업 ( 텍스트 전처리 ): 텍스트를 피처로 만들기 전에 미리 클렌징 , 대 / 소문자 변경 , 특수문자 삭제피처 벡터화 / 추출: 사전 준비 작업으로 가공된 텍스트에서 피처를 추출하고 여기에 벡터 값을 할당합니다. 대표적ML 모델 수립 및 학습 / 예측 / 평가 : 피처 벡터화된 데이터 세트에 ML 모델을 적용해 학습 / 예측 및 평가를 수행합
NLTK(Natural Language Toolkit for Python)
Gensim
SpaCy
사이킷런은 머신러닝 중심의 라이브러리로, NLP에 특화된 기능은 제한적임.
하지만 텍스트 전처리 및 머신러닝 모델 입력을 위한 기본적인 텍스트 가공 및 피처 처리 기능은 제공함.
복잡한 NLP 작업이 필요한 경우에는 NLTK, Gensim, SpaCy 등의 NLP 전용 라이브러리와 함께 사용함.
문장 토큰화 (sentence tokenization) 는 문장의 마침표 (.), 개행문자 (\n) 등 문장의 마지막을 뜻하는 기호에 따라 분리하는 것이 일반적임
또한 정규 표현식에 따른 문장 토큰화도 가능
NTLK에서 일반적으로 많이 쓰이는 sent_tokenize 를 이용해 토큰화 수행
다음은 3 개의 문장으로 이루어진 텍스트 문서를 문장으로 각각 분리하는 예제
NLTK 의 경우 단어 사전과 같이참조가 필요한 데이터 세트의 경우 인터넷으로 다운로드받을 수 있음
다운로드가 완료된 경우에는 다시 다운로드하지 않지만 최초 다운로드가 필요하기 때문에 수행하려는 컴퓨터에 인터넷 연결이 돼 있는지 먼저 확인하고 다운로드를 수행하면 됨
nltk.download('punkt')는 마침표 , 개행 문자등의 데이터 세트를 다운로드함
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)
NTLK 에서 기본으로 제공하는 word_tokenize() 를 이용해 단어로 토큰화
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)
sent_tokenize 와 word_tokenize를 조합해 문서에 대해서 모든 단어를 토큰화해
문서를 먼저 문장으로 나누고 , 개별 문장을 다시 단어로 토큰화하는 tokenize_text() 함수를 생성
from nltk import word_tokenize, sent_tokenize
#여러개의 문장으로 된 입력 데이터를 문장별로 단어 토큰화 만드는 함수 생성
def tokenize_text(text):
# 문장별로 분리 토큰
sentences = sent_tokenize(text)
# 분리된 문장별 단어 토큰화
word_tokens = [word_tokenize(sentence) for sentence in sentences]
return word_tokens
#여러 문장들에 대해 문장별 단어 토큰화 수행.
word_tokens = tokenize_text(text_sample)
print(type(word_tokens),len(word_tokens))
print(word_tokens)
NTLK 의 스톱 워드에는 어떤 것이 있는지 확인
이를 위해 먼저 NLTK 의 stopwords 목록을 내려받습니다.
import nltk
nltk.download('stopwords')
다운로드가 완료되고 나면 NTLK 의 English의 경우 몇 개의 stopwords가 있는지 알아보고 그중 20개만 확인
print('영어 stop words 갯수:',len(nltk.corpus.stopwords.words('english')))
print(nltk.corpus.stopwords.words('english')[:20])
문장별로 단어를 토큰화해 생성된 word_tokens 리스트 (3 개의 문장별 단어 토큰화 값을 가지는 내포된 리스트로 구성 )에 대해서 stopwords를 필터링으로 제거해 분석을 위한 의미 있는 단어만 추출
import nltk
stopwords = nltk.corpus.stopwords.words('english')
all_tokens = []
# 위 예제의 3개의 문장별로 얻은 word_tokens list 에 대해 stop word 제거 Loop
for sentence in word_tokens:
filtered_words=[]
# 개별 문장별로 tokenize된 sentence list에 대해 stop word 제거 Loop
for word in sentence:
#소문자로 모두 변환합니다.
word = word.lower()
# tokenize 된 개별 word가 stop words 들의 단어에 포함되지 않으면 word_tokens에 추가
if word not in stopwords:
filtered_words.append(word)
all_tokens.append(filtered_words)
print(all_tokens)
is, this 와 같은 스톱 워드가 필터링을 통해 제거됐음
Stemming과 Lemmatization을 비교
from nltk.stem import LancasterStemmer
stemmer = LancasterStemmer()
print(stemmer.stem('working'),stemmer.stem('works'),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'))
WordNetLemmatizer를 이용해 Lemmatization을 수행
from nltk.stem import WordNetLemmatizer
import nltk
nltk.download('wordnet')
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'))
stemmer 보다 정확하게 원형 단어를 추출해줌
Bag of Words 모델은 문서가 가지는 모든 단어 (Words) 를 - 문맥이나 순서를 무시하고 일괄적으로 단어에 대해 빈도 값을 부여해 피처 값을 추출하는 모델입니다. 문서 내 모든 단어를 한꺼번에 봉투 (Bag) 안에 넣은 뒤에 흔들어서 섞는 것이다.
문장을 Bag of words 의 단어 수 (Word Count) 기반으
로 피처를 추출
My wife likes to watch baseball games and my daughter likes to watch baseball games too
My wife likes to play baseball
문장 1 과 문장 2 에 있는 모든 단어에서 중복을 제거하고 각 단어 (feature 또는 term) 를 칼럼 형태로 나열합니다. 그
러고 나서 각 단어에 고유의 인덱스를 다음과 같이 부여합니다.
'and':0, 'baseball': 1, 'daughter': 2, 'games': 3, 'likes':4, 'my':5, 'play': 6, 'to': 7, 'too': 8,'watch': 9, 'wife': 10
개별 문장에서 해당 단어가 나타나는 횟수 (Occurrence) 를 각 단어 ( 단어 인덱스 ) 에 기재합니다. 예를 들어 base-
bal 은 문장 1, 2 에서 총 2 번 나타나며 , daughter는 문장 1 에서만 1 번 나타납니다.
장점:
단점:
일반적으로 BOW 의 피처 벡터화는 두 가지 방식이 있습니다.
카운트 벡터화 (Count Vectorization):
TF-IDF 벡터화 (Term Frequency-Inverse Document Frequency):
주요 특징

CountVectorizer 주요 입력 파라미터
CountVectorizer 동작 순서
1. 모든 문자를 소문자 변환
2. n-gram 기반 단어 단위 토큰화 (기본: unigram)
3. 정규화 수행
4. 지정된 조건에 따라 단어 벡터화 수행
사이킷런에서 TF-IDF 벡터화는 Tfidfvectorizer 클래스를 이용합니다.
희소 행렬(Sparse Matrix)
피처 수 증가 요인
모든 문서의 고유 단어 → 수만~수십만 개 단어
n-gram 설정 증가 (예: (1,2), (1,3)) → 피처 수 급증
벡터화된 결과는 대부분 희소 행렬
ML 모델의 성능, 속도에 영향을 주므로 희소 행렬 특성 이해 필요
COO(Compressed Coordinate) 형식은 희소 행렬을 3개의 1차원 배열로 구성해 저장합니다.
구성 요소:
data: 0이 아닌 실제 값들
row: 각 값이 위치한 행 인덱스
col: 각 값이 위치한 열 인덱스
Python에서는 보통 SciPy의 sparse 모듈을 사용해 COO 형식의 희소 행렬로 변환합니다.
import numpy as np
dense = np.array( [ [ 3, 0, 1 ], [0, 2, 0 ] ] )
이 세 배열을 coo_matrix()의 입력 파라미터로 전달하여 희소 행렬 생성
from scipy import sparse
# 0 이 아닌 데이터 추출
data = np.array([3,1,2])
# 행 위치와 열 위치를 각각 array로 생성
row_pos = np.array([0,0,1])
col_pos = np.array([0,2,1])
# sparse 패키지의 coo_matrix를 이용하여 COO 형식으로 희소 행렬 생성
sparse_coo = sparse.coo_matrix((data, (row_pos,col_pos)))
sparse_coo COO 형식의 희소 행렬 객체 변수입니다. 이를 toarray() 메서드를 이용해 다시 밀집
형태의 행렬로 출력해 보겠습니다.
sparse_coo.toarray()
CSR(Compressed Sparse Row) 형식은 COO 형식이 행과 열의 위치를 나타내기 위해서 반복적인
위치 데이터를 사용해야 하는 문제점을 해결한 방식입니다. 먼저 COO 변환 형식의 문제점을 알아보
겠습니다. 다음과 같은 2 차원 배열을 COO 형식으로 변환해 보겠습니다.



CSR 방식의 변환은 사이파이의 csr_matrix 클래스를 이용해 쉽게 할 수 있습니다. 0 이 아닌 데이터 배
열과 열 위치 배열 , 그리고 행 위치 배열의 고유한 값의 시작 위치 배열을 csr_matrix 의 생성 파라미터
로 입력하면 됩니다.
from scipy import sparse
dense2 = np.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]])
# 0 이 아닌 데이터 추출
data2 = np.array([1, 5, 1, 4, 3, 2, 5, 6, 3, 2, 7, 8, 1])
# 행 위치와 열 위치를 각각 array로 생성
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))
print('COO 변환된 데이터가 제대로 되었는지 다시 Dense로 출력 확인')
print(sparse_coo.toarray())
print('CSR 변환된 데이터가 제대로 되었는지 다시 Dense로 출력 확인')
print(sparse_csr.toarray())
COO 와 CSR 이 어떻게 희소 행렬의 메모리를 줄일 수 있는지 지금까지 예제를 통해서 살펴봤습니다.
실제 사용 시에는 다음과 같이 밀집 행렬을 생성 파라미터로 입력하면 COOL CSR 희소 행렬로 생성
합니다.
dense3 = np.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]])
coo = sparse.coo_matrix(dense3)
csr = sparse.csr_matrix(dense3)
여러 가지 유형의 텍스트 분석을 실제로 구현
적합한 분류 알고리즘:
분류 과정:
고급 기법:
fetch_20newsgroups()는 인터넷에서 로컬 컴퓨터로 데이터를 먼저 내려받은 후에 메모리로 데이터
를 로딩합니다. 수행하려는 컴퓨터에 인터넷 연결이 정상적으로 되는지 확인한 후에 다음 예제를 수행
합니다.
from sklearn.datasets import fetch_20newsgroups
news_data = fetch_20newsgroups(subset='all',random_state=156)
fetch_20newsgroups( ) 는 사이킷런의 다른 데이터 세트 예제와 같이 파이썬 딕셔너리와 유사한
Bunch 객체를 반환합니다. 어떠한 key 값을 가지고 있는지 확인해 보겠습니다.
print(news_data.keys())
import pandas as pd
print('target 클래스의 값과 분포도 \n',pd.Series(news_data.target).value_counts().sort_index())
print('target 클래스의 이름들 \n',news_data.target_names)
Target 클래스의 값은 0 부터 19 까지 20 개로 구성돼 있으며 , 위의 출력 결과처럼 주어졌습니다 (Target
값 0: alt.atheism, Target 값 1: comp.graphics, . ) . 개별 데이터가 텍스트로 어떻게 구성돼 있는
지 데이터를 한 개만 추출해 값을 확인해 보겠습니다.
print(news_data.data[0])
from sklearn.datasets import fetch_20newsgroups
# subset='train'으로 학습용(Train) 데이터만 추출, remove=('headers', 'footers', 'quotes')로 내용만 추출
train_news= fetch_20newsgroups(subset='train', remove=('headers', 'footers', 'quotes'), random_state=156)
X_train = train_news.data
y_train = train_news.target
print(type(X_train))
# subset='test'으로 테스트(Test) 데이터만 추출, remove=('headers', 'footers', 'quotes')로 내용만 추출
test_news= fetch_20newsgroups(subset='test',remove=('headers', 'footers','quotes'),random_state=156)
X_test = test_news.data
y_test = test_news.target
print('학습 데이터 크기 {0} , 테스트 데이터 크기 {1}'.format(len(train_news.data) , len(test_news.data)))
from sklearn.feature_extraction.text import CountVectorizer
# Count Vectorization으로 feature extraction 변환 수행.
cnt_vect = CountVectorizer()
cnt_vect.fit(X_train)
X_train_cnt_vect = cnt_vect.transform(X_train)
# 학습 데이터로 fit( )된 CountVectorizer를 이용하여 테스트 데이터를 feature extraction 변환 수행.
X_test_cnt_vect = cnt_vect.transform(X_test)
print('학습 데이터 Text의 CountVectorizer Shape:',X_train_cnt_vect.shape)
학습 데이터를 CountVectorizer로 피처를 추출한 결과 11314 개의 문서에서 피처 , 즉 단어가 101631
개로 만들어졌습니다. 이렇게 피처 벡터화된 데이터에 로지스틱 회귀를 적용해 뉴스그룹에 대한 분류
를 예측해 보겠습니다.
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import warnings
warnings.filterwarnings('ignore')
# LogisticRegression을 이용하여 학습/예측/평가 수행.
lr_clf = LogisticRegression(solver='liblinear')
lr_clf.fit(X_train_cnt_vect , y_train)
pred = lr_clf.predict(X_test_cnt_vect)
print('CountVectorized Logistic Regression 의 예측 정확도는 {0:.3f}'.format(accuracy_score(y_test,pred)))
Count 기반으로 피처 벡터화가 적용된 데이터 세트에 대한 로지스틱 회귀의 예측 정확도는 약 0.616
입니다. 이번에는 Count 기반에서 TF-IDF 기반으로 벡터화를 변경해 예측 모델을 수행하겠습니다.
from sklearn.feature_extraction.text import TfidfVectorizer
# TF-IDF Vectorization 적용하여 학습 데이터셋과 테스트 데이터 셋 변환.
tfidf_vect = TfidfVectorizer()
tfidf_vect.fit(X_train)
X_train_tfidf_vect = tfidf_vect.transform(X_train)
X_test_tfidf_vect = tfidf_vect.transform(X_test)
# LogisticRegression을 이용하여 학습/예측/평가 수행.
lr_clf = LogisticRegression(solver='liblinear')
lr_clf.fit(X_train_tfidf_vect , y_train)
pred = lr_clf.predict(X_test_tfidf_vect)
print('TF-IDF Logistic Regression 의 예측 정확도는 {0:.3f}'.format(accuracy_score(y_test ,pred)))
# stop words 필터링을 추가하고 ngram을 기본(1,1)에서 (1,2)로 변경하여 Feature Vectorization 적용.
tfidf_vect = TfidfVectorizer(stop_words='english', ngram_range=(1,2), max_df=300 )
tfidf_vect.fit(X_train)
X_train_tfidf_vect = tfidf_vect.transform(X_train)
X_test_tfidf_vect = tfidf_vect.transform(X_test)
lr_clf = LogisticRegression(solver='liblinear')
lr_clf.fit(X_train_tfidf_vect , y_train)
pred = lr_clf.predict(X_test_tfidf_vect)
print('TF-IDF Vectorized Logistic Regression 의 예측 정확도는 {0:.3f}'.format(accuracy_score(y_test ,pred)))
GridSearchCV 를 이용해 로지스틱 회귀의 하이퍼 파라미터 최적화를 수행해 보겠습니다. 로
지스틱 회귀의 C 파라미터만 변경하면서 최적의 C 값을 찾은 뒤 이 C 값으로 학습된 모델에서 테스트 데
이터로 예측해 성능을 평가하겠습니다.
from sklearn.model_selection import GridSearchCV
# 최적 C 값 도출 튜닝 수행. CV는 3 Fold셋으로 설정.
params = { 'C':[0.01, 0.1, 1, 5, 10]}
grid_cv_lr = GridSearchCV(lr_clf ,param_grid=params , cv=3 , scoring='accuracy' , verbose=1 )
grid_cv_lr.fit(X_train_tfidf_vect , y_train)
print('Logistic Regression best C parameter :',grid_cv_lr.best_params_ )
# 최적 C 값으로 학습된 grid_cv로 예측 수행하고 정확도 평가.
pred = grid_cv_lr.predict(X_test_tfidf_vect)
print('TF-IDF Vectorized Logistic Regression 의 예측 정확도는 {0:.3f}'.format(accuracy_score(y_test ,pred)))
로지스틱 회귀의 C 가 10일 때 GridSearchCV의 교차 검증 테스트 세트에서 가장 좋은 예측 성능을 나
타냈으며 , 이를 테스트 데이터 세트에 적용해 약 0.704 로 이전보다 약간 향상된 성능 수치가 됐습니다.
사이킷런의 Pipeline 클래스는
장점:
Pipeline([('vectorizer', CountVectorizer()), ('clf', LogisticRegression())])
0 텍스트 분류, 수치형 데이터 모델링 등 다양한 작업에 활용 가능
from sklearn.pipeline import Pipeline
# TfidfVectorizer 객체를 tfidf_vect 객체명으로, LogisticRegression객체를 lr_clf 객체명으로 생성하는 Pipeline생성
pipeline = Pipeline([
('tfidf_vect', TfidfVectorizer(stop_words='english', ngram_range=(1,2), max_df=300)),
('lr_clf', LogisticRegression(solver='liblinear', C=10))
])
# 별도의 TfidfVectorizer객체의 fit_transform( )과 LogisticRegression의 fit(), predict( )가 필요 없음.
# pipeline의 fit( ) 과 predict( ) 만으로 한꺼번에 Feature Vectorization과 ML 학습/예측이 가능.
pipeline.fit(X_train, y_train)
pred = pipeline.predict(X_test)
print('Pipeline을 통한 Logistic Regression 의 예측 정확도는 {0:.3f}'.format(accuracy_score(y_test ,pred)))
사이킷런의 GridSearchCV는
Pipeline 객체도 입력 가능하여→ 피처 벡터화 + 모델 하이퍼파라미터를 동시에 최적화할 수 있음.
param_grid의 key 값은 "객체명__파라미터명" 형식 사용
예: tfidf_vect__ngram_range
너무 많은 파라미터 조합은 시간이 많이 소요됨
→ (예: 27 조합 × 3 CV = 81회 실행 → 약 24분)
from sklearn.pipeline import Pipeline
pipeline = Pipeline([
('tfidf_vect', TfidfVectorizer(stop_words='english')),
('lr_clf', LogisticRegression(solver='liblinear'))
])
# Pipeline에 기술된 각각의 객체 변수에 언더바(_)2개를 연달아 붙여 GridSearchCV에 사용될
# 파라미터/하이퍼 파라미터 이름과 값을 설정. .
params = { 'tfidf_vect__ngram_range': [(1,1), (1,2), (1,3)],
'tfidf_vect__max_df': [100, 300, 700],
'lr_clf__C': [1, 5, 10]
}
# GridSearchCV의 생성자에 Estimator가 아닌 Pipeline 객체 입력
grid_cv_pipe = GridSearchCV(pipeline, param_grid=params, cv=3 , scoring='accuracy',verbose=1)
grid_cv_pipe.fit(X_train , y_train)
print(grid_cv_pipe.best_params_ , grid_cv_pipe.best_score_)
pred = grid_cv_pipe.predict(X_test)
print('Pipeline을 통한 Logistic Regression 의 예측 정확도는 {0:.3f}'.format(accuracy_score(y_test ,pred)))
분류 방식:
지도학습(Supervised learning)
감성 레이블이 있는 학습 데이터를 사용
이를 기반으로 새로운 텍스트의 감성을 예측
일반적인 텍스트 분류 방식과 유사
비지도학습(Unsupervised learning)
Lexicon(감성 어휘 사전)을 활용
단어와 문맥의 감성 정보를 바탕으로
문서가 긍정적인지 부정적인지 판단
지도 학습은 데이터 기반 학습 → 예측
비지도 학습은 사전 기반 규칙 → 판단
import pandas as pd
review_df = pd.read_csv('/content/sample_data/labeledTrainData.tsv', header=0, sep="\t", quoting=3)
review_df.head(3)
피처
텍스트가 어떻게 구성돼 있는지 확인 (html형식)
print(review_df['review'][0])
import re
# <br> html 태그는 replace 함수로 공백으로 변환
review_df['review'] = review_df['review'].str.replace('<br />',' ')
# 파이썬의 정규 표현식 모듈인 re를 이용하여 영어 문자열이 아닌 문자는 모두 공백으로 변환
review_df['review'] = review_df['review'].apply( lambda x : re.sub("[^a-zA-Z]", " ", x) )
from sklearn.model_selection import train_test_split
class_df = review_df['sentiment']
feature_df = review_df.drop(['id','sentiment'], axis=1, inplace=False)
X_train, X_test, y_train, y_test= train_test_split(feature_df, class_df, test_size=0.3, random_state=156)
X_train.shape, X_test.shape
학습 데이터: 17,500개 리뷰, 테스트 데이터: 7,500개 리뷰
CountVectorizer : 벡터화
LogisticRegression : 분류 적용
Pipeline 사용해 벡터화 + 분류를 한꺼번에 수행
평가 지표:
정확도 (Accuracy)
ROC-AUC (이진 분류이므로 사용)
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score
# 스톱 워드는 English, filtering, ngram은 (1,2)로 설정해 CountVectorization수행.
# LogisticRegression의 C는 10으로 설정.
pipeline = Pipeline([
('cnt_vect', CountVectorizer(stop_words='english', ngram_range=(1,2) )),
('lr_clf', LogisticRegression(solver='liblinear', C=10))])
# Pipeline 객체를 이용하여 fit(), predict()로 학습/예측 수행. predict_proba()는 roc_auc때문에 수행.
pipeline.fit(X_train['review'], y_train)
pred = pipeline.predict(X_test['review'])
pred_probs = pipeline.predict_proba(X_test['review'])[:,1]
print('예측 정확도는 {0:.4f}, ROC-AUC는 {1:.4f}'.format(accuracy_score(y_test ,pred),
roc_auc_score(y_test, pred_probs)))
TfidfVectorizer 적용
# 스톱 워드는 english, filtering, ngram은 (1,2)로 설정해 TF-IDF 벡터화 수행.
# LogisticRegression의 C는 10으로 설정.
pipeline = Pipeline([
('tfidf_vect', TfidfVectorizer(stop_words='english', ngram_range=(1,2) )),
('lr_clf', LogisticRegression(solver='liblinear', C=10))])
pipeline.fit(X_train['review'], y_train)
pred = pipeline.predict(X_test['review'])
pred_probs = pipeline.predict_proba(X_test['review'])[:,1]
print('예측 정확도는 {0:.4f}, ROC-AUC는 {1:.4f}'.format(accuracy_score(y_test ,pred),
roc_auc_score(y_test, pred_probs)))
TF-IDF 기반 피처 벡터화의 예측 성능이 조금 더 나아짐
비지도 감성 분석은 Lexicon(감성 어휘 사전) 기반 방식입니다. 라벨이 없는 데이터에도 사용할 수 있어 유용합니다.
Lexicon(감성 사전): 긍정/부정 감성의 정도를 수치화한 Polarity Score 보유
문맥, 주변 단어, 품사 등을 고려해 감성 점수 부여
구현: NLTK의 Lexicon 모듈
WordNet: 영어 단어의 시맨틱(문맥 의미) 정보 제공
단어는 문맥에 따라 의미 달라지므로, 이를 Synset(동의어 집합)으로 표현함
NLTK의 감성 사전은 감성 분석의 기초 자료로는 훌륭하지만, 예측 성능이 낮다는 한계가 있음
따라서 실제 업무에서는 NLTK 외의 다른 감성 사전을 사용하는 경우가 많습니다.
대표적인 감성 사전
SentiWordNet:
VADER:
Pattern:
import nltk
nltk.download('all')
from nltk.corpus import wordnet as wn
term = 'present'
# 'present'라는 단어로 wordnet의 synsets 생성.
synsets = wn.synsets(term)
print('synsets() 반환 type :', type(synsets))
print('synsets() 반환 값 갯수:', len(synsets))
print('synsets() 반환 값 :', synsets)
synsets() 호출 결과: 여러 개의 synset 객체를 가지는 리스트 반환됨
present: 단어 의미
n: POS 태그 (명사)
01: 동일 품사 내 의미 인덱스
synset 객체는 다음 속성 포함:
POS (Part of Speech)
정의 (Definition)
부명제 (Lemma)
for synset in synsets :
print('##### Synset name : ', synset.name(),'#####')
print('POS :',synset.lexname())
print('Definition:',synset.definition())
print('Lemmas:',synset.lemma_names())
Synset('present.n.01')
Synset('present.n.02')
Synset('show.v.01')
동사 verb.perception
의미: ‘관객에게 전시물 등을 보여주다’
Synset은 하나의 단어가 가지는 다양한 의미(시맨틱 정보)를 개별 클래스로 표현함
WordNet은 단어 간 유사도를 나타낼 수 있음
# synset 객체를 단어별로 생성합니다.
tree = wn.synset('tree.n.01')
lion = wn.synset('lion.n.01')
tiger = wn.synset('tiger.n.02')
cat = wn.synset('cat.n.01')
dog = wn.synset('dog.n.01')
entities = [tree , lion , tiger , cat , dog]
similarities = []
entity_names = [ entity.name().split('.')[0] for entity in entities]
# 단어별 synset 들을 iteration 하면서 다른 단어들의 synset과 유사도를 측정합니다.
for entity in entities:
similarity = [ round(entity.path_similarity(compared_entity), 2) for compared_entity in entities ]
similarities.append(similarity)
# 개별 단어별 synset과 다른 단어의 synset과의 유사도를 DataFrame형태로 저장합니다.
similarity_df = pd.DataFrame(similarities , columns=entity_names,index=entity_names)
similarity_df

senti_synsets() 함수는
import nltk
from nltk.corpus import sentiwordnet as swn
senti_synsets = list(swn.senti_synsets('slow'))
print('senti_synsets() 반환 type :', type(senti_synsets))
print('senti_synsets() 반환 값 갯수:', len(senti_synsets))
print('senti_synsets() 반환 값 :', senti_synsets)
father → 감성 지수: 거의 0 → 객관성 지수: 1
fabulous → 긍정 감성 지수 높음 → 객관성 지수 낮음import nltk from nltk.corpus import sentiwordnet as swn
father = swn.senti_synset('father.n.01')
print('father 긍정감성 지수: ', father.pos_score())
print('father 부정감성 지수: ', father.neg_score())
print('father 객관성 지수: ', father.obj_score())
print('\n')
fabulous = swn.senti_synset('fabulous.a.01')
print('fabulous 긍정감성 지수: ',fabulous .pos_score())
print('fabulous 부정감성 지수: ',fabulous .neg_score())
**father: 감성 단어가 아님**
- 긍정 감성 지수 = 0
- 부정 감성 지수 = 0
- 객관성 지수 = 1.0 (매우 객관적인 단어)
**fabulous: 감성 단어 **
- 긍정 감성 지수 = 0.875
- 부정 감성 지수 = 0.125
- 객관성 지수 < 1.0 (감성 포함됨)
감성 사전은 단어가 감성적인지 객관적인지 수치로 구분
## SentiWordNet을 이용한 영화 감상평 감성 분석
SentiWordNet을 활용한 IMDB 영화 리뷰 감성 분석의 전체 순서는 다음과 같다
1. 문서(Document)를 문장(Sentence) 단위로 분해
2. 다시 문장을 단어(Word) 단위로 토큰화하고 품사 태깅
3. 품사 태깅된 단어 기반으로 Synset 객체와 Senti_Synset 객체를 생성
4. Senti_Synset 객체에서 긍정 감성 / 부정 감성 지수를 구하고 이를 모두 합산해 특정 임계치 값 이상일 때 긍정 감성으로, 그렇지 않을 때는 부정 감성으로 결정
SentiWordNet을 이용하기 위해서 WordNet을 이용해 문서를 다시 단어로 토큰화한 뒤, 어근 추출(Lemmatization)과 품사 태깅(PoS Tagging)을 적용해야 합니다.
먼저, 품사 태깅을 수행하는 내부 함수를 생성하겠습니다.
from nltk.corpus import wordnet as wn
def penn_to_wn(tag):
if tag.startswith('J'):
return wn.ADJ
elif tag.startswith('N'):
return wn.NOUN
elif tag.startswith('R'):
return wn.ADV
elif tag.startswith('V'):
return wn.VERB
return
----
- 문서를 문장 - 단어 토큰 - 품사 태깅 후에 SentiSynset 클래스를 생성하고, Polarity Score를 합산하는 함수를 생성
- 각 단어의 긍정 감성 지수와 부정 감성 지수를 모두 합한 총 감성 지수가 0 이상일 경우 긍정 감성, 그렇지 않을 경우 부정 감성으로 예측
from nltk.stem import WordNetLemmatizer
from nltk.corpus import sentiwordnet as swn
from nltk import sent_tokenize, word_tokenize, pos_tag
def swn_polarity(text):
# 감성 지수 초기화
sentiment = 0.0
tokens_count = 0
lemmatizer = WordNetLemmatizer()
raw_sentences = sent_tokenize(text)
# 분해된 문장별로 단어 토큰 -> 품사 태깅 후에 SentiSynset 생성 -> 감성 지수 합산
for raw_sentence in raw_sentences:
# NTLK 기반의 품사 태깅 문장 추출
tagged_sentence = pos_tag(word_tokenize(raw_sentence))
for word , tag in tagged_sentence:
# WordNet 기반 품사 태깅과 어근 추출
wn_tag = penn_to_wn(tag)
if wn_tag not in (wn.NOUN , wn.ADJ, wn.ADV):
continue
lemma = lemmatizer.lemmatize(word, pos=wn_tag)
if not lemma:
continue
# 어근을 추출한 단어와 WordNet 기반 품사 태깅을 입력해 Synset 객체를 생성.
synsets = wn.synsets(lemma , pos=wn_tag)
if not synsets:
continue
# sentiwordnet의 감성 단어 분석으로 감성 synset 추출
# 모든 단어에 대해 긍정 감성 지수는 +로 부정 감성 지수는 -로 합산해 감성 지수 계산.
synset = synsets[0]
swn_synset = swn.senti_synset(synset.name())
sentiment += (swn_synset.pos_score() - swn_synset.neg_score())
tokens_count += 1
if not tokens_count:
return 0
# 총 score가 0 이상일 경우 긍정(Positive) 1, 그렇지 않을 경우 부정(Negative) 0 반환
if sentiment >= 0 :
return 1
return 0
- swn_polarity(text) 함수를 IMDB 감상평 데이터셋의 각 문서에 적용해 감성(긍정/부정)을 예측함
- 이때 pandas의 apply(lambda ...) 문법을 사용하여 review_df의 각 행에 함수 적용함
- 예측 결과는 새로운 컬럼 'preds'에 저장됨
이후 sentiment 컬럼(실제 정답 레이블)과 비교하여 이를 평가:
- 정확도 (Accuracy)
- 정밀도 (Precision)
- 재현율 (Recall)
review_df['preds'] = review_df['review'].apply( lambda x : swn_polarity(x) )
y_target = review_df['sentiment'].values
preds = review_df['preds'].values
SentiWordNet의 감성 분석 예측 성능
from sklearn.metrics import accuracy_score, confusion_matrix, precision_score
from sklearn.metrics import recall_score, f1_score, roc_auc_score
import numpy as np
print(confusion_matrix( y_target, preds))
print("정확도:", np.round(accuracy_score(y_target , preds), 4))
print("정밀도:", np.round(precision_score(y_target , preds),4))
print("재현율:", np.round(recall_score(y_target, preds), 4))
SentiWordNet 기반 감성 분석의 결과:
- 정확도: 약 66.13%
- 재현율: 약 70.91%
- 전체적으로 성능이 만족스럽지는 않음
- SentiWordNet은 WordNet을 기반으로 하지만, 성능 개선에는 한계가 있음
## VADER를 이용한 감성 분석
- VADER는 소셜 미디어 텍스트의 감성 분석을 위해 만들어진 룰 기반의 Lexicon이음.
- SentimentIntensityAnalyzer 클래스를 이용해 감성 점수를 쉽게 계산할 수 있음.
- NLTK 서브모듈로 제공되며, 별도 설치 없이 사용할 수 있음.
- 또는 pip install vaderSentiment로 설치한 뒤 사용할 수도 있음.
- 문장에 대해 긍정, 부정, 중립, 종합 점수를 반환함.
- 버전에 따라 출력 결과는 다를 수 있음.
from nltk.sentiment.vader import SentimentIntensityAnalyzer
senti_analyzer = SentimentIntensityAnalyzer()
senti_scores = senti_analyzer.polarity_scores(review_df['review'][0])
print(senti_scores)
- VADER를 이용하면 매우 쉽게 감성 분석을 수행할 수 있음.
- 먼저 SentimentIntensityAnalyzer 객체를 생성한 뒤, 각 문서에 대해
- polarity_scores() 메서드를 호출하여 감성 점수를 구함.
- polarity_scores()는 네 가지 점수인 'neg', 'neu', 'pos', 'compound'를 포함한 딕셔너리를 반환함.
- 'compound' 점수는 -1에서 1 사이의 값으로 전체적인 감성의 강도를 나타냄.
- 보통 compound 값이 0.1 이상이면 긍정, 이하면 부정으로 간주함.
- 상황에 따라 이 임계값은 조정될 수 있음.
- vader_polarity() 함수를 정의해 감성 분석을 자동화하고, apply(lambda)를 통해 각 문서에 적용해 결과를 저장함.
- 해당 결과를 이용해 VADER의 예측 성능을 측정함.
def vader_polarity(review,threshold=0.1):
analyzer = SentimentIntensityAnalyzer()
scores = analyzer.polarity_scores(review)
# compound 값에 기반하여 threshold 입력값보다 크면 1, 그렇지 않으면 0을 반환
agg_score = scores['compound']
final_sentiment = 1 if agg_score >= threshold else 0
return final_sentiment
review_df['vader_preds'] = review_df['review'].apply( lambda x : vader_polarity(x, 0.1) )
y_target = review_df['sentiment'].values
vader_preds = review_df['vader_preds'].values
print(confusion_matrix( y_target, vader_preds))
print("정확도:", np.round(accuracy_score(y_target , vader_preds),4))
print("정밀도:", np.round(precision_score(y_target , vader_preds),4))
print("재현율:", np.round(recall_score(y_target, vader_preds),4))
정확도가 SentiWordNet보다 향상됐고, 특히 재현율은 약 85.14%로 매우 크게 향상됐음.
이 외에도 뛰어난 감성 사전으로 pattern 패키지가 있음.

- 감성 사전을 이용한 감성 분석 예측 성능은 지도 학습 분류 기반의 예측 성능에 비해 아직은 낮은 수준임.
- 그러나 결정 클래스 값이 없는 상황을 고려한다면 예측 성능에 일정 수준 만족할 수 있을 것임.
### 토픽 모델링 (Topic Modeling) - 20뉴스그룹
- 토픽 모델링이란 문서 집합에서 숨겨진 주제를 자동으로 찾아내는 작업임.
- 사람이 직접 모든 문서를 읽고 핵심 주제를 파악하는 것은 비효율적이므로, 머신러닝 기반 방법을 통해 핵심 단어를 추출해 효율적으로 표현함.
- 머신러닝 기반 토픽 모델링 기법에는 LSA와 LDA가 있으나, 여기서는 LDA(Latent Dirichlet Allocation)만 사용함.
- 이 LDA는 차원 축소 기법인 LDA(Linear Discriminant Analysis)와는 다른 알고리즘이므로 주의가 필요함.
- 실습은 앞서 사용한 20 Newsgroups 데이터셋을 활용하며, 이 데이터는 20가지 주제를 가진 뉴스 그룹으로 구성됨
**총 8개의 주제 — 모터사이클, 야구, 그래픽스, 윈도우, 중동, 기독교, 전자공학, 의학 — 을 추출하여, 이들 텍스트에 대해 LDA(Latent Dirichlet Allocation) 기반 토픽 모델링을 적용함.**
- LDA는 사이킷런에서 LatentDirichletAllocation 클래스를 통해 제공되며, 초기에는 지원되지 않았으나 gensim의 인기에 따라 추가된 기능임.
**모델링 절차는 다음과 같음:**
- fetch_20newsgroups() API에서 categories 파라미터를 사용해 필요한 주제만 필터링함.
- 텍스트는 Count 기반 벡터화로 변환함 (LDA는 TF-IDF가 아닌 Count 기반만 사용함).
**벡터화는 다음 설정을 따름:**
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
cats = ['rec.motorcycles'
, 'rec.sport.baseball', 'comp.graphics', 'comp.windows.x',
'talk.politics.mideast', 'soc.religion.christian', 'sci.electronics', 'sci.med']
news_df= fetch_20newsgroups(subset='all', remove=('headers', 'footers', 'quotes'),
categories=cats, random_state=0)
count_vect = CountVectorizer(max_df=0.95, max_features=1000, min_df=2, stop_words='english',
ngram_range=(1, 2))
feat_vect = count_vect.fit_transform(news_df.data)
print( 'CountVectorizer Shape:', feat_vect.shape)
- CountVectorizer로 생성한 feat_vect는 총 7,862개의 문서와 1,000개의 피처로 구성된 행렬 데이터를 나타냄.
- 이 피처 벡터화된 데이터를 기반으로 LDA 토픽 모델링을 수행함.
- 토픽 수는 뉴스 그룹에서 추출한 주제 수와 동일하게 8개로 설정함.
- LatentDirichletAllocation 클래스의 n_components 파라미터를 통해 토픽 수를 설정하며, random_state는 실행 결과의 일관성을 위해 설정함.
lda = LatentDirichletAllocation(n_components=8, random_state=0)
lda.fit(feat_vect)
- LatentDirichletAllocation.fit(데이터셋)을 수행하면 LDA 모델 객체는 components_라는 속성을 가지게 됨.
- 이 components_ 속성은 **각 토픽마다 단어(feature)**가 얼마나 많이 해당 토픽에 할당되었는지를 나타내는 수치 행렬임.
- 형식: components_는 (n_topics, n_features) 형태의 2차원 배열
각 행(row)은 하나의 토픽
각 열(column)은 하나의 단어 피처
- 값이 클수록 해당 단어가 그 토픽의 **핵심 단어(중요 단어)**임을 의미함
print(lda.components.shape)
lda.components
components_ 값만으로는 각 토픽별 중심 단어를 사람이 이해하기 어렵기 때문에, display_topics() 함수를 만들어야 함.
def displaytopics(model, feature_names, no_top_words):
for topic_index, topic in enumerate(model.components):
print('Topic #', topic_index)
topic_word_indexes = topic.argsort()[::-1]
top_indexes=topic_word_indexes[:no_top_words]
feature_concat = ' '.join([feature_names [i] for i in top_indexes])
print(feature_concat)
feature_names = count_vect.get_feature_names_out()
display_topics(lda, feature_names, 15)
총 8개의 주제 중 일부는 잘 매핑되었으나, 일부는 주제가 불명확함.
- 의학, 중동, 그래픽스, 기독교, 윈도우즈 관련 토픽은 명확하게 추출됨.
- 반면 모터사이클, 야구 관련 주제는 추출되지 않았으며, Topic #1, #3, #5는 일반 단어 위주로 애매한 주제어가 포함됨.