Bag of Words - BOW
- Bag of Words 모델은 문서가 가지는 모든 단어를 문맥이나 순서를 무시하고 일괄적으로 단어에 대해 빈도값을 부여해 피처값을 추출하는 모델이다.
- 다음과 같은 2개의 문장이 있다고 가정하고 이 문장을 BOW의 단어 수 기반으로 피처를 추출해보자
- 문장 1: 'My wife likes to watch baseball games and my daughter likes to watch baseball games too'
- 문장 2: 'My wife likes to play baseball'
- 문장 1과 문장 2에 있는 모든 단어에서 중복을 제거하고, 각 단어를 칼럼 형태로 나열한다. 그리고 각 단어에 고유의 인덱스를 다음과 같이 부여한다.
- 'and':0, 'baseball':1, 'daughter':2, 'games':3, 'likes':4, 'my':5, 'play':6, 'to':7, 'too':8, 'watch':9, 'wife':10
- 개별 문장에서 해당 단어가 나타나는 횟수를 각 단어에 기재한다. 예를 들어, baseball은 문장 1,2에서 총 2번 나타난다.
- BOW 모델의 장점은 쉽고 빠른 구축에 있다. 단순히 단어 발생 횟수에 기반하지만 예상보다 문서의 특징을 잘 나타낼 수 있는 모델이라 전통적으로 여러 분야에서 활용도가 높다.
- 그러나, BOW 기반의 NLP 연구는 여러가지 제약에 부딪힌다.
- 문맥 의미 반영 부족: BOW는 단어의 순서를 고려하지 않기 때문에 문장 내에서 단어의 문맥적 의미가 무시된다. 이를 보완하기 위해 n_gram을 활용할 수 있지만 제한적이다.
- 희소 행렬 문제(희소성): BOW로 피처 벡터화를 수행하면 희소 행렬 형태의 데이터셋이 만들어지기 쉽다. 많은 문서에서 단어를 추출하면 매우 많은 단어가 칼럼으로 만들어진다. 문서마다 서로 다른 단어로 구성되기에 단어가 문서마다 나타나지 않는 경우가 훨씬 더 많다. 따라서 대부분의 데이터는 0값으로 채워지게 되는데 이처럼 대규모의 칼럼으로 구성된 행렬에서 대부분의 값이 0으로 채워지는 행렬을 희소 행렬이라고 한다. 희소 행렬은 일반적으로 ML 알고리즘의 수행 시간과 예측 성능을 떨어뜨리기 때문에 희소 행렬을 위한 특별한 기법이 마련되어있다.
BOW 피처 벡터화
- 피처 벡터화는 각 문서의 텍스트를 단어로 추출해 피처로 할당하고, 각 단어의 발생 빈도와 같은 값을 이 피처에 값으로 부여해 각 문서를 이 단어 피처의 발생 빈도값으로 구성된 벡터로 만드는 기법이다.
- BOW 모델에서 피처 벡터화를 수행한다는 것은 모든 문서에서 모든 단어를 칼럼 형태로 나열하고 각 문서에서 해당 단어의 횟수나 정규화된 빈도를 값으로 부여하는 데이터셋 모델로 변경하는 것이다.
- 예를 들어, M개의 텍스트 문서가 있고, 이 문서에서 모든 단어를 추출해 나열했을 때 N개의 단어가 있다고 가정하면, 피처 벡터화를 수행하면 M개의 문서는 각각 N개의 값이 할당된 피처의 벡터 세트가 된다. 결과적으로 M X N개의 단어 피처로 이루어진 행렬을 구성하게 된다.
- 단어 피처에 값을 부여할 때 각 문서에서 해당 단어가 나타나는 횟수, 즉 Count를 부여한다.
- 카운트값이 높을수록 중요한 단어로 인식된다.
- 그러나, 카운트만 부여할 경우 그 문서의 특징을 나타내기 보다 언어의 특성상 문장에서 자주 사용될 수 밖에 없는 단어까지 높은 값을 부여받게 된다.
- 이러한 문제를 보완하기 위해 TF-IDF 벡터화를 사용한다.일반적으로 BOW의 피처 벡터화는 두 가지 방식이 있다.
1) TF-IDF(Term Frequency = Inverse Document Frequency) 기반의 벡터화
-
- 개별 문서에서 자주 나타나는 단어에 높은 가중치를 두되, 모든 문서에서 전반적으로 자주 나타나는 단어에 대해서는 페널티를 주는 방식으로 값을 부여한다.
- 예를 들어, 여러 가지 뉴스의 문서에서 '분쟁', '종교 대립', '유혈 사태'와 같은 단어가 자주 나타난다면 해당 문서는 지역 분쟁과 관련한 뉴스일 가능성이 높고 해당 단어는 그 문서의 특징을 잘 나타낸다고 볼 수 있다.
- 하지만 '많은', '빈번하게', '당연히', '조직' 등과 같은 단어는 문서의 특징과 관련성이 적지만 보편적으로 많이 사용되기 때문에 단순히 등장하는 횟수에 따라 중요도를 평가받으면 안된다. 따라서, 이런 단어들에는 페널티를 부여하여 단어에 대한 가중치 균형을 맞추는 것이다.
2) 카운트 기반의 벡터화
- 문서마다 텍스트가 길고 문서 개수가 많은 경우, 카운트 방식보다 TF-IDF 방식이 더 좋은 예측 성능을 보장할 수 있다.
사이킷런의 Count 및 TF-IDF 벡터화 구현: CountVectorizer, TfidfVectorizer
- 사이킷런의 CountVectorizer는 단지 피처 벡터화만 수행하지 않으며 소문자 일괄 변환, 토큰화, 스톱 워드 필터링 등의 텍스트 전처리도 함께 수행한다.
- CountVectorizer에 텍스트 전처리 및 피처 벡터화를 위한 파라미터를 설정하면 된다.
- max_df: 전체 문서에 걸쳐 너무 높은 빈도수를 가지는 단어 피처를 제외하기 위함. 100으로 설정하면 전체 문서에 걸쳐 100개 이하로 나타나는 단어만 피처로 추출함. 0.95같이 설정하면 빈도수 0~95%까지의 단어만 피처로 추출함.
- min_df: 전체 문서에 걸쳐 너무 낮은 빈도수를 가지는 단어 피처를 제외하기 위함. 2로 설정하면 2번 이하로 나타나는 단어는 추출하지 않음. 크게 중요하지 않거나 가비지(garbage)성 단어일 확률이 높음
- max_features: 추출하는 피처의 개수를 제한하며 정수로 값을 지정한다. 2000으로 설정하면 가장 높은 빈도를 가지는 단어순으로 정렬해 2000개까지 추출함
- stop_words: 'english'로 지정하면 영어 스톱 워드로 지정된 단어는 추출에서 제외함
- n_gram_range: BOW 모델의 단어 순서를 어느정도 보강하기 위한 n_gram의 범위를 설정함. (1,1)로 설정하면 토큰화된 단어를 1개씩 피처로 추출함. (1,2)로 설정하면 1개씩, 순서대로 2개씩 묶어서 피처로 추출함
- analyzer: 피처 추출을 수행한 단위를 지정함. 디폴트는 'word'.
- token_pattern: 토큰화를 수행하는 정규 표현식 패턴을 지정함. 디폴트값은 '\b\w\w+\b'로 디폴트값을 변경할 경우는 거의 없음
- tokenizer: 토큰화를 별도의 커스텀 함수로 이용시 적용함.
단계
- 사전 데이터 가공: 영어의 경우 모든 문자를 소문자로 변경
- 토큰화: 디폴트로 단어 기준으로 n_gram_range를 반영하여 각 단어를 토큰화
- 텍스트 정규화: 스톱 워드 필터링만 수행
- 피처 벡터화: max_df, min_df, max_features 등의 파라미터를 이용하여 토큰화된 단어를 피처로 추출하고 단어 빈도수 벡터값을 적용
BOW 벡터화를 위한 희소 행렬
- 사이킷런의 CountVectorizer/TfidfVectorizer를 이용해 텍스트를 피처 단위로 벡터화해 변환하고 CSR 형태의 희소 행렬을 반환한다.
- 좀 더 난이도있는 ML 모델을 수립하기 위해서는 이런 희소 행렬이 어떤 형태로 되어있는지 알아야 한다.
모든 문서에 있는 단어를 추출해 이를 피처로 벡터화하는 작업은 필연적으로 많은 피처 칼럼을 만들 수 밖에 없다.
모든 문서에 있는 단어의 중복을 제거하고 피처로 만들면 일반적으로 수만~수십만 개의 단어가 만들어진다. 여기서 n_gram을 (1,2)이나 (1,3)으로 증가시키면 칼럼 수는 더 많아진다.
이런 대규모 행렬이 생성되더라도 레코드의 각 문서가 가지는 단어의 수는 제한적이기 때문에 이 행렬의 값은 대부분 0이 차지할 수 밖에 없다.
BOW 형태를 가진 언어 모델의 피처 벡터화는 대부분 희소 행렬이다.
이 희소 행렬은 너무 많은 불필요한 0값이 메모리 공간을 차지하고 행렬의 크기가 커서 연산 시에도 데이터 엑세스를 위한 시간이 많이 소모된다.
따라서 희소 행렬을 물리적으로 적은 메모리 공간을 차지할 수 있도록 변환해야 하는데, 대표적으로 COO와 CSR 형식이 있다.
희소 행렬 - COO 형식
- COO(Coordinate: 좌표) 형식은 0이 아닌 데이터만 별도의 데이터 배열에 저장하고, 그 데이터가 가리키는 행과 열의 위치를 별도의 배열로 저장하는 방식이다.
- 예를 들어, [ [3,0,1], [0,2,0] ]과 같은 2차원 데이터가 있다고 가정하자. 0이 아닌 데이터는 [3,1,2]이며 0이 아닌 데이터가 있는 위치를 (row, col)로 표시하면 (0,0), (0,2), (1,1)이 된다. 로우와 칼럼을 별도의 배열로 저장하면 로우는 [0,0,1], 칼럼은 [0,2,1]이다.
- 파이썬에서는 희소 행렬 변환을 위해 Scipy를 이용한다. Scipy의 Sparse 패키지, coo_matrix()를 이용해 수행해보자
In [1]:
import numpy as np
dense = np.array( [ [3, 0, 1], [0, 2, 0] ] )
from scipy import sparse
#0이 아닌 데이터 추출
data = np.array([3,1,2])
#행, 열 위치를 배열로 생성
row_pos = np.array([0,0,1])
col_pos = np.array([0,2,1])
#COO 형식으로 희소 행렬 생성
sparse_coo = sparse.coo_matrix((data, (row_pos, col_pos)))
sparse_coo는 COO 형식의 희소 행렬 객체 변수
toarray()를 이용해 다시 밀집 형태의 행렬로 출력해보자
In [2]:
sparse_coo.toarray()
Out[2]:
array([[3, 0, 1],
[0, 2, 0]])
희소 행렬 - CSR 형식
- CSR(Compressed Sparse Row) 형식은 COO 형식이 행과 열의 위치를 나타내기 위해 반복적인 위치 데이터를 사용해야 하는 문제점을 해결한 방식이다.
- 먼저, COO 형식의 문제점을 알아보자
- 예를 들어, [ [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] ] 이라는 2차원 배열이 있다고 가정하자.
- 그럼 데이터 배열은 [1,5,1,4,3,2,5,6,3,2,7,8,1]이고 행 위치 배열은 [0,0,1,1,1,1,1,2,2,3,4,4,5], 열 위치 배열은 [2,5,0,1,3,4,5,1,3,0,3,5,0]이다.
- 행 위치 배열에서 같은 값이 반복적으로 나타난다. 행 위치 배열이 0부터 순차적으로 증가하는 값으로 이뤄졌다는 특성을 고려하면 행 위치 배열의 고유한 값의 시작 위치만 표기하는 방법으로 이런 반복을 제거할 수 있다.(즉, 위치의 위치를 표기하는 것)
- 행 위치 배열의 첫번째는(인덱스 0) 0, 두번째(인덱스 1)는 0, 세번째(인덱스 2)가 1이라면 고유값 시작 위치는 [0,2]인 것이다.
- CSR은 이처럼 행 위치 배열 내에 있는 고유값의 시작 위치만 다시 별도의 위치 배열로 가지는 변환 방식을 말한다.
- Scipy의 csr_matrix()를 이용하여 수행해보자
In [4]:
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] ])
data2 = np.array([1,5,1,4,3,2,5,6,3,2,7,8,1])
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 변환된 데이터가 제대로 됐는지 다시 Dense로 출력 확인
[[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]]
CSR 변환된 데이터가 제대로 됐는지 다시 Dense로 출력 확인
[[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와 CSR 형식을 실제로 사용 시에는 다음과 같이 밀집 행렬을 생성 파라미터로 입력하면 COO나 CSR 희소 행렬로 생성한다
In [5]:
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)
사이킷런의 CountVectorizer나 TfidfVectorizer로 변환된 피처 벡터화 행렬은 모두 Scipy의 CSR 형태의 희소 행렬이다.
'Data Science > 파이썬 머신러닝 완벽 가이드' 카테고리의 다른 글
[sklearn] (51) - 텍스트 분류 실습 - 20 뉴스그룹 분류 (0) | 2023.08.01 |
---|---|
[sklearn] (49) 텍스트 사전 분비 작업(텍스트 전처리) - 텍스트 정규화 (0) | 2023.07.31 |
[sklearn] (48) 자연어처리(NLP, Natural Language Processing)와 텍스트 분석(Text Analytics) (0) | 2023.07.28 |
[sklearn] (47) 군집화 실습 - 고객 세그먼테이션(Customer Segmentation) (0) | 2023.07.28 |
[sklearn] (46) DBSCAN(밀도 기반 클러스터링), make_circles (0) | 2023.07.26 |