본문 바로가기
LG 헬로비전 DX DATA SCHOOL/Python

NLP(Natural Language Processing) 자연어 처리

by 황밤 2023. 9. 5.
728x90
반응형

1. 자연어 처리

 

1.1) NLP

 

 

 

1.2) 텍스트 분석의 기술 영역

  • 텍스트 분류 : 텍스트를 보고 어떤 카테고리에 속하는지 분류
  • 감성 분석
  • 텍스트 요약 : 대표적인 기법이 토픽 모델링
  • 텍스트 군집화와 유사도 측정
  • 텍스트 전처리
  • ML 모델 수립 및 학습/예측/평가 : 초창기에는 일반 머신러닝 모델을 가지고 많이 작업을 했는데 최근에는 딥러닝의 RNN, Transformer 같은 생성 모델이나 미리 학습된 모델 등을 많이 이용

 

1.3) 자연어 처리를 위한 패키지

  • NLTK : 가장 많이 사용되던 자연어 처리 패키지 이나, 수행 속도가 느려 요즘같은 대량의 텍스트 기반 자연어 처리에는 부적합
  • Gensim : 토픽 모델링에서 많이 사용되는 패키지, Word2Vec 같은 알고리즘이 구현되어 있음.
  • SpaCy 
  • Konlpy : 한글 형태소 분석 패키지
  • 그 이외 Tensorflow 나 Pytorch 또는 미리 학습된 모델인 BERT, GPT 같은 모델을 많이 이용합니다.

1.4) 한글 형태소 분석기 설치

  • 1.7 버전 이상의 JDK 설치하고 PATH에 JDK 경로의 bin 을 추가하고 JAVA_HOME 이라는 환경 변수에 JDK 의 경로를 추가 
  • Windows 는 Visual C++ 재배포 패키지 나 Visual Studio Build Tool를 설치
  • JPype1-py3 를 설치

pip로 설치해도 되는 경우가 있고 conda로 설치해야 하는  경우가 있음

  • konlpy를 설치
  • colab(구글에서 제공하는 파이썬 IDE - Ubuntu Linux)에서 사용
%%bash

apt-get update

apt-get install g++ openjdk-8-jdk python-dev python3-dev

pip3 install JPype1

pip3 install konlpy



%env JAVA_HOME "/usr/lib/jvm/java-8-openjdk-amd64

 


2. 텍스트 전처리

  • 텍스트 자체를 바로 피처로 사용할 수 없기 때문에 사전에 텍스트를 가공하는 작업이 필요

2.1) 전처리 작업

  • 클렌징 - 불필요한 문자열 제거
  • 토큰화 - 단어나 글자를 분리
  • 필터링, 스톱워드제거, 철자 수정
  • Stemming & Lemmatization

 

2.2) 토큰화

  • 문장 토큰화와 단어 토큰화 2가지
  • 문장 토큰화
마침표나 개행 문자(\n 또는 \r\n)를 기준으로 문장을 분리하는 것
정규 표현식을 이용하기도 함
nltk 패ㅣ지의 punkt 서브 패키지를 이용
sent_tokenize 함수를 사용

2.3) Stop Word 제거

  • stop word(불용어) : 분석에 큰 의미가 없는 단어
  • 영어의 경우는 nltk 패키지에서 제공을 합니다.
패키지에서 제공하는 것 만으로는 충분하지 않기 때문에 직접 추가해서 사용하는 경우가 많습니다.
  • 설치
import nltk
nltk.download('stopwords')

 

2.4) Stemming 과 Lemmatization

 

  • 동일한 단어가 과거나 현재 또는 단수와 복수 그리고 진행형 등 많은 조건에 따라 단어가 변경됨
  • 동일한 의미를 가지고 있는단어의 어근을 찾아서 사용해야 하는 경우가 발생
  • 단어의 어근을 찾아주는 API로 Stemming 과 Lemmatization이 제공
Stemming은 원형 단어로 변환할 때 일반적인 적용을 하거나 더 단순화된 방법을 적용해서 원래 단어에서 일부 철자가 훼손된 어근 단어를 추출하는 경향이 있음
Lemmatization은 품사와 같은 문법적인 요소와 더 의미적인 부분을 감안해서 어근을 찾아주기 때문에 더 정교
  • nltk에서는 Stemmer를 위해서 Porter, Lancaster, Snowball Stemmer를 제공하고 Lemmatization을 위해서는 WordNetLemmatization을 제공
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('happy'), stemmer.stem('happiest'))
print(stemmer.stem('fancier'), stemmer.stem('fanciest'))


## 동사는 잘하는데 부사에서는 성능이 별로


##result
work work work
amus amus amus
happy happiest
fant fanciest

 

from nltk.stem import WordNetLemmatizer
import nltk
nltk.download('wordnet')
stemmer = WordNetLemmatizer()

print(stemmer.lemmatize('working', 'v'), stemmer.lemmatize('works','v'), stemmer.lemmatize('worked','v'))
print(stemmer.lemmatize('amusing','v'), stemmer.lemmatize('amuses','v'), stemmer.lemmatize('amused','v'))
print(stemmer.lemmatize('happy','a'), stemmer.lemmatize('happiest','a'))
print(stemmer.lemmatize('fancier','a'), stemmer.lemmatize('fanciest','a'))

##result
work work work
amuse amuse amuse
happy happy
fancy fancy

 

2.5) 텍스트 피처화 - BoW

  • Bag of Words 의 약자로 문서가 가지는 모든 단어를 문맥이나 순서를 무시하고 일괄적으로 단어에 대해 빈도 값을 부여해 피처 값을 추출하는 모델
My wife likes to watch baseball games and my daughter likes to watch baseball games too
My wife likes to play baseball
위의 문장에서 중복을 제거한 모든 단어를 추출

My:0 wife:1 likes:2 to:3 watch:4 baseball:5 games:6 and:7 daughter:8 too:9 play:10

0 -> 2
1 -> 1
2 -> 2
3 -> 2
4 -> 2
5 -> 2
6 -> 2
7 -> 1
8 -> 1
9 -> 1
10 -> 0

이러한 형태를 bag of words 라고 함

 

  • 장점 : 쉽고 빠른 구축
  • 단점 : 문맥의 의미를 반영하지 않음 - 단어의 순서를 고려하지 않음, 실제 여러 개의 문장이 존재한다면 각 단어는 문장에 포함되지 않을 가능성이 높기 때문에 0이 아주 많아지게 되고 이렇게 0이 많으면 대다수의 경우 희소 행렬(Sparse Matrix) 로 표현하게 되는데 희소 행렬은 대다수의 ML 알고리즘의 성능을 떨어뜨립니다.
  • BOW 피처 벡터화 : 숫자의 배열로 변환
카운트 기반의 벡터화 : 단어 피처에 값을 부여할 때 각 문서에서 단어가 등장한 횟수를 부여 - word cloud에서 주로 이용
TD-IDF(Term Frequency - Inverse Document Frequency) 기반의 벡터화 : 개별 문서에서 자주 등장하는 단어에는 가중치를 부여하고 여러 문서에서 자주 등장하는 단어는 패널티를 부여하는 방식
가중치 = 개별 문서에서의 빈도 * (log(문서 개수/ 단어를 가지고 있는 문서 개수))

 

3. Word Cloud

3.1) Word Cloud 또는 Tag Cloud

  • 태그 클라우드(영어: tag cloud) 또는 워드 클라우드(word cloud)는 메타 데이터에서 얻어진 태그들을 분석하여 중요도나 인기도 등을 고려하여 시각적으로 늘어 놓아 표시하는 것
  • 보통은 2차원의 표와 같은 형태로 태그들이 배치되며 이때 순서는 알파벳/가나다 순으로 배 치되는데 시각적인 중요도를 강조하기 위해 각 태그들은 그 중요도(혹은 인기도)에 따라 글 자의 색상이나 굵기를 다르게 해서 표시
  • Python에서는 여러 가지 패키지를 제공하는데 tagcloud 나 wordcloud 패키지가 많이 사용

3.2) TagCloud 패키지 이용

  • 패키지 설치
pytagcloud
pygame
simplejson
  • 작업 순서
단어의 list를 생성
Collections 모듈의 Counter를 이용해서 각 단어의 개수를 가지는 Counter 객체를 생성
Counter 객체의 most_commons(사용할 데이터 개수)를 호출해서 단어와 단어 별 개수를 튜플로 갖는 list를 생성
pytacloud.make_tags(태그 목록, maxsize = 최대 크기)
pytacloud.create_tag_image(앞에서 만들어진 결과, 이미지 파일 경로, fontname="한국어를 출력하고자 하는 경우", rectangular=사각형 여부)

 

  • 한글 폰트를 사용하고자 하는 경우
폰트 파일을 pytagcloud 가 설치된 곳의 fonts 디렉토리에 복사 
MS-Windows: 아나콘다 설치 디렉토리\Lib\site-packages\pytagcloud\fontsfont.json 파일에 폰트를 등록을 해야 함.
[{
"name": “폰트이름",
"ttf": "폰트파일경로",
"web": "웹 글꼴 경로"
},
{
"name": "Nobile",
"ttf": "nobile.ttf"
import pytagcloud
import collections

#데이터 생성
nouns = list()
nouns.extend(['불고기' for t in range(8)])
nouns.extend(['비빔밥' for t in range(7)])
nouns.extend(['김치찌개' for t in range(7)])
nouns.extend(['돈까스' for t in range(6)])
nouns.extend(['순두부백반' for t in range(6)])
nouns.extend(['짬뽕' for t in range(6)])
nouns.extend(['짜장면' for t in range(6)])
nouns.extend(['삼겹살' for t in range(5)])
nouns.extend(['초밥' for t in range(5)])
nouns.extend(['우동' for t in range(5)])

#데이터 개수 세기
count = collections.Counter(nouns)
tag2 = count.most_common(100)

#태그 목록 만들기
taglist = pytagcloud.make_tags(tag2, maxsize=50)
print(taglist)

#태그 클라우드 생성
pytagcloud.create_tag_image(taglist, 'wordcloud.png', size=(900, 600), fontname='Korean', rectangular=False)

## result
pygame 2.5.1 (SDL 2.28.2, Python 3.10.9)
Hello from the pygame community. https://www.pygame.org/contribute.html
[{'color': (158, 74, 60), 'size': 106, 'tag': '불고기'}, {'color': (33, 115, 74), 'size': 95, 'tag': '비빔밥'}, {'color': (159, 57, 204), 'size': 95, 'tag': '김치찌개'}, {'color': (206, 184, 120), 'size': 84, 'tag': '돈까스'}, {'color': (218, 159, 55), 'size': 84, 'tag': '순두부백반'}, {'color': (83, 49, 169), 'size': 84, 'tag': '짬뽕'}, {'color': (44, 220, 41), 'size': 84, 'tag': '짜장면'}, {'color': (58, 180, 45), 'size': 73, 'tag': '삼겹살'}, {'color': (177, 101, 22), 'size': 73, 'tag': '초밥'}, {'color': (173, 95, 200), 'size': 73, 'tag': '우동'}]

 

3.3) wordcloud 패지키 이용

  • 이미지를 먼저 배경으로 만들고 이미지 위에 워드클라우드를 작성하는 것이 가능
  • 설치 : pip install wordcloud
from wordcloud import WordCloud, STOPWORDS
from PIL import Image

#이미지 출력
mask = np.array(Image.open('./python_opencv-main/data/appleBar.png'))
plt.figure(figsize=(8,8))
plt.imshow(mask, cmap=plt.cm.gray, interpolation='bilinear')
plt.axis('off')
plt.show()

##이미지 출력
#문자열 생성
text = ''
for t in range(8):
    text = text + 'Python '
for t in range(7):
    text = text + 'Java '
for t in range(7):
    text = text + 'C '
for t in range(8):
    text = text + 'JavaScript '
for t in range(5):
    text = text + 'C# '
for t in range(3):
    text = text + 'Ruby '
for t in range(2):
    text = text + 'scala '
for t in range(6):
    text = text + 'PHP '
for t in range(3):
    text = text + 'Swift '
for t in range(3):
    text = text + 'Kotlin ' 

#제거할 단어 설정
stopwords = set(STOPWORDS)
stopwords.add("Kotlin")

#워드 클라우드 만들기
wordcloud = WordCloud(background_color='white', max_words=2000, mask=mask,
              stopwords = stopwords)
#특수문자를 무시하므로 C 와 C#을 동일한 단어로 판단
wordcloud = wordcloud.generate(text)
wordcloud.words_


##result

{'C': 1.0,
 'Python': 0.6666666666666666,
 'JavaScript': 0.6666666666666666,
 'Java': 0.5833333333333334,
 'PHP': 0.5,
 'Ruby': 0.25,
 'Swift': 0.25,
 'scala': 0.16666666666666666}
#워드 클라우드 화면 출력
plt.figure(figsize=(12,12))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis('off')
plt.show()

  • 동아일보 혹은 그외 기사를 검색해서 크롤링 후 워드클라우드로 작성
정적 텍스트는 BeautifulSoup 만 있으면 크롤링하여 사용이 가능
동적 텍스트(로그인을 해야 보여진다던지 또는 ajax 형태로 데이터)를 읽어오는 경우에는 Selenuim 같은 별도의 패키지를 읽어오는 경우에는 Selenium 같은 별도의 패키지를 활용해야 함.
  • 크롤링을 할 때는 URL 패턴을 확인
#URL에서 파라미터 부분은 반드시 인코딩이 되어야 함.
https://www.donga.com/news/search?query=%EA%B9%80%EB%82%A8%EA%B5%AD (김남국)
더보기 : &sorting=1&check_news=91&search_date=1&v1=&v2=&more=1

1페이지 : https://www.donga.com/news/search?p=1&query=%EA%B9%80%EB%82%A8%EA%B5%AD&check_news=91&more=1&sorting=1&search_date=1&v1=&v2=
2페이지 : https://www.donga.com/news/search?p=16&query=%EA%B9%80%EB%82%A8%EA%B5%AD&check_news=91&more=1&sorting=1&search_date=1&v1=&v2=

#p 파라미터와 query 파라미터가 중요

#p 파라미터는 기사의 일련 번호 - 1, 16, 31 .... 15단위 증가

#query 파라미터가 검색어

1 방법 :  https://www.donga.com/news/searchp=1 + &query=%EA%B9%80%EB%82%A8%EA%B5%AD + &check_news=91&more=1&sorting=1&search_date=1&v1=&v2=

 2 방법 : https://www.donga.com/news/searchp=1 + &check_news=91&more=1&sorting=1&search_date=1&v1=&v2=&query=%EA%B9%80%EB%82%A8%EA%B5%AD&

 

page_num = int(int(input("읽어올 기사의 개수:")) / 15 + 0.95) 
print(page_num)

## 다운로드 받은 텍스트를 저장할 파일을 개방
output_file = open(keyword + ".txt", 'w', encoding="utf8")

for i in range(int(page_num)):
    current_page_num = 1 + i * 15
    target_URL = "http://news.donga.com/search?p=" + str(current_page_num) +  '&query=' + quote(
        keyword) + '&check_news=91&more=1&sorting=1&search_date=1&v1=&v2='
    print(target_URL)
    source_code_from_URL = requests.get(target_URL)
    #기사의 링크를 가져와야 함.
    bs = BeautifulSoup(source_code_from_URL.text, 'html.parser')
    links = bs.select('span.tit > a')
    for title in links:
        title_link = title['href']
        
        source_code = requests.get(title_link)
        bs = BeautifulSoup(source_code.text, 'html.parser')
        content_of_article = bs.select('#article_txt')
        if content_of_article != None :
            print(content_of_article)
            for item in content_of_article:
                string_item = str(item.find_all(text=True))
                output_file.write(string_item)
            
output_file.close()
  • 불러온 기사를 텍스트 파일에 저장

 

  • 크롤링 기사 한국어 형태소 분석
from konlpy.tag import Twitter
open_text_file = open(keyword + ".txt", 'r', encoding="utf8") ##읽기모드
text = open_text_file.read()
spliter = Twitter()
nouns = spliter.nouns(text)
open_text_file.close()
print(nouns)
from konlpy.tag import Twitter
open_text_file = open(keyword + ".txt", 'r', encoding="utf8") ##읽기모드
text = open_text_file.read()
spliter = Twitter()
nouns = spliter.nouns(text)
open_text_file.close()
print(nouns)
import nltk
ko = nltk.Text(nouns, name='김남국')
print('전체 단어 개수:', len(ko.tokens))
print('중복 제거 후 개수:', len(set(ko.tokens)))
#단어 별 등장 횟수
print(ko.vocab())

plt.figure(figsize=(12,6))
ko.plot(50)
plt.show()

#불용어 제거
stop_words = ['것','의원','김','대표','이','고','공유','전','위','기자','기사','등', '힘','당',
              '며', '구독','날','그','말','명','거','중','김남국','안','수']

ko = [each_word for each_word in ko if each_word not in stop_words and len(each_word) != 1] #한글자 제외
print(ko)
  • 워드 클라우드
data = ko.vocab().most_common(150)
    
wordcloud = WordCloud(font_path = 'C:/Users/USER/anaconda3/Lib/site-packages/pytagcloud/fonts/incheon.ttf',
                      relative_scaling = 0.5,
                      background_color='white',
                      ).generate_from_frequencies(dict(data))
plt.figure(figsize=(12,8))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()

  • 제 지인인 김남국 의원에 대해서 해보았습니다
import numpy as np
mask = np.array(Image.open('./python_opencv-main/data/appleBar.png'))
data = ko.vocab().most_common(100)
wordcloud = WordCloud(font_path='C:/Users/USER/anaconda3/Lib/site-packages/pytagcloud/fonts/incheon.ttf',
                      relative_scaling = 0.5,
                      background_color='white',
                      mask=mask,
                      ).generate_from_frequencies(dict(data))
%matplotlib inline
plt.figure(figsize=(12,8))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()
  • 워드 클라우드에 사진입히기


4. 텍스트 분류

4.1) 뉴스 데이터 분류

  • sklearn 에서는 fetch_20newsgroups 라는 API를 이용해서 뉴스 그룹의 분류를 수행해 볼 수 있는 예제를 제공
  • 텍스트를 피처 벡터화하면 일반적으로 희소 행렬 형태가 되고 이러한 희소 행렬의 데이터를 가지고 분류를 잘 하는 알고리즘은 로지스틱 회귀, 선형 서포트 벡터 머신, 나이브 베이즈 등
  • 텍스트를 가지고 분류를 할 때는 먼저 텍스트를 정규화(전처리)를 수행하고 피처 벡터화를 하고 그 이후에 머신 러닝 알고리즘을 적용
#데이터 가져오기
from sklearn.datasets import fetch_20newsgroups

news_data = fetch_20newsgroups(subset='all',random_state=156)
print(news_data.keys())
#result

dict_keys(['data', 'filenames', 'target_names', 'target', 'DESCR'])
import pandas as pd
#분포 확인 - 분포가 한쪽으로 치우치게 되면 데이터를 층화 추출을 할 것인지 아니면 오버 샘플링이나 언더 샘플링인지 또는
#로그 변환을 할 것인지 선택
print('target 클래스의 값과 분포도 \n',pd.Series(news_data.target).value_counts().sort_index())
print('target 클래스의 이름들 \n',news_data.target_names)


##result
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']
  • 실제 기사 확인
print(news_data.data[0])

##result
From: egreen@east.sun.com (Ed Green - Pixel Cruncher)
Subject: Re: Observation re: helmets
Organization: Sun Microsystems, RTP, NC
Lines: 21
Distribution: world
Reply-To: egreen@east.sun.com
NNTP-Posting-Host: laser.east.sun.com

In article 211353@mavenry.altcit.eskimo.com, maven@mavenry.altcit.eskimo.com (Norman Hamer) 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, but not in a
small one).

---
Ed Green, former Ninjaite |I was drinking last night with a biker,
  Ed.Green@East.Sun.COM   |and I showed him a picture of you.  I said,
DoD #0111  (919)460-8302  |"Go on, get to know her, you'll like her!"
 (The Grateful Dead) -->  |It seemed like the least I could do...

 

  • 불필요한 부분 제거 필요 + 훈련 셋, 테스트 셋 분리
  • 이 경우, remove 옵션에 headers, footers, quotes를 설정하면 텍스트만 넘어옴.
#테스트 데이터 생성
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)))
  • 피처 벡터화 - 문자열을 벡터화
문자열 데이터를 가지고 데이터의 개수를 기반으로 벡터화 -sklearn.feature_extration.text.CountVectorizer
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)

 

  • 로지스틱 회귀 적용
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

# LogisticRegression을 이용하여 학습/예측/평가 수행. 
lr_clf = LogisticRegression(solver = "lbfgs", max_iter=1000)
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)))


##result

CountVectorized Logistic Regression 의 예측 정확도는 0.607
  • 피처 벡터화를 할 때, TD-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()
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)))


## result
TF-IDF Logistic Regression 의 예측 정확도는 0.674

## 정확도가 높아짐을 알 수 있음
  • TF-IDF를 이용할 때 파라미터를 설정
# 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 ,pr

##result
TF-IDF Vectorized Logistic Regression 의 예측 정확도는 0.692

 


5. 감성분석

5.1) 개요

  • 감성 분석(Sentiment Analysis)은 문서의 주관적인 감성/의견/감정/기분 등을 파악하기 위한 방법
  • 문서 텍스트가 나타내는 여러 가지 주관적인 단어와 문맥을 기반으로 감성 수치를 계산
  • 감성 수치는 긍정과 부정으로 나뉘며 이들 지수를 합산해서 긍정 또는 부정으로 결정
  • 지도 학습과 비지도 학습 모두 가능
비지도 학습의 경우는 영문의 경우는 Lexicon 이라는 감성 어휘 사전을 이용
한국어의 경우는 Google의 BERT 나 KoBERT 또는 기업에서 제공하는 pre-trained 모델을 이용할 수 있습니다.
  • 나이브 베이즈(확률 기반) 분류를 많이 사용

5.2) 나이브 베이즈 분류기를 이용한 감성 분석

  • 샘플 데이터 생성 - 직접 크롤링 해서 수행이 가능한데, 평점이 제공되는 경우는 평점에 threshold를 설정해서 긍정과 부정으로 나누고 분류하는 것도 가능
#훈련 데이터 생성
### 단어별 분류를 위한 패키지 import
from nltk.tokenize import word_tokenize
import nltk

### 훈련 데이터 만들기
train = [('i like you', 'pos'), 
         ('i do not like you', 'neg'),
         ('i hate you', 'neg'), 
         ('i do not hate you', 'pos'),
        ('i love you', 'pos'),
        ('I do not love you', 'neg')]
all_words = set(word.lower() for sentence in train 
            for word in word_tokenize(sentence[0]))
all_words

##result
{'do', 'hate', 'i', 'like', 'love', 'not', 'you'}

#분류를 위한 준비

#단어 토큰화
t = [({word: (word in word_tokenize(x[0])) for word in all_words}, x[1])
                                                        for x in train]
print(t)

#분류기 만들기
classifier = nltk.NaiveBayesClassifier.train(t)
classifier.show_most_informative_features()

##result
[({'hate': False, 'like': True, 'i': True, 'love': False, 'not': False, 'you': True, 'do': False}, 'pos'), ({'hate': False, 'like': True, 'i': True, 'love': False, 'not': True, 'you': True, 'do': True}, 'neg'), ({'hate': True, 'like': False, 'i': True, 'love': False, 'not': False, 'you': True, 'do': False}, 'neg'), ({'hate': True, 'like': False, 'i': True, 'love': False, 'not': True, 'you': True, 'do': True}, 'pos'), ({'hate': False, 'like': False, 'i': True, 'love': True, 'not': False, 'you': True, 'do': False}, 'pos'), ({'hate': False, 'like': False, 'i': False, 'love': True, 'not': True, 'you': True, 'do': True}, 'neg')]
Most Informative Features
                      do = False             pos : neg    =      1.7 : 1.0
                      do = True              neg : pos    =      1.7 : 1.0
                     not = False             pos : neg    =      1.7 : 1.0
                     not = True              neg : pos    =      1.7 : 1.0
                       i = True              pos : neg    =      1.4 : 1.0
                    hate = False             neg : pos    =      1.0 : 1.0
                    hate = True              neg : pos    =      1.0 : 1.0
                    like = False             neg : pos    =      1.0 : 1.0
                    like = True              neg : pos    =      1.0 : 1.0

#예측해보

#샘플 문장 테스트 
test_sentence = 'i do not like jessica'
test_sent_features = {word.lower():(word in word_tokenize(test_sentence.lower()))
                     for word in all_words}
print(test_sent_features)
print(classifier.classify(test_sent_features))

##result
{'hate': False, 'like': True, 'i': True, 'love': False, 'not': True, 'you': False, 'do': True}
neg

 

5.3) 한글 감성 분석

  • 한글을 이용할 때는 형태소 분석을 수행을 해서 품사를 같이 이용하는 것이 좋습니다.
#샘플 문장 생성
from konlpy.tag import Twitter
twitter = Twitter()
train = [('나는 당신을 사랑합니다', 'pos'), 
         ('나는 당신을 사랑하지 않아요', 'neg'),
         ('나는 당신을 만나는 것이 지루합니다', 'neg'),
         ('나는 당신을 만나는 것이 지루하지 않습니다', 'pos'),
         ('나는 당신이 좋습니다', 'pos'),
         ('나는 당신이 좋지 않습니다', 'neg'),
         ('나는 당신과 노는 것이 즐겁습니다', 'pos'),
         ('나는 당신과 노는 것이 즐겁지 않습니다', 'neg'),
        ('나는 제시카와 함께 있는 것이 즐겁습니다', 'pos'),
        ('나는 일을 하는 것이 즐겁지 않습니다', 'neg'),
         ('나는 일이 너무 힘들어', 'neg')]
         
#단순 단어 분류 – 조사가 다른 경우 다른 단어로 구분됨
all_words = set(word.lower() for sentence in train
                        for word in word_tokenize(sentence[0]))
print(all_words)

##result
{'좋습니다', '일을', '나는', '사랑하지', '지루합니다', '않습니다', '즐겁지',
'당신을', '노는', '지루하지', '당신과', '함께', '사랑합니다', '힘들어', '너무',
'일이', '있는', '당신이', '제시카와', '것이', '즐겁습니다', '않아요', '좋지', '하는', '만나는'}

 

5.4) IMDB 영화평 데이터를 이용한 지도 학습 기반 감성 분석

  • 데이터 : https://www.kaggle.com/competitions/word2vec-nlp-tutorial/data에서 다운로드
  • 데이터 불러오기 및 불필요한 데이터 제거
import pandas as pd

review_df = pd.read_csv('.//labeledTrainData.tsv', header=0, sep="\t", quoting=3)
review_df.head(3)

#정규식 모듈
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

##result
((17500, 1), (7500, 1))



#로지스틱 회귀를 이용한 훈련 과 평가 지표 확인
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(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)))
##result
예측 정확도는 0.8860, ROC-AUC는 0.9503



#전처리 작업을 수행 한 후 학습
# 스톱 워드는 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)))

##result
예측 정확도는 0.8936, ROC-AUC는 0.9598

 

5.5) 비지도 학습 기반의 감성 분석

  • 감성 분석 사전을 이용
  • 영문의 경우는 Lexicon 이라는 사전이 존재
  • 한글의 경우는 직접 만들거나 미리 학습된 모델을 이용

 

5.6) 네이버 식당 리뷰 데이터를 이용한 한글 지도학습 기반의 감성 분석

  • score는 평점, review는 리뷰, y는 감성인데 score가 4이상이면 긍정(1), 그렇지 않으면 부정(0)
  • 데이터 불러오기
df = pd.read_csv("./review_data.csv")
print(df.head())

import re
# 텍스트 정제 함수 : 한글 이외의 문자는 전부 제거
def text_cleaning(text):
    # 한글의 정규표현식으로 한글만 추출합니다.
    hangul = re.compile('[^ ㄱ-ㅣ가-힣]+')
    result = hangul.sub('', text)
    return result
from konlpy.tag import Okt

# konlpy라이브러리로 텍스트 데이터에서 형태소를 추출합니다.
def get_pos(x):
    tagger = Okt()
    pos = tagger.pos(x)
    pos = ['{}/{}'.format(word,tag) for word, tag in pos]
    return pos

# 형태소 추출 동작을 테스트합니다.
result = get_pos(df['ko_text'][0])
print(result)
['친절하시고/Adjective', '깔끔하고/Adjective', '좋았습니다/Adjective']
#학습 데이터 생성

from sklearn.feature_extraction.text import CountVectorizer

# 형태소를 벡터 형태의 학습 데이터셋(X 데이터)으로 변환
index_vectorizer = CountVectorizer(tokenizer = lambda x: get_pos(x))
X = index_vectorizer.fit_transform(df['ko_text'].tolist())

X.shape


##result
(545, 3030)

 

  • 분류 진행 
print(str(index_vectorizer.vocabulary_)[:100]+"..")
#{'친절하시고/Adjective': 2647, '깔끔하고/Adjective': 428, '좋았습니다/Adjective': 2403, '조용하고/Adjective': 2356, '고..
print(df['ko_text'][0])
print(X[0])
#친절하시고 깔끔하고 좋았습니다
#  (0, 2647)	1
#  (0, 428)	1
#  (0, 2403)	1

 

 

#TF-IDF 변환
from sklearn.feature_extraction.text import TfidfTransformer
​
# TF-IDF 방법으로, 형태소를 벡터 형태의 학습 데이터셋(X 데이터)으로 변환합니다.
tfidf_vectorizer = TfidfTransformer()
X = tfidf_vectorizer.fit_transform(X)
​
print(X.shape)
print(X[0])
#(545, 3030)
#  (0, 2647)	0.5548708693511647
#  (0, 2403)	0.48955631270748484
#  (0, 428)	0.6726462183300624


#학습용 데이터 생성
from sklearn.model_selection import train_test_split
​
y = df['y']
x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.30)
print(x_train.shape)
print(x_test.shape)
#(381, 3030)
#(164, 3030)


#분류 훈련
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
​
# 로지스틱 회귀모델을 학습합니다.
lr = LogisticRegression(random_state=0)
lr.fit(x_train, y_train)
y_pred = lr.predict(x_test)
y_pred_probability = lr.predict_proba(x_test)[:,1]
​
# 로지스틱 회귀모델의 성능을 평가합니다.
print("accuracy: %.2f" % accuracy_score(y_test, y_pred))
print("Precision : %.3f" % precision_score(y_test, y_pred))
print("Recall : %.3f" % recall_score(y_test, y_pred))
print("F1 : %.3f" % f1_score(y_test, y_pred))
#accuracy: 0.91
#Precision : 0.915
#Recall : 1.000
#F1 : 0.955


from sklearn.metrics import confusion_matrix
# Confusion Matrix를 출력합니다.
confmat = confusion_matrix(y_true=y_test, y_pred=y_pred)
print(confmat)
#[[  0  14]
# [  0 150]]


from matplotlib import plt 
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt
# AUC를 계산합니다.
false_positive_rate, true_positive_rate, thresholds = roc_curve(y_test, y_pred_probability)
roc_auc = roc_auc_score(y_test, y_pred_probability)
print("AUC : %.3f" % roc_auc)
​
# ROC curve 그래프를 출력합니다.
plt.rcParams['figure.figsize'] = [5, 4]
plt.plot(false_positive_rate, true_positive_rate, label='ROC curve (area = %0.3f)' % roc_auc, 
         color='red', linewidth=4.0)
plt.plot([0, 1], [0, 1], 'k--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.0])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC curve of Logistic regression')
plt.legend(loc="lower right")
#AUC : 0.903

  • ROC_AUC 값을 그래프로 출력 : 특정 시점에서 곡선이 만들어지지 않고 계단처럼 보이는 현상이 발생
  • 이런 경우는 대부분 데이터의 불균형 때문
  • 1인 데이터가 492개, 0인 데이터가 53개
  • UnderSampling - 비율이 높은 샘플의 비율을 낮추는 것
  • 크롤링해서 데이터를 만드는 경우 데이터의 비율을 맞추는 것이 중요합니다.
리뷰의 경우는 긍정적인 리뷰가 많습니다
부정적인 데이터가 부족해서 감성 분류를 하게되면 부정이 나오는 경우가 드뭅니다.
정확도는 높게 나올 가능성이 높습니다.

6. 토픽 모델링

6.1) 개요

  • 문서 집합에 숨어있는 주제를 찾아내는 것
  • 머신러닝 기반의 토픽 모델은 숨겨진 주제를 효과적으로 표현할 수 있는 중심 단어를 함축적으로 추출
  • 군집처럼 몇 개로 분류 할 것인지를 설정하면 그룹을 나누어서 중심 단어를 찾아줍니다.
  • sklearn 에서는 LDA(Latent Dirichlet Allocation) 기반의 토픽 모델링을 수행해주는 LatentDirichletAllocation 클래스를 제공
  • 중요 단어를 추출하기 때문에 개수 기반의 피처 벡터화만 이용해도 됩니다.

 

6.2) sklearn의 뉴스데이터 불러와서 분석

차원 축소의 개념 :
피처 선택 - 중요한 피처만 선택
새로운 피처 추출 - 여러 개의 피처의 상관 관계를 확인해서 여러 개의 피처에서 작은 개수의 피처를 추출(주성분 분석)
  • 텍스트 벡터화
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'  ]

# 위에서 cats 변수로 기재된 category만 추출. featch_20newsgroups( )의 categories에 cats 입력
news_df= fetch_20newsgroups(subset='all',remove=('headers', 'footers', 'quotes'), 
                            categories=cats, random_state=42)

#LDA 는 Count기반의 Vectorizer만 적용합니다.  
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)

##result
CountVectorizer Shape: (7862, 1000)
#토픽별로 연관도가 높은 순으로 나열

def display_topics(model, feature_names, no_top_words):
    for topic_index, topic in enumerate(model.components_):
        print('Topic #',topic_index)

        # components_ array에서 가장 값이 큰 순으로 정렬했을 때, 그 값의 array index를 반환. 
        topic_word_indexes = topic.argsort()[::-1]
        top_indexes=topic_word_indexes[:no_top_words]
        
        # top_indexes대상인 index별로 feature_names에 해당하는 word feature 추출 후 join으로 concat
        feature_concat = ' '.join([feature_names[i] for i in top_indexes])                
        print(feature_concat)

# CountVectorizer객체내의 전체 word들의 명칭을 get_features_names( )를 통해 추출
feature_names = count_vect.get_feature_names_out()

# Topic별 가장 연관도가 높은 word를 15개만 추출
display_topics(lda, feature_names, 15)

 

 


 

7. 군집

7.1) 문서의 군집

  • 일반 군집과 알고리즘 자체는 동일한데 문장을 가지고 수행되므로 피처 벡터화를 수행하고 거리를 계산해서 수행한다는 것이 다른 점

7.2) 문서의 군집 실습

  • https://archive.ics.uci.edu/dataset/191/opinosis+opinion+frasl+review
  • 특정 디렉토리 내의 모든 파일의 내용을 읽어서 하나로 만드는 것
  • 데이터 만들기
import pandas as pd
import glob ,os

path = './data/OpinosisDataset1.0/topics'                     
# path로 지정한 디렉토리 밑에 있는 모든 .data 파일들의 파일명을 리스트로 취합
all_files = glob.glob(os.path.join(path, "*.data"))
filename_list = []
opinion_text = []

# 개별 파일들의 파일명은 filename_list 리스트로 취합, 
# 개별 파일들의 파일내용은 DataFrame로딩 후 다시 string으로 변환하여 opinion_text 리스트로 취합 
for file_ in all_files:
    # 개별 파일을 읽어서 DataFrame으로 생성 
    df = pd.read_table(file_,index_col=None, header=0,encoding='latin1')
    
    # 절대경로로 주어진 file 명을 가공. 만일 Windows에서 수행시에는 아래 /를 \\ 변경. 맨 마지막 .data 확장자도 제거
    filename_ = file_.split('/')[-1]
    filename = filename_.split('.')[0]

    #파일명 리스트와 파일내용 리스트에 파일명과 파일 내용을 추가. 
    filename_list.append(filename)
    opinion_text.append(df.to_string())

# 파일명 리스트와 파일내용 리스트를  DataFrame으로 생성
document_df = pd.DataFrame({'filename':filename_list, 'opinion_text':opinion_text})
print(document_df.head())

 

  • 군집 알고리즘(K-Means) 수행
from sklearn.cluster import KMeans

# 5개 집합으로 군집화 수행
km_cluster = KMeans(n_clusters=5, max_iter=10000, random_state=0)
km_cluster.fit(feature_vect)
cluster_label = km_cluster.labels_
cluster_centers = km_cluster.cluster_centers_

 


 

8. 문장의 유사도 측정

  • 각 문장을 벡터로 만들어서 거리를 측정하는 개념을 이용 - 코사인 유사도
  • 코사인 유사도는 일반적인 거리 측정의 개념과는 다르고 방향성을 중요시하는 개념입니다.
  • 두 개의 벡터 간의 사잇각을 구해서 얼마나 유사한지 수치로 적용한 것
  • sklearn에서 코사인 유사도를 계산하는 방법
sklearn.feature_extraction.text 패키지를 이용해서 피처 벡터화를 하고 sklearn.metircs.pairwise 패키지의 cosine_similarity 함수를 이용해서 측정
  • 알고리즘 자체는 
import numpy as np

def cos_similarity(v1, v2):
    dot_product = np.dot(v1, v2)
    l2_norm = (np.sqrt(sum(np.square(v1))) * np.sqrt(sum(np.square(v2))))
    similarity = dot_product / l2_norm     
    
    return similarity
  • 리뷰 문서에서 유사도 측정
km_cluster = KMeans(n_clusters=3, max_iter=10000, random_state=0)
km_cluster.fit(feature_vect)
cluster_label = km_cluster.labels_
cluster_centers = km_cluster.cluster_centers_
document_df['cluster_label'] = cluster_label

from sklearn.metrics.pairwise import cosine_similarity

# cluster_label=1인 데이터는 호텔로 클러스터링된 데이터임. DataFrame에서 해당 Index를 추출
hotel_indexes = document_df[document_df['cluster_label']==1].index
print('호텔로 클러스터링 된 문서들의 DataFrame Index:', hotel_indexes)

# 호텔로 클러스터링된 데이터 중 첫번째 문서를 추출하여 파일명 표시.  
comparison_docname = document_df.iloc[hotel_indexes[0]]['filename']
print('##### 비교 기준 문서명 ',comparison_docname,' 와 타 문서 유사도######')

''' document_df에서 추출한 Index 객체를 feature_vect로 입력하여 호텔 클러스터링된 feature_vect 추출 
이를 이용하여 호텔로 클러스터링된 문서 중 첫번째 문서와 다른 문서간의 코사인 유사도 측정.'''
similarity_pair = cosine_similarity(feature_vect[hotel_indexes[0]] , feature_vect[hotel_indexes])
print(similarity_pair)

##result
호텔로 클러스터링 된 문서들의 DataFrame Index: Int64Index([6, 7, 16, 17, 18, 22, 25, 29, 37, 47], dtype='int64')
##### 비교 기준 문서명  topics\comfort_honda_accord_2008  와 타 문서 유사도######
[[1.         0.83969704 0.15655631 0.33044002 0.25981841 0.16544257
  0.27569738 0.18050974 0.65502034 0.06229873]]

 

  • 한글 문서의 유사도 측정 - 한글은 형태소 분석을 수행해야 합니다.
### 훈련 데이터 만들기
from sklearn.feature_extraction.text import CountVectorizer
from konlpy.tag import Twitter
twitter = Twitter()
vectorizer = CountVectorizer(min_df = 1)
contents = ['우리 과일 먹으로 가자',
                   '나는 고기를 좋아합니다',
                   '나는 공원에서 산책하는 것을 싫어합니다',
                   '안녕하세요 반갑습니다 그동안 잘 계셨어요']
contents_tokens = [twitter.morphs(row) for row in contents]
print(contents_tokens)

 

contents_for_vectorize = []

for content in contents_tokens:
    sentence = ''
    for word in content:
        sentence = sentence + ' ' + word
        
    contents_for_vectorize.append(sentence)
    
print(contents_for_vectorize)

 

  • 피처 벡터화
### 훈련 데이터의 차원 확인
X = vectorizer.fit_transform(contents_for_vectorize)
num_samples, num_features = X.shape
print(num_samples, num_features)

### 훈련 데이터의 확인
print(vectorizer.get_feature_names_out())

### 훈련 데이터의 벡터 값 확인
print(X.toarray().transpose())

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형
LIST