<jupyter_start><jupyter_text>2022/01/21/FRI >  텍스트 분류 실습 사이킷런이 내부에 가지고 있는 예제 데이터인 20 뉴스그룹 데이터 세트를 이용해 텍스트 분류를 적용해 보자. 텍스트 분류는 특정 문서의 분류를 학습 데이터를 통해 학습해 모델을 생성한 뒤 이 학습 모델을 이용해 다른 문서의 분류를 예측하는 것이다. 텍스트를 feature 벡터화로 변환하면 일반적으로 희소 행렬 형태가 된다. 그리고 이러한 희소 행렬에 분류를 효과적으로 잘 처리할 수 있는 알고리즘은 로지스틱 회구, 선형 서포트 벡터 머신, 나이브 베이즈 등이다. 이 중 로지스틱 회귀를 이용해 분류를 수행해보자. 텍스트를 기반으로 분류를 수행할 때는 먼저 텍스트를 정규화한 뒤 feature 벡터화를 적용한다. 그리고 그 이후에 적합한 머신러닝 알고리즘을 적용해 분류를 학습/예측/평가합니다.  - 텍스트 정규화<jupyter_code>from sklearn.datasets import fetch_20newsgroups

news_data = fetch_20newsgroups(subset='all',random_state=156)<jupyter_output>C:\Users\ehfus\Anaconda3\envs\dv2021\lib\site-packages\numpy\_distributor_init.py:30: UserWarning: loaded more than 1 DLL from .libs:
C:\Users\ehfus\Anaconda3\envs\dv2021\lib\site-packages\numpy\.libs\libopenblas.EL2C6PLE4ZYW3ECEVIV3OXXGRN2NRFM2.gfortran-win_amd64.dll
C:\Users\ehfus\Anaconda3\envs\dv2021\lib\site-packages\numpy\.libs\libopenblas.XWYDX2IKJW2NMTWSFYNGFUWKQU3LYTCZ.gfortran-win_amd64.dll
  warnings.warn("loaded more than 1 DLL from .libs:"<jupyter_text>- 어떠한 key 값을 가지고 있는지 확인해 보자.<jupyter_code>print(news_data.keys())<jupyter_output>dict_keys(['data', 'filenames', 'target_names', 'target', 'DESCR'])<jupyter_text>- 다음으로 Target 클래스가 어떻게 구성돼 있는지 확인해 보자<jupyter_code>import pandas as pd

print('target 클래스의 값과 분포도 \n',pd.Series(news_data.target).value_counts().sort_index())
print('target 클래스의 이름들 \n',news_data.target_names)
len(news_data.target_names), pd.Series(news_data.target).shape<jupyter_output>target 클래스의 값과 분포도 
 0     799
1     973
2     985
3     982
4     963
5     988
6     975
7     990
8     996
9     994
10    999
11    991
12    984
13    990
14    987
15    997
16    910
17    940
18    775
19    628
dtype: int64
target 클래스의 이름들 
 ['alt.atheism', 'comp.graphics', 'comp.os.ms-windows.misc', 'comp.sys.ibm.pc.hardware', 'comp.sys.mac.hardware', 'comp.windows.x', 'misc.forsale', 'rec.autos', 'rec.motorcycles', 'rec.sport.baseball', 'rec.sport.hockey', 'sci.crypt', 'sci.electronics', 'sci.med', 'sci.space', 'soc.religion.christian', 'talk.politics.guns', 'talk.politics.mideast', 'talk.politics.misc', 'talk.religion.misc']<jupyter_text>- 개별 데이터가 텍스트로 어떻게 구성돼 있는지 데이터를 한 개만 추출해 값을 확인해 보자<jupyter_code>print(news_data.data[0])<jupyter_output>From: <EMAIL> (<NAME> - Pixel Cruncher)
Subject: Re: Observation re: helmets
Organization: Sun Microsystems, RTP, NC
Lines: 21
Distribution: world
Reply-To: <EMAIL>
NNTP-Posting-Host: laser.east.sun.com

In article <EMAIL>, <EMAIL> (<NAME>) writes:
> 
> The question for the day is re: passenger helmets, if you don't know for 
>certain who's gonna ride with you (like say you meet them at a .... church 
>meeting, yeah, that's the ticket)... What are some guidelines? Should I just 
>pick up another shoei in my size to have a backup helmet (XL), or should I 
>maybe get an inexpensive one of a smaller size to accomodate my likely 
>passenger? 

If your primary concern is protecting the passenger in the event of a
crash, have him or her fitted for a helmet that is their size.  If your
primary concern is complying with stupid helmet laws, carry a real big
spare (you can put a big or small head in a big helmet, bu[...]<jupyter_text>- 텍스트 데이터를 확인해 보면 뉴스그룹 기사의 내용뿐만 아니라 뉴스그룹 제목, 작성자, 소속, 이메일 등의 다양한 정보를 가지고 있다. 이 중에서 내용을 제외하고 제목 등의 다른 정보는 제거하자. - remove 파라미터를 이용하면 뉴스그룹 기사의 헤더, 푸터 정보등을 제거할 수 있다. - 또한 fetch_20newsgroup()는 subset 파라미터를 이용해 학습 데이터 세트와 테스트 데이터 세트를 분리해 내려받을 수 있다.<jupyter_code>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)))<jupyter_output><class 'list'>
학습 데이터 크기 11314 , 테스트 데이터 크기 7532<jupyter_text>- feature 벡터화 변환과 머신러닝 모델 학습/예측/평가      학습 데이터는 11314개의 뉴스 그룹 문서가 리스트 형태로 주어지고, 테스트 데이터는 7532개의 문서가 역시 리스트 형태로 주어졌다. CountVectorizer를 이용해 학습 데이터의 텍스트를 feature 벡터화하겠다. 테스트 데이터 역시 feature 벡터화를 수행하는데 한 가지 반드시 유의해야할 것은 바로 테스트 데이터에서 CountVectorizer를 적용할 때는 반드시 학습 데이터를 이용해 fit()이 수행된 CountVertorizer 객체를 이용해 테스트 데이터를 변환해야 한다는 것이다. 그래야만 학습 시 설정된 CountVectorizer의 feature 개수와 테스트 데이터를 CountVectorizer로 변환할 feature 개수가 같아진다. 테스트 데이터의 feature 벡터화는 학습 데이터에 사용된 CountVectorizer 객체 변수인 cnt_vect.transform()을 이용해 변환한다.<jupyter_code>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, X_test_cnt_vect.shape)<jupyter_output>학습 데이터 Text의 CountVectorizer Shape: (11314, 101631) (7532, 101631)<jupyter_text>- 학습 데이터를 CountVectorizer로 feature를 추출한 결과 11341개의 문서에서 feature 즉, 단어가 101631개로 만들어졌다. 이렇게 feature 벡터화된 데이터에 로지스틱 회귀를 적용해 뉴스 그룹에 대한 분류를 예측해보자 ```pythonfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import accuracy_score LogisticRegression을 이용하여 학습/예측/평가 수행. lr_clf = LogisticRegression()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 기반으로 feature 벡터화가 적용된 데이터 세트에 대한 로지스틱 회귀의 예측 정확도는 0.617이다. - TF-IDF 기반으로 벡터화를 변경해 예측 모델을 수행해보자<jupyter_code>from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score
# 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()
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)))<jupyter_output>TF-IDF Logistic Regression 의 예측 정확도는 0.674<jupyter_text>- TF-IDF가 단순 카운트 기반보다 훨씬 높은 예측 정확도를 제공한다. - 일반적으로 문서 내에 텍스트가 많고 많은 문서를 가지는 텍스틑 분석에서 카운트 벡터화보다는 TF-IDF 벡터화가 좋은 예측 결과를 도출한다.- 텍스트 분석에서 머신러닝 모델의 성능을 향상시키는 중요한 2가지 방법은 최적의 ML 알고리즘을 선택하는 것과 최상의 feature 전처리를 수행하는 것이다.- TF-IDF 벡터화에서 좀 더 다양한 파라미터를 적용해보자 ```python 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()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값으로 학습된 모델에서 테스트 데이터로 예측해 성능을 평가해보자 ```pythonfrom 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.703으로 이전보다 약간 향상된 성능 수치가 됐다.  >  ***사이킷런 파이프라인 사용 및 GridSearchCV와의 결합*** 사이킷런의 Pipeline 클래스를 이용하면 feature 벡터화와 ML 알고리즘 학습/예측을 위한 코드 작성을 한 번에 진행할 수 있다. 일반적으로 머신러닝에서 Pipeline이란 데이터의 가공, 변환 등의 전처리와 알고리즘 적용을 마치 '수도관(Pipe)에서 물이 흐르듯' 한꺼번에 스트림 기반으로 처리한다는 의미이다. 이렇게 Pipeline을 이용하면 데이터 전처리와 머신러닝 학습 과정을 통일된 API 기반에서 처리할 수 있어 더 직관적인 ML 모델 코드를 생성할 수 있다. 또한 대용량 데이터의 feature 벡터화 결과를 별도 데이터로 저장하지 않고 스트림 기반에서 바로 머신러닝 알고리즘의 데이터로 입력할 수 있기 때문에 수행시간을 절약할 수 있다. 일반적으로 사이킷런 파이프라인은 텍스트 기반의 feature 벡터화뿐만 아니라 모든 데이터 전처리 작업과 Estimator를 결합할 수 있다. 예를 들어 스케일링 또는 벡터 정규화, PCA 등의 변환 작업과 분류, 회귀 등의 Estimator를 한 번에 결합하는 것이다.        다음은 위에서 텍스트 분류 예제 코드를 Pipeline을 이용해 다시 작성한 코드이다       Pipeline 객체는 다음과 같이 선언한다.  - TfidfVectorizer 객체를 tfidf_vect라는 객체 변수 명으로, LogisticRegression 객체를 lr_clf라는 객체 변수 명으로 생성한 뒤 이 두 개의 객체를 파이프라인으로 연결하는 Pipeline 객체 pipeline을 생성한다는 의미이다. - 또한 다음 코드를 보면 기존 TfidfVectorizer의 학습 데이터와 테스트 데이터에 대한 fit()과 transform() 수행을 통한 feature 벡터화와 LogisticRegressor의 fit()과 predict() 수행을 통한 머신러닝 모델의 학습과 예측이 Pipeline의 fit()과 predict()로 통일돼 수행됨을 알 수 있다. 이렇게 Pipeline 방식을 적용하면 머신러닝 코드를 더 직관적이고 쉽게 작성할 수 있다. ```pythonfrom 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(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을 입력해 Pipeline 기반에서도 하이퍼 파라미터 튜닝을 GridSearchCV 방식으로 진행할 수 있게 지원한다. 이렇게 하면 feature 벡터화를 위한 파라미터와 ML 알고리즘의 하이퍼 파라미터를 모두 한 번에 GridSearchCV를 이용해 최적화할 수 있다. - 다음 예제는 GridSearchCV에 Pipeline을 입력하면서 TfidfVectorizer의 파라미터와 LogisticRegressor의 하이퍼 파라미터를 함께 최적화한다. - GridSearchCV에 Estimator가 아닌 Pipeline을 입력할 경우에는 param_grid의 입력 값 설정이 기존과 약간 다르다. 딕셔너리 형태로 들어간다. - Pipeline+GridSearchCV를 적용할 때 유의할 점을 모두의 파라미터를 최적화하려면 너무 많은 튜닝 시간이 소모된다는 점이다.- 다음 코드는 수행시 너무 많은 시간이 소모되므로 Markdown 형식으로 남겨놓겠다. ```pythonfrom sklearn.pipeline import Pipelinepipeline = Pipeline([    ('tfidf_vect', TfidfVectorizer(stop_words='english')),    ('lr_clf', LogisticRegression())]) 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)))``` >  감성 분석 문서의 주고나적인 감성,의견,감정,기분 등을 파악하기 위한 방법으로 소셜 미디어, 여론 조사, 온라인 리뷰, 피드백 등 다양한 분야에서 활용되고 있다. 감성 분석은 문서 내 텍스트가 나타내는 여러 가지 주관적인 단어와 문맥을 기바으로 감성(Sentiment) 수치를 계산하는 방법을 이용한다. 이러한 감성 지수는 긍정 감성 지수와 부정 감성 지수로 구성되며 이들 지수를 합산해 긍정 감성 또는 부정 감성을 결정한다. 이러한 감성 분석은 머신러닝 관점에서 지도학습과 비지도학습 방식으로 나눌 수 있다.            지도학습 : 학습 데이터와 타킷 레이블 값을 기반으로 감성 분석 학습을 수행한 뒤 이를 기반으로 다른 데이터의 감성 분석을 예측하는 방법으로 일반적인 텍스트 기반의 분류와 거의 동일하다.      비지도학습 : 'Lexicon'이라는 일종의 감성 어휘 사전을 이용한다. Lexicon은 감성 분석을 위한 용어와 문맥에 대한 다양한 정보를 가지고 있으며, 이를 이용해 문서의 긍정적, 부정적 감성 여부를 판단한다. - 지도학습 기반 감성 분석 실습 $\to$ IMDB 영화평- 먼저 지도학습 기반으로 감성 분석을 수행하겠다. 유명한 IMBD의 영화 사이트의 영화평을 이용하겠다. (저자는 감성 분석이라는 타이틀이 붙었지만 지도학습 기반 감성 분석은 텍스트 기반의 이진 분류라고 표현하고 싶다고 했다.)- 영화평의 텍스트를 분석해 감성 분석 결과가 긍정 또는 부정인지를 예측하는 모델을 만들어보자.<jupyter_code>import pandas as pd

review_df = pd.read_csv('./labeledTrainData.tsv', header=0, sep="\t", quoting=3)
review_df.head(3)<jupyter_output><empty_output><jupyter_text>- Sentiment 칼럼의 1은 긍정적 평가, 0은 부정적 평가를 의미한다. - 이번에는 텍스트가 어떻게 구성돼 있는지 review 칼럼의 텍스트 값을 하나만 살펴보자<jupyter_code>print(review_df['review'][0])<jupyter_output>"With all this stuff going down at the moment with MJ i've started listening to his music, watching the odd documentary here and there, watched The Wiz and watched Moonwalker again. Maybe i just want to get a certain insight into this guy who i thought was really cool in the eighties just to maybe make up my mind whether he is guilty or innocent. Moonwalker is part biography, part feature film which i remember going to see at the cinema when it was originally released. Some of it has subtle messages about MJ's feeling towards the press and also the obvious message of drugs are bad m'kay.<br /><br />Visually impressive but of course this is all about <NAME> so unless you remotely like MJ in anyway then you are going to hate this and find it boring. Some may call MJ an egotist for consenting to the making of this movie BUT MJ and most of his fans would say that he made it for the fans which if true is really nice of him.<br /><br />The actual feature film bit when it finally sta[...]<jupyter_text>- HTML 형식에서 추출해 \ 태그가 여전히 존재한다. 이 문자열은 feature로 만들 필요가 없으니 삭제.- 판다스의 DataFrame/Series는 문자열 연산을 지원하기 위해 str 속성을 이용한다. DataFrame/Seires 객체에서 str을 적용하면 다양한 문자열 연산을 수행할 수 있다.- \ 태그를 공백으로 모두 바꿔주자- 영어가 아닌 숫자와 특수문자 역시 Sentiment를 위한 feature로는 별 의미가 없어 보이므로 이들 모두 공란으로 변경하자. 이를 찾고 공란으로 변환하는 과정은 정규 표현식을 이용하겠다.- 정규 표현식은 텍스트를 처리하는 데 있어서 매우 유용하므로 익혀둘 필요가 있다.<jupyter_code>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) )<jupyter_output><empty_output><jupyter_text>- 결정 값 클래스인 sentiment 칼럼을 별도로 추출해 결정 값 데이터 세트를 만들고, 원본 데이터 세트에서 id와 sentiment 칼럼을 삭제해 feature 데이터 세트를 생성한다. - 그리고 train_test_split()을 이용해 학습용과 테스트용 데이터 세트로 분리한다.<jupyter_code>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<jupyter_output><empty_output><jupyter_text>- 이제 review 텍스트를 feature 벡터화한 후에 ML 분류 알고리즘을 적용해 예측 성능을 측정하자. - 앞 절에서 설명한 Pipeline 객체를 이용해 이 두 가지를 한꺼번에 수행해보자.<jupyter_code>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<jupyter_output><empty_output><jupyter_text>```python 스톱 워드는 English, filtering, ngram은 (1,2)로 설정해 CountVectorization수행.  LogisticRegression의 C는 10으로 설정. pipeline = Pipeline([    ('cnt_vect', CountVectorizer(stop_words='english', ngram_range=(1,2) )),    ('lr_clf', LogisticRegression(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)))``` - 이번에는 TF-IDF 벡터화를 적용해 다시 예측 성능을 특정해 보자.<jupyter_code># 스톱 워드는 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(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)))<jupyter_output>예측 정확도는 0.8936, ROC-AUC는 0.9598<jupyter_text>- TF-IDF 기반 feature 벡터화의 예측 성능이 조금 더 나아졌다.  > 비지도학습 기반 감성 분석      Lexicon을 기반으로 이루어짐. 위에선 데이터 세트가 레이블 값을 가지고 있었지만 많은 감성 분석용 데이터는 이러한 결정된 레이블 값을 가지고 있지 않다. 이러한 경우에 Lexicon은 유용하게 사용된다. Lexicon은 긍정 감성 또는 부정 감성의 정도를 의미하는 수치를 가지고 있으며 이를 감성 지수라고 한다. 이 감성 지수는 단어의 위치나 주변 단어, 문맥, POS(Part of Speech)등을 참고해 결정된다.<jupyter_code>import nltk<jupyter_output><empty_output><jupyter_text>- 502p 참고- SentiWordNet과 VADER 감성 사전을 이용해 감성 분석을 수행한 뒤 예측 성능을 지도학습 기반의 분류와 비교해보자- 'present' 단어에 대한 Synset을 추출하자.<jupyter_code>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)<jupyter_output>synsets() 반환 type : <class 'list'>
synsets() 반환 값 갯수: 18
synsets() 반환 값 : [Synset('present.n.01'), Synset('present.n.02'), Synset('present.n.03'), Synset('show.v.01'), Synset('present.v.02'), Synset('stage.v.01'), Synset('present.v.04'), Synset('present.v.05'), Synset('award.v.01'), Synset('give.v.08'), Synset('deliver.v.01'), Synset('introduce.v.01'), Synset('portray.v.04'), Synset('confront.v.03'), Synset('present.v.12'), Synset('salute.v.06'), Synset('present.a.01'), Synset('present.a.02')]<jupyter_text>- 총 18개의 서로 다른 semantic(문맥상의)을 가지는 synset 객체가 반환됐다. - synset 객체가 가지는 여러 가지 속성을 살펴보면, POS(품사), 정의, 부명제 등으로 시맨틱적인 요소를 표현할 수 있다.  ```pythonfor synset in synsets :    print(' Synset name : ', synset.name(),'')    print('POS :',synset.lexname())    print('Definition:',synset.definition())    print('Lemmas:',synset.lemma_names())``` - 이처럼 synset은 하나의 단어가 가질 수 있는 여러 가지 시맨틱 정보를 개별 클래스로 나타낸 것이다. - WordNet은 어떤 어휘와 다른 어휘 간의 관계를 유사도로 나타낼 수 있다. synset 객체는 단어 간의 유사도를 나타내기 위해서 path_similarity() 메서드를 제공한다. - 해당 메서드를 통해 'tree','lion','tiger','cat','dog'라는 단어의 상호 유사도를 살펴보자<jupyter_code># 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<jupyter_output><empty_output><jupyter_text>- SentiWordNet은 WordNet의 Synset과 유사한 Senti_Synset 클래스를 가지고 있다. SentiWordNet 모듈의 senti_synsets()는 WordNet 모듈이라서 synsets()와 비슷하게 Senti_Synset 클래스를 리스트 형태로 반환한다.<jupyter_code>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)<jupyter_output>senti_synsets() 반환 type : <class 'list'>
senti_synsets() 반환 값 갯수: 11
senti_synsets() 반환 값 : [SentiSynset('decelerate.v.01'), SentiSynset('slow.v.02'), SentiSynset('slow.v.03'), SentiSynset('slow.a.01'), SentiSynset('slow.a.02'), SentiSynset('dense.s.04'), SentiSynset('slow.a.04'), SentiSynset('boring.s.01'), SentiSynset('dull.s.08'), SentiSynset('slowly.r.01'), SentiSynset('behind.r.03')]<jupyter_text>- SentiSynset 객체는 단어의 감성을 나타내는 감성 지수와 객관성을 나타내는 객관성 지수를 가지고 있다. 감성 지수는 다시 긍정 감성 지수와 부정 감성 지수로 나뉜다. 어떤 단어가 전혀 감성적이지 않으면 객관성 지수는 1이 되고 감성 지수는 모두 0이 된다. 다음은 father라는 단어와 fabulous라는 두 개의 단어의 감성 지수와 객관성 지수를 나타낸다.<jupyter_code>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())<jupyter_output>father 긍정감성 지수:  0.0
father 부정감성 지수:  0.0
father 객관성 지수:  1.0


fabulous 긍정감성 지수:  0.875
fabulous 부정감성 지수:  0.125<jupyter_text>- father는 객관적인 단어로 객관성 지수가 1이고 긍정 감성/부정 감성 지수 모두 0이다. 반면에 fabulous는 감성 단어로서 긍정 감성 지수가 0.9에 육박한다. - 이제 IMBD 영화 감상평 감성 분석을 SentiWordNet Lexicon 기반으로 수행해 보자.1. 문서를 문장 단위로 분해2. 다시 문장을 단어 단위로 토큰화하고 품사 태깅3. 품사 태깅된 단어 기반으로 synset 객체와 senti_synset 객체를 생성4. Senti_synset에서 긍정 감성/부정 감성 지수를 구하고 이를 모두 합산해 특정 임계치 값 이상일 때 긍정 감성으로, 그렇지 않을 때는 부정 감성으로 결정- SentiWordNet을 이용하기 위해서 WordNet을 이용해 문서를 다시 단어로 토큰화한 뒤 어근 추출과 품사 태깅을 적용한다.- 먼저 품사 태킹을 수행하는 내부 함수를 생성한다.<jupyter_code>from nltk.corpus import wordnet as wn

# 간단한 NTLK PennTreebank Tag를 기반으로 WordNet기반의 품사 Tag로 변환
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<jupyter_output><empty_output><jupyter_text>- 이제 문서를 문장 $\to$ 단어 토큰 $\to$ 품사 태깅 후에 SentiSynset 클래스를 생성하고 Polarity Score를 합산하는 함수를 생성한다. - 각 단어의 긍정 감성 지수와 부정 감성 지수를 모두 합한 총 감성 지수가 0 이상일 경우 긍정 감성, 그렇지 않을 경우 부정 감성으로 예측한다.<jupyter_code>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
review_df['preds'] = review_df['review'].apply( lambda x : swn_polarity(x) )
y_target = review_df['sentiment'].values
preds = review_df['preds'].values
from sklearn.metrics import accuracy_score, confusion_matrix, precision_score 
from sklearn.metrics import recall_score, f1_score, roc_auc_score

print(confusion_matrix( y_target, preds))
print("정확도:", accuracy_score(y_target , preds))
print("정밀도:", precision_score(y_target , preds))
print("재현율:", recall_score(y_target, preds))<jupyter_output>[[7668 4832]
 [3636 8864]]
정확도: 0.66128
정밀도: 0.647196261682243
재현율: 0.70912<jupyter_text>- 전반적인 성능 평가 지표가 만족스러울 만한 수치는 아니다. - 이번에는 좀 더 성능 평가 지표가 잘 나올 것으로 예상되는 VADER를 이용해 감성 분석을 수행해보자. - VADER는 소셜 미디어의 감성 분석 용도로 만들어진 룰 기반의 Lexicon이다. - 간략하게 IMDB의 감상평 한 개만 감성 분석을 수행해 결과를 살펴보자- VADER의 경우 지속적으로 버전이 업데이트 되므로 설치한 VADER 버전에 따라 다음 결과는 상이할 수 있다.<jupyter_code>from nltk.sentiment.vader import SentimentIntensityAnalyzer

senti_analyzer = SentimentIntensityAnalyzer()
senti_scores = senti_analyzer.polarity_scores(review_df['review'][0])
print(senti_scores)<jupyter_output>{'neg': 0.13, 'neu': 0.743, 'pos': 0.127, 'compound': -0.7943}<jupyter_text>- 이처럼 VADER를 이용하면 매우 쉽게 감성 분석을 수행할 수 있다. 먼저 위 셀의 두번 째 줄처럼 객체를 생성한 뒤 문서별로 polarity_scores() 메서드를 호출해 감성 점수를 구한 뒤, 해당 문서의 감성 점수가 특정 임계값 이상이면 긍정, 그렇지 않으면 부정으로 판단한다. - VADER를 이용해 IMDB의 감성 분석을 수행해보자.<jupyter_code>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

# apply lambda 식을 이용하여 레코드별로 vader_polarity( )를 수행하고 결과를 'vader_preds'에 저장
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('#### VADER 예측 성능 평가 ####')
from sklearn.metrics import accuracy_score, confusion_matrix, precision_score 
from sklearn.metrics import recall_score, f1_score, roc_auc_score

print(confusion_matrix( y_target, vader_preds))
print("정확도:", accuracy_score(y_target , vader_preds))
print("정밀도:", precision_score(y_target , vader_preds))
print("재현율:", recall_score(y_target, vader_preds))<jupyter_output>#### VADER 예측 성능 평가 ####
[[ 6747  5753]
 [ 1858 10642]]
정확도: 0.69556
정밀도: 0.6491003354681305
재현율: 0.85136
{'fraction_non_alphanumeric': 0.10180499661314102, 'fraction_numerical': 0.022831414113240626, 'mean_word_length': 4.393939393939394, 'pattern_counts': {'":': 0, '<': 90, '<?xml version=': 0, '>': 105, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '22', 'dir': 'jupyter-structured-clean-dedup', 'id': '433875', 'license': 'Apache-2.0', 'n_tokens_mistral': 14408, 'n_tokens_neox': 16962, 'n_words': 3034, 'path': '_notebooks/2022-01-21-intro.ipynb', 'repo_name': 'rhkrehtjd/INTROml', 'size': '25187'}