텍스트 데이터 다루기
'파이썬 라이브러리를 활용한 머신러닝' 책으로 공부한 내용을 바탕으로 작성한 글입니다.
문자열 데이터의 종류
- 범주형 데이터
고정된 목록으로 구성된다.
예) 드롭다운 메뉴의 “빨강”, “녹색”, “파랑”, “노랑” 중 하나를 선택하는 경우
- 범주에 의미를 연결시킬 수 있는 임의의 문자열
입력 받은 문자를 일정한 범주 안에 포함시킨다.
예) 텍스트 필드에서 쥐색, 회색 등의 답을 입력 받고 이를 ‘여러가지 색’ 범주에 할당한다.
- 구조화된 문자열 데이터
입력한 값들이 일정한 구조를 가진다.
- 텍스트 데이터
자유로운 절과 문장으로 구성되어 있다.
데이터셋 - 말뭉치
데이터 포인트 - 문서
영화 리뷰 감성 분석하기
이 데이터셋은 리뷰 텍스트와 '양성' 혹은 '음성'을 나타내는 레이블을 포함한다. 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())
👉 각 문장에 나타난 단어의 출현 횟수를 나타낸다.
영화 리뷰에 대한 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%로 이전과 동일하지만 특성의 개수가 줄어서 처리 속도가 빨라지고, 불필요한 특성이 없어져 모델이 알아보기 간편해졌다.