귀퉁이 서재

NLP - 7. 희소 행렬 (Sparse Matrix) - COO 형식, CSR 형식 본문

자연어 처리 (NLP)

NLP - 7. 희소 행렬 (Sparse Matrix) - COO 형식, CSR 형식

Baek Kyun Shin 2020. 2. 16. 11:51

이번 장에선 희소 행렬에 대해 알아보겠습니다. 마찬가지로 파이썬 머신러닝 완벽 가이드 (권철민 저), 딥 러닝을 이용한 자연어 처리 입문(유원주 저)을 요약정리했습니다.

이전 장에서 배웠던 CountVectorizer, TfidfVectorizer를 이용해 피처 벡터화를 하면 상당히 많은 칼럼이 생깁니다. 모든 문서에 포함된 모든 고유 단어를 피처로 만들어주기 때문입니다. 모든 문서의 단어를 피처로 만들어주면 수만 개에서 수십만 개의 단어가 만들어집니다. 이렇게 대규모의 행렬이 생기더라도 각 문서에 포함된 단어의 수는 제한적이기 때문에 행렬의 대부분의 값은 0으로 채워집니다. 이렇듯 대부분 값이 0으로 채워진 행렬을 희소 행렬(Sparse Matrix)이라고 합니다. 아래는 희소 행렬의 예시입니다.

출처: Quora

이와 반대로 대부분의 값이 0이 아닌 값으로 채워진 행렬은 밀집 행렬(Dense Matrix)이라고 합니다. BOW 형태를 가진 언어 모델의 피처 벡터화는 대부분 희소 행렬을 만듭니다. 

희소 행렬은 너무 많은 불필요한 0 값으로 인해 메모리 낭비가 심합니다. 또한 행렬의 크기가 커서 연산 시 시간도 많이 소모됩니다. 따라서 이런 희소 행렬을 메모리 낭비가 적도록 변환해야 하는데, 대표적인 방법이 COO 형식과 CSR 형식입니다.

희소 행렬 - COO 형식

COO(Coordinate: 좌표) 형식은 0이 아닌 데이터만 별도의 배열에 저장하고, 그 데이터가 가리키는 행과 열의 위치를 별도의 배열에 저장하는 방식입니다. 예를 들어 아래와 같은 2 x 3 행렬이 있다고 해봅시다.

3 0 1
0 2 0

0이 아닌 값은 [3, 1, 2]입니다. 3의 행과 열의 위치는 (0, 0)이고, 1의 행과 열의 위치는 (0, 2)이며, 2의 행과 열의 위치는 (1, 1)입니다. 행 위치 값만 모으면 [0, 0, 1], 열 위치 값만 모으면 [0, 2, 1]입니다.

COO 형식은 0이 아닌 값, 행 위치 값, 열 위치 값에 대한 배열로 표현하는 형식입니다. 0이 아닌 값 배열: [3, 1, 2], 0이 아닌 값의 행 위치 값 배열: [0, 0, 1], 0이 아닌 값의 열 위치 값 배열: [0, 2, 1]로 표현하는 것입니다. 이 세개의 배열만 저장해도 이를 통해 원본 행렬을 구할 수 있습니다. 따라서 원본 행렬을 다 저장하며 메모리를 낭비할 필요가 없습니다.

COO 형식을 활용해 원본 행렬을 구해보는 실습을 해보겠습니다.

from scipy import sparse

# 0 이 아닌 데이터 추출
data = np.array([3,1,2])

# 행 위치와 열 위치를 각각 array로 생성 
row_pos = np.array([0,0,1])
col_pos = np.array([0,2,1])

# sparse 패키지의 coo_matrix를 이용하여 COO 형식으로 희소 행렬 생성
sparse_coo = sparse.coo_matrix((data, (row_pos,col_pos)))

print(type(sparse_coo))
print(sparse_coo)
dense01=sparse_coo.toarray()
print(type(dense01),"\n", dense01)
<class 'scipy.sparse.coo.coo_matrix'>
  (0, 0)	3
  (0, 2)	1
  (1, 1)	2
<class 'numpy.ndarray'> 
 [[3 0 1]
 [0 2 0]]

sparse_coo는 COO 형식의 희소 행렬 객체 변수입니다. sparse_coo.toarray()를 해주면 원본 행렬이 추출됨을 알 수 있습니다.

희소 행렬 - CSR 형식

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

COO 형식을 만들기 위해 0이 아닌 값을 추출하면

[1, 5, 1, 4, 3, 2, 5, 6, 3, 2, 7, 8, 1]입니다.

0이 아닌 값의 행과 열의 위치를 나타내는 배열을 구해보면,

행 위치 배열 = [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, 1, 1, 1, 1, 1, 2, 2, 3, 4, 4, 5]을 보면 순차적으로 같은 값이 반복적으로 나타남을 볼 수 있습니다. 0이 2번, 1이 5번, 2가 2번, 4가 2번 반복됩니다. 행 위치 배열이 0부터 순차적으로 증가한다는 특성을 고려해 행 위치 배열의 고유한 값의 시작 위치만 표기하는 방법으로 이런 반복을 제거할 수 있습니다. 0의 시작 인덱스는 0이고, 1의 시작 인덱스는 2이며, 2의 시작 인덱스는 7입니다. 

따라서 행 위치 배열인 [0, 0, 1, 1, 1, 1, 1, 2, 2, 3, 4, 4, 5]을 행 위치 배열 내 고유한 값의 시작 위치 배열인 [0, 2, 7, 9, 10, 12]로 변환하면 반복도 줄이고 메모리도 적게 사용할 수 있습니다. 마지막에는 총항목의 개수를 추가해주면 됩니다. 총항목이 13개이므로 최종 배열은 [0, 2, 7, 9, 10, 12, 13]입니다.

이렇듯 COO 형식의 문제점을 보완하기 위한 방식이 CSR(Compressed Sparse Row) 형식입니다. COO 형식에 비해 메모리가 적게 들고 빠른 연산이 가능하여, 일반적으로 COO 형식보다는 CSR 형식을 많이 씁니다.

예제를 통해 COO 형식과 CSR 형식을 비교해보겠습니다.

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]])

# 0 이 아닌 데이터 추출
data2 = np.array([1, 5, 1, 4, 3, 2, 5, 6, 3, 2, 7, 8, 1])

# 행 위치와 열 위치를 각각 array로 생성 
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]]

실제 사용 시에는 아래와 같이 희소 행렬을 생성 파라미터로 입력해주면 됩니다.

sparse = 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(sparse)
csr = sparse.csr_matrix(sparse)

사이킷런의 CountVectorizer, TfidfVectorizer 클래스로 피처 벡터화된 행렬은 모두 CSR 형식입니다.

지금까지 희소 행렬과 희소 행렬의 표현 방식에 대해 알아봤습니다.

Reference

Reference1: 파이썬 머신러닝 완벽 가이드 (권철민 저)

Comments