스터디/머신러닝

텍스트 데이터 다루기

뀨린 2021. 5. 5. 16:23

'파이썬 라이브러리를 활용한 머신러닝' 책으로 공부한 내용을 바탕으로 작성한 글입니다. 

 

문자열 데이터의 종류

 

  •  범주형 데이터

        고정된 목록으로 구성된다. 

 

         예) 드롭다운 메뉴의 빨강”, 녹색”, 파랑”, 노랑 중 하나를 선택하는 경우

 

  • 범주에 의미를 연결시킬 수 있는 임의의 문자열

        입력 받은 문자를 일정한 범주 안에 포함시킨다.

       

          예) 텍스트 필드에서 쥐색, 회색 등의 답을 입력 받고 이를 여러가지 색 범주에 할당한다.

 

  •  구조화된 문자열 데이터   

        입력한 값들이 일정한 구조를 가진다.

 

  • 텍스트 데이터

    자유로운 절과 문장으로 구성되어 있다.

       

    데이터셋 - 말뭉치

   데이터 포인트 - 문서

 

영화 리뷰 감성 분석하기

 

이 데이터셋은 리뷰 텍스트와 '양성' 혹은 '음성'을 나타내는 레이블을 포함한다.  IMDb 웹사이트에는 1에서 10까지 점수가 있다. 이 데이터셋은 7점 이상은 '양성', 4점 이하는 '음성'인 이진 분류 데이터셋으로 구분된다. 

 

mac OS나 리눅스 사용자는 다음 명령으로 이 데이터를 다운로드하고 압축 해제가 가능하다. 

 

! wget -nc http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz -P data
! tar xzf data/aclImdb_v1.tar.gz --skip-old-files -C data

 

압축을 풀면 두 폴더에 텍스트 파일이 들어 있는데, 하나는 훈련 데이터고, 다른 하나는 텍스트 데이터이다. 이 두 폴더는 다시 pos와 neg 하위폴더를 포함한다. 

 

# !은 셸(shell) 명령을 실행해주는 IPython의 매직 명령어입니다.
# tree 명령이 없다면 find ./data -type d 명령을 사용해 하위 폴더의 목록을 
# 볼 수 있습니다. 윈도에서는 !tree data/aclImdb 와 같이 사용하세요.
# !tree -dL 2 data/aclImdb
!find ./data -type d

output을 확인해보면 unsup 폴더는 레이블이 없는 데이터가 있으므로 삭제 명령어(rm)를 통해 삭제한다.

 

!rm -r data/aclImdb/train/unsup

1. 훈련 데이터를 load_files 함수로 읽어들인다. 

from sklearn.datasets import load_files

reviews_train = load_files("data/aclImdb/train/")
# 텍스트와 레이블을 포함하고 있는 Bunch 오브젝트를 반환합니다.
text_train, y_train = reviews_train.data, reviews_train.target
print("text_train의 타입:", type(text_train))
print("text_train의 길이:", len(text_train))
print("text_train[6]:\n", text_train[6])

결과 분석: text_train 리스트의 길이는 25,000이고 각 항목은 리뷰 한 개에 대한 문자열이다. 앞에서 인덱스가 6인 리뷰를 출력했다. 

 

2. 줄바꿈 태그(<br />) 삭제

text_train = [doc.replace(b"<br />", b" ") for doc in text_train]

print("클래스별 샘플 수 (훈련 데이터):", np.bincount(y_train)) //(1)


reviews_test = load_files("data/aclImdb/test/")
text_test, y_test = reviews_test.data, reviews_test.target
print("테스트 데이터의 문서 수:", len(text_test))
print("클래스별 샘플 수 (테스트 데이터):", np.bincount(y_test))
text_test = [doc.replace(b"<br />", b" ") for doc in text_test]   //(2)

Output

 

(1) 클래스별 샘플 수 (훈련 데이터): [12500 12500]

 

 이 데이터셋은 양성 클래스와 음성클래스를 같은 비율로 수집했기 때문에 양성 레이블과 음성 레이블의 수가 같다.

 

(2) 테스트 데이터의 문서 수: 25000

클래스별 샘플 수 (테스트 데이터): [12500 12500]

 

같은 방식으로 테스트 데이터셋을 읽어들인다. 

 

풀려는 문제: 리뷰의 텍스트 내용을 보고 '양성'인지 '음성'인지 구분하는 것. 이는 이진 분류 문제이다. 그러나 텍스트 데이터는 머신러닝 모델이 다룰 수 있는 형태가 아니다. 그래서 텍스트의 문자열 표현을 머신러닝 알고리즘에 적용할 수 있도록 수치 표현으로 바꿔야 한다. 

 

 

텍스트 데이터를 BOW로 표현하기 

 

BOW란?

 

  • 각 단어가 말뭉치에 있는 텍스트에 얼마나 많이 나타나는지 분석하는 방법
  • BOW 표현의 계산 방법 

      1단계. 토큰화: 각 문서를 문서에 포함된 단어로 나눈다.

 

      2단계. 어휘 사전 구축: 모든 문서에 나타난 모든 단어의 어휘를 모으고 번호를 매긴다.

 

      3단계. 인코딩: 어휘 사전의 단어가 문서마다 몇번 나타나는지 헤아린다.

 

  • 인코딩을 거친 계산의 최종 출력은 각 문서에서 나타난 단어의 횟수가 담긴 하나의 벡터

샘플 데이터에 BOW 적용하기

 

BOW 표현은 CountVectorizer에 변환기 인터페이스로 구현되어 있다! 

 

from sklearn.feature_extraction.text import CountVectorizer

 

👉두 샘플만 포함한 간단한 데이터셋에 적용해 어떻게 작동하는지 살펴보기

bards_words =["The fool doth think he is wise,",
              "but the wise man knows himself to be a fool"]

 

 CounterVectorizer를 임포트하고 객체를 생성해서 샘플 데이터에 fit 메서드 적용                                                 

vect = CountVectorizer()  //객체 생성
vect.fit(bards_words)  //메서드 적용

CounterVecotrizer의 fit 메서드는 훈련 데이터를 토큰으로 나누고, 어휘 사전을 구축해 vocabulry_속성에 저장한다. 

print("어휘 사전의 크기:", len(vect.vocabulary_))
print("어휘 사전의 내용:\n", vect.vocabulary_)

                                                                                                                                                         이 어휘 사전은 "be"에서 "wise"까지 13개의 단어로 구성되어 있다. 

훈련 데이터에 대해 BOW 표현을 만들려면 transform 메서드를 호출한다. 

 

bag_of_words = vect.transform(bards_words) //transform 메서드 사용
print("BOW:", repr(bag_of_words))

BOW 표현은 0이 아닌 값만 저장하는 SciPy 희소 행렬로 저장되어 있다. 

이 행렬의 크기는 2x13인데, 각각의 행은 하나의 데이터 포인트를 나타내고, 각 특성은 어휘 사전에 있는 각 단어에 대응한다. 

 

➰ 왜 희소 행렬을 사용하는가? 

 

대부분의 문서는 어휘 사전에 있는 단어 중 일부만 포함한다. 특성 배열의 대부분의 원소가 0이라서 희소행렬을 사용한다.  값이 0인 원소를 모두 저장하면 메모리 낭비를 일으킬 수 있다. 희소행렬의 실제 내용을 보려면 toarray 메서드를 사용하여 밀집된 NumPy 배열로 바꿔야 한다. 

print("BOW의 밀집 표현:\n", bag_of_words.toarray())

밀집된 Numpy 배열로

                                                                                                                                                       

👉 문장에 나타난 단어의 출현 횟수를 나타낸다.

 

영화 리뷰에 대한 BOW

 

  영화 리뷰에 대한 감성 분석을 적용해보기

 

IMDb 리뷰의 훈련 데이터와 테스트 데이터를 읽어서 문자열 리스트로 변환한다(text_train과 text_test).

 

vect = CountVectorizer().fit(text_train)
X_train = vect.transform(text_train)
print("X_train:\n", repr(X_train))

                                                                                                                                                        훈련 데이터의 BOW 표현인 X_train의 크기는 위와 같고, 이 어휘 사전은 단어를 74,849개만큼 담고 있다. 이 데이터는 SciPy 행렬로 저장되어 있다. 

 

 CountVectorizer 객체의 get_feature_name 메서드로 각 특성에 해당하는 단어를 리스트로 반환해보기                                                                                                                                                                               

feature_names = vect.get_feature_names()  //get_feature_names 메서드 사용
print("특성 개수:", len(feature_names))
print("처음 20개 특성:\n", feature_names[:20])
print("20010에서 20030까지 특성:\n", feature_names[20010:20030])
print("매 2000번째 특성:\n", feature_names[::2000])

  👉 어휘 사전의 처음 20개 중 15개의 항목이 숫자이다. 이 숫자들이 리뷰로 추출되었고, 007과 같은 영화가 아닌 이상 의미가 있는 단어로 추출되진 않았음을 알 수 있다.

 

👉 또한, "dra"로 시작하는 영어 단어의 목록을 확인할 수 있다. "draught", "drawback", "drawer" 모두 단수와 복수형이 서로 다른 단어로 어휘 사전에 포함된다. 의미가 매우 비슷하기 때문에 다른 단어로 포함되는 것은 바람직하지 않다. 단어가 많아질수록, 처리해야하는 연산의 양이 많아지면 성능 또한 느려지니까!

 

👉 특성 추출 방법을 개선해야 한다! 

 

분류기를 만들어 성능 수치 확인해보기 

 

  y_train에 있는 훈련 레이블, X_train에 있는 훈련 데이터의 BOW 표현으로 분류기 학습해보기

 

희소 행렬의 고차원 데이터셋에서는 LogisticRegression 같은 선형 모델의 성능이 가장 뛰어나므로, 교차 검증을 사용해 선형 모델의 성능을 평가해보겠다. 

                                                                                                                                                       

from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression

scores = cross_val_score(LogisticRegression(max_iter=1000), X_train, y_train, cv=5)
print("크로스 밸리데이션 평균 점수: {:.2f}".format(np.mean(scores)))

교차 검증 평균 점수: 0.88

 

👉 교차 검증 평균 점수는 88%, 꽤 괜찮은 이진 분류 성능임을 확인했다. 

 

선형 회귀 모델에는 규제 매개변수 C가 있으므로 그리드 서치를 사용해 조정해보겠다. 

 

from sklearn.model_selection import GridSearchCV  //그리드서치
param_grid = {'C': [0.001, 0.01, 0.1, 1, 10]}
grid = GridSearchCV(LogisticRegression(max_iter=5000), param_grid, cv=5)
grid.fit(X_train, y_train)
print("최상의 크로스 밸리데이션 점수: {:.2f}".format(grid.best_score_))
print("최적의 매개변수: ", grid.best_params_)

최상의 교차 검증 점수: 0.89

최적의 매개변수: {'C': 0.1}

 

  👉 C = 0.1에서 교차 검증 점수 89%, 이 매개변수를 사용해 테스트 세트의 일반화 성능 측정.

 

X_test = vect.transform(text_test)
print("테스트 점수: {:.2f}".format(grid.score(X_test, y_test)))

테스트 점수: 0.88

 

  단어 추출 방법 개선해보기

 

✨ CounterVecotrizer는 모든 단어를 소문자로 바꾸고(herineeda, Herineeda, hErineeda가 다 같은 단어로) 모두 같은 토큰(특성)이 된다. 그러나 의미 없는 특성 또한 많이 생성한다(앞에서의 숫자처럼). 이를 줄이기 위해 두 개의 문서에 나타난 토큰만을 사용한다. 

 

min_df 매개변수로 토큰이 나타날 최소 문서 개수를 지정가능하다. 

 

vect = CountVectorizer(min_df=5).fit(text_train) //최소 문서 개수 5로 지정
X_train = vect.transform(text_train)
print("min_df로 제한한 X_train:", repr(X_train))

                                                                                                                                                              토큰을 적어도 다섯번 이상 나타나야하게 지정했으므로, 기존의 특성의 수보다 1/3만큼 줄었음을 확인할 수 있다. (74,849개 -> 27,271개로)

 

  토큰 내용 살펴보기                                                                                                                               

feature_names = vect.get_feature_names()

print("First 50 features:\n", feature_names[:50])
print("Features 20010 to 20030:\n", feature_names[20010:20030])
print("Every 700th feature:\n", feature_names[::700])

    숫자의 길이가 줄고, 희귀한 단어, 철자가 틀린 단어가 사라졌다. 

 

  그리드 서치를 사용해 모델의 성능 확인해보기

grid = GridSearchCV(LogisticRegression(max_iter=5000), param_grid, cv=5)
grid.fit(X_train, y_train)
print("최적의 크로스 밸리데이션 점수: {:.2f}".format(grid.best_score_))

  최상의 크로스 밸리데이션 점수: 0.89

 

👉👉 모델 성능은 89%로 이전과 동일하지만 특성의 개수가 줄어서 처리 속도가 빨라지고, 불필요한 특성이 없어져 모델이 알아보기 간편해졌다.