자연어 처리 - 단어 표현
단어 표현
단어 벡터(word vector), 워드 임베딩(word embedding)
원-핫 인코딩(one-hot encoding)
매우 간단하고 이해하기 쉬움
실제 자연어 처리를 할 경우 수십~수백만 개의 단어를 표현해야 하는데 각 단어 벡터의 크기가 너무 커져 공간을 많이 사용 -> 매우 비효율적
단어의 의미나 특성 같은 것들이 전혀 표현되지 않음
카운트 기반
text = ['''
오늘 네가
보고싶다
널 다시 품에
안아보고 싶다
오늘 네가 난
생각난다
너랑 같이 산책하던
그곳에 서있다
너의 체온이
기억난다
따뜻하게 내 쉬던
숨소리 들려
오늘 네가
온 것 같아
우리 같이 잠들던
벤치에 기대니
시간이 흘러흘러
다시 만날 순 없지
그래도 보고싶다 널
그리운 내 사랑아
오늘 네가 정말
보고싶다
너를 다시 내 품에
안아보고 싶다
오늘 네가 난
생각난다
너를 쓰다듬던 손이
너를 기억한다
니가 보고싶다
''']
sklearn - CountVectorizer
단어들의 카운트(출현 빈도;frequency)로 여러 문서들을 벡터화
2글자 이상만 체크
from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
import numpy as np
cv = CountVectorizer()
countv = cv.fit_transform(text).toarray()
countv
array([[1, 2, 1, 1, 1, 1, 1, 1, 1, 3, 1, 5, 1, 3, 1, 1, 1, 1, 4, 1, 1, 2,
1, 1, 1, 1, 1, 2, 1, 2, 1, 5, 1, 1, 1, 1, 2, 1]], dtype=int64)
print(cv.vocabulary_)
{'오늘': 31, '네가': 11, '보고싶다': 18, '다시': 13, '품에': 36, '안아보고': 29, '싶다': 27, '생각난다': 21, '너랑': 8, '같이': 1, '산책하던': 20, '그곳에': 2, '서있다': 22, '너의': 10, '체온이': 35, '기억난다': 6, '따뜻하게': 15, '쉬던': 25, '숨소리': 24, '들려': 14, '같아': 0, '우리': 32, '잠들던': 33, '벤치에': 17, '기대니': 5, '시간이': 26, '흘러흘러': 37, '만날': 16, '없지': 30, '그래도': 3, '그리운': 4, '사랑아': 19, '정말': 34, '너를': 9, '쓰다듬던': 28, '손이': 23, '기억한다': 7, '니가': 12}
pd.DataFrame({'vocab':sorted(list(cv.vocabulary_.keys())), 'CountVector':countv[0]})
vocab | CountVector | |
---|---|---|
0 | 같아 | 1 |
1 | 같이 | 2 |
2 | 그곳에 | 1 |
3 | 그래도 | 1 |
4 | 그리운 | 1 |
5 | 기대니 | 1 |
6 | 기억난다 | 1 |
7 | 기억한다 | 1 |
8 | 너랑 | 1 |
9 | 너를 | 3 |
10 | 너의 | 1 |
11 | 네가 | 5 |
12 | 니가 | 1 |
13 | 다시 | 3 |
14 | 들려 | 1 |
15 | 따뜻하게 | 1 |
16 | 만날 | 1 |
17 | 벤치에 | 1 |
18 | 보고싶다 | 4 |
19 | 사랑아 | 1 |
20 | 산책하던 | 1 |
21 | 생각난다 | 2 |
22 | 서있다 | 1 |
23 | 손이 | 1 |
24 | 숨소리 | 1 |
25 | 쉬던 | 1 |
26 | 시간이 | 1 |
27 | 싶다 | 2 |
28 | 쓰다듬던 | 1 |
29 | 안아보고 | 2 |
30 | 없지 | 1 |
31 | 오늘 | 5 |
32 | 우리 | 1 |
33 | 잠들던 | 1 |
34 | 정말 | 1 |
35 | 체온이 | 1 |
36 | 품에 | 2 |
37 | 흘러흘러 | 1 |
sklearn - TfidfVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
ti = TfidfVectorizer()
tfidfv = ti.fit_transform(text).toarray()
tfidfv
array([[0.08703883, 0.17407766, 0.08703883, 0.08703883, 0.08703883,
0.08703883, 0.08703883, 0.08703883, 0.08703883, 0.26111648,
0.08703883, 0.43519414, 0.08703883, 0.26111648, 0.08703883,
0.08703883, 0.08703883, 0.08703883, 0.34815531, 0.08703883,
0.08703883, 0.17407766, 0.08703883, 0.08703883, 0.08703883,
0.08703883, 0.08703883, 0.17407766, 0.08703883, 0.17407766,
0.08703883, 0.43519414, 0.08703883, 0.08703883, 0.08703883,
0.08703883, 0.17407766, 0.08703883]])
print(ti.vocabulary_)
{'오늘': 31, '네가': 11, '보고싶다': 18, '다시': 13, '품에': 36, '안아보고': 29, '싶다': 27, '생각난다': 21, '너랑': 8, '같이': 1, '산책하던': 20, '그곳에': 2, '서있다': 22, '너의': 10, '체온이': 35, '기억난다': 6, '따뜻하게': 15, '쉬던': 25, '숨소리': 24, '들려': 14, '같아': 0, '우리': 32, '잠들던': 33, '벤치에': 17, '기대니': 5, '시간이': 26, '흘러흘러': 37, '만날': 16, '없지': 30, '그래도': 3, '그리운': 4, '사랑아': 19, '정말': 34, '너를': 9, '쓰다듬던': 28, '손이': 23, '기억한다': 7, '니가': 12}
pd.DataFrame({'vocab':sorted(list(ti.vocabulary_.keys())), 'TfidfVector':tfidfv[0]})
vocab | TfidfVector | |
---|---|---|
0 | 같아 | 0.087039 |
1 | 같이 | 0.174078 |
2 | 그곳에 | 0.087039 |
3 | 그래도 | 0.087039 |
4 | 그리운 | 0.087039 |
5 | 기대니 | 0.087039 |
6 | 기억난다 | 0.087039 |
7 | 기억한다 | 0.087039 |
8 | 너랑 | 0.087039 |
9 | 너를 | 0.261116 |
10 | 너의 | 0.087039 |
11 | 네가 | 0.435194 |
12 | 니가 | 0.087039 |
13 | 다시 | 0.261116 |
14 | 들려 | 0.087039 |
15 | 따뜻하게 | 0.087039 |
16 | 만날 | 0.087039 |
17 | 벤치에 | 0.087039 |
18 | 보고싶다 | 0.348155 |
19 | 사랑아 | 0.087039 |
20 | 산책하던 | 0.087039 |
21 | 생각난다 | 0.174078 |
22 | 서있다 | 0.087039 |
23 | 손이 | 0.087039 |
24 | 숨소리 | 0.087039 |
25 | 쉬던 | 0.087039 |
26 | 시간이 | 0.087039 |
27 | 싶다 | 0.174078 |
28 | 쓰다듬던 | 0.087039 |
29 | 안아보고 | 0.174078 |
30 | 없지 | 0.087039 |
31 | 오늘 | 0.435194 |
32 | 우리 | 0.087039 |
33 | 잠들던 | 0.087039 |
34 | 정말 | 0.087039 |
35 | 체온이 | 0.087039 |
36 | 품에 | 0.174078 |
37 | 흘러흘러 | 0.087039 |
- Tfidf: Term Frequency-Inverse Document Frequency
(TF: 단어의 반복)/(IDF: 문서에서 반복)
모든 문서에 상투적으로 사용되는 단어(중요도가 낮은 단어)를 나눠서 제어해준다. - 무의미성
documents = [
'먹고 싶은 치킨',
'먹고 싶은 피자',
'맛있지만 살이 찌는 피자 피자',
'나는 야식이 좋아요'
]
vocab = list(set(word for document in documents for word in document.split()))
print(vocab)
vocab2 = []
for document in documents:
for word in document.split():
vocab2.append(word)
print(list(set(vocab2)))
['나는', '살이', '치킨', '야식이', '피자', '찌는', '좋아요', '싶은', '먹고', '맛있지만']
['나는', '살이', '치킨', '야식이', '피자', '찌는', '좋아요', '싶은', '먹고', '맛있지만']
vocab.sort()
len(vocab), vocab
(10, ['나는', '맛있지만', '먹고', '살이', '싶은', '야식이', '좋아요', '찌는', '치킨', '피자'])
# 문서의 길이
N = len(documents)
N
4
TF 코드 구현
CounterVectorizer
# TF 구하는 함수
def tf(text, d):
return d.count(text)
tf('피자', '맛있지만 살이 찌는 피자 피자')
2
result = []
for i in range(N):
result.append([])
d = documents[i]
for j in range(len(vocab)):
t = vocab[j]
result[-1].append(tf(t, d))
result # Bag of words: 분류 하기 위함, 특히 감성 평가
[[0, 0, 1, 0, 1, 0, 0, 0, 1, 0],
[0, 0, 1, 0, 1, 0, 0, 0, 0, 1],
[0, 1, 0, 1, 0, 0, 0, 1, 0, 2],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 0]]
# CountVectorizer와 비교
cv = CountVectorizer()
countv = cv.fit_transform(documents).toarray()
countv
array([[0, 0, 1, 0, 1, 0, 0, 0, 1, 0],
[0, 0, 1, 0, 1, 0, 0, 0, 0, 1],
[0, 1, 0, 1, 0, 0, 0, 1, 0, 2],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 0]], dtype=int64)
tf_res = pd.DataFrame(result, columns=vocab)
tf_res
나는 | 맛있지만 | 먹고 | 살이 | 싶은 | 야식이 | 좋아요 | 찌는 | 치킨 | 피자 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 |
1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
2 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 2 |
3 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
‘먹고 싶은 치킨’,
‘먹고 싶은 피자’,
‘맛있지만 살이 찌는 피자 피자’,
‘나는 야식이 좋아요’
IDF 코드 구현
T 문서마다 많이 나온 단어는 값이 작음(inverse)
# IDF 구하는 함수
from math import log
def idf(text):
df = 0
for doc in documents:
df += t in doc # 문서마다 반복 수 카운트
return log(N/(df+1)) # N:문서 수, df+1:zero division 방지, log:큰 값을 제한
result = []
for j in range(len(vocab)):
t = vocab[j]
result.append(idf(t))
np.array(result)
array([0.69314718, 0.69314718, 0.28768207, 0.69314718, 0.28768207,
0.69314718, 0.69314718, 0.69314718, 0.69314718, 0.28768207])
idf_result = pd.DataFrame(result, columns=['idf'], index=vocab)
idf_result
idf | |
---|---|
나는 | 0.693147 |
맛있지만 | 0.693147 |
먹고 | 0.287682 |
살이 | 0.693147 |
싶은 | 0.287682 |
야식이 | 0.693147 |
좋아요 | 0.693147 |
찌는 | 0.693147 |
치킨 | 0.693147 |
피자 | 0.287682 |
TF-IDF 코드 구현
TfidfVectorizer
# TF-IDF 구하는 함수
def tfidf(t, d):
return tf(t, d) * idf(t)
result = []
for i in range(N):
result.append([])
d = documents[i]
for j in range(len(vocab)):
t = vocab[j]
result[-1].append(tfidf(t, d))
np.array(result)
array([[0. , 0. , 0.28768207, 0. , 0.28768207,
0. , 0. , 0. , 0.69314718, 0. ],
[0. , 0. , 0.28768207, 0. , 0.28768207,
0. , 0. , 0. , 0. , 0.28768207],
[0. , 0.69314718, 0. , 0.69314718, 0. ,
0. , 0. , 0.69314718, 0. , 0.57536414],
[0.69314718, 0. , 0. , 0. , 0. ,
0.69314718, 0.69314718, 0. , 0. , 0. ]])
sklearn의 TfidfVectorizer 구현
# TF 구하는 함수
def tf(text, d):
return d.count(text)
count_vec = []
for i in range(N):
count_vec.append([])
d = documents[i]
for j in range(len(vocab)):
t = vocab[j]
count_vec[-1].append(tf(t, d))
count_vec
[[0, 0, 1, 0, 1, 0, 0, 0, 1, 0],
[0, 0, 1, 0, 1, 0, 0, 0, 0, 1],
[0, 1, 0, 1, 0, 0, 0, 1, 0, 2],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 0]]
보편적인 idf를 구하는 식에서 로그항의 분자에 1을 더해주고 로그항에 1을 더함
from math import log
def s_idf(text):
df = 0
for doc in documents:
df += t in doc
return log((N+1)/(df+1))+1
s_idf_rst = []
for j in range(len(vocab)):
t = vocab[j]
s_idf_rst.append(s_idf(t))
np.array(s_idf_rst)
array([1.91629073, 1.91629073, 1.51082562, 1.91629073, 1.51082562,
1.91629073, 1.91629073, 1.91629073, 1.91629073, 1.51082562])
Tfidfvectorizer의 idf와 비교해 본다.
ti = TfidfVectorizer()
ti.fit(documents)
ti.idf_
array([1.91629073, 1.91629073, 1.51082562, 1.91629073, 1.51082562,
1.91629073, 1.91629073, 1.91629073, 1.91629073, 1.51082562])
구한 TF 벡터와 IDF 벡터를 곱해준다.
import numpy as np
np.multiply(np.array(count_vec), np.array(s_idf_rst))
array([[0. , 0. , 1.51082562, 0. , 1.51082562,
0. , 0. , 0. , 1.91629073, 0. ],
[0. , 0. , 1.51082562, 0. , 1.51082562,
0. , 0. , 0. , 0. , 1.51082562],
[0. , 1.91629073, 0. , 1.91629073, 0. ,
0. , 0. , 1.91629073, 0. , 3.02165125],
[1.91629073, 0. , 0. , 0. , 0. ,
1.91629073, 1.91629073, 0. , 0. , 0. ]])
L2 정규화를 한다.
from sklearn.preprocessing import normalize
tfidf_bfor_l2 = np.multiply(np.array(count_vec), np.array(s_idf_rst))
tfidf_afer_l2 = normalize(tfidf_bfor_l2, norm='l2')
tfidf_afer_l2
array([[0. , 0. , 0.52640543, 0. , 0.52640543,
0. , 0. , 0. , 0.66767854, 0. ],
[0. , 0. , 0.57735027, 0. , 0.57735027,
0. , 0. , 0. , 0. , 0.57735027],
[0. , 0.42693074, 0. , 0.42693074, 0. ,
0. , 0. , 0.42693074, 0. , 0.6731942 ],
[0.57735027, 0. , 0. , 0. , 0. ,
0.57735027, 0.57735027, 0. , 0. , 0. ]])
TfidfVectorizer 최종 결과와 비교해본다.
ti = TfidfVectorizer()
tfidfv = ti.fit_transform(documents).toarray()
tfidfv
array([[0. , 0. , 0.52640543, 0. , 0.52640543,
0. , 0. , 0. , 0.66767854, 0. ],
[0. , 0. , 0.57735027, 0. , 0.57735027,
0. , 0. , 0. , 0. , 0.57735027],
[0. , 0.42693074, 0. , 0.42693074, 0. ,
0. , 0. , 0.42693074, 0. , 0.6731942 ],
[0.57735027, 0. , 0. , 0. , 0. ,
0.57735027, 0.57735027, 0. , 0. , 0. ]])
tfidf_res = pd.DataFrame(tfidf_afer_l2, columns=vocab)
tfidf_res
나는 | 맛있지만 | 먹고 | 살이 | 싶은 | 야식이 | 좋아요 | 찌는 | 치킨 | 피자 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0.00000 | 0.000000 | 0.526405 | 0.000000 | 0.526405 | 0.00000 | 0.00000 | 0.000000 | 0.667679 | 0.000000 |
1 | 0.00000 | 0.000000 | 0.577350 | 0.000000 | 0.577350 | 0.00000 | 0.00000 | 0.000000 | 0.000000 | 0.577350 |
2 | 0.00000 | 0.426931 | 0.000000 | 0.426931 | 0.000000 | 0.00000 | 0.00000 | 0.426931 | 0.000000 | 0.673194 |
3 | 0.57735 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.57735 | 0.57735 | 0.000000 | 0.000000 | 0.000000 |
네이버 영화 리뷰 분석
from sklearn.feature_extraction.text import TfidfVectorizer
import pandas as pd
df_train = pd.read_csv('datasets/ratings_train.txt', delimiter='\t')
df_train.head()
id | document | label | |
---|---|---|---|
0 | 9976970 | 아 더빙.. 진짜 짜증나네요 목소리 | 0 |
1 | 3819312 | 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 | 1 |
2 | 10265843 | 너무재밓었다그래서보는것을추천한다 | 0 |
3 | 9045019 | 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 | 0 |
4 | 6483659 | 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 ... | 1 |
# df_train = df_train[:10000]
df_train.dropna(inplace=True)
df_train.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 149995 entries, 0 to 149999
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 149995 non-null int64
1 document 149995 non-null object
2 label 149995 non-null int64
dtypes: int64(2), object(1)
memory usage: 4.6+ MB
df_test = pd.read_csv('datasets/ratings_test.txt', delimiter='\t')
df_test.head()
id | document | label | |
---|---|---|---|
0 | 6270596 | 굳 ㅋ | 1 |
1 | 9274899 | GDNTOPCLASSINTHECLUB | 0 |
2 | 8544678 | 뭐야 이 평점들은.... 나쁘진 않지만 10점 짜리는 더더욱 아니잖아 | 0 |
3 | 6825595 | 지루하지는 않은데 완전 막장임... 돈주고 보기에는.... | 0 |
4 | 6723715 | 3D만 아니었어도 별 다섯 개 줬을텐데.. 왜 3D로 나와서 제 심기를 불편하게 하죠?? | 0 |
df_test.dropna(inplace=True)
df_test.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 49997 entries, 0 to 49999
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 49997 non-null int64
1 document 49997 non-null object
2 label 49997 non-null int64
dtypes: int64(2), object(1)
memory usage: 1.5+ MB
X_train = df_train['document']
y_train = df_train['label']
X_test = df_test['document']
y_test = df_test['label']
출력 초과 에러
CMD
naver_re_tv = TfidfVectorizer()
naver_re_tv.fit(X_train)
print(naver_re_tv.vocabulary_)
{'더빙': 71119, '진짜': 246232, '짜증나네요': 248358, '목소리': 99567, '포스터보고': 273335, '초딩영화줄': 255126, '오버연기조차': 190112, '가볍지': 16352, '않구나': 167602, '너무재밓었다그래서보는것을추천한다': 57394, '교도소': 33783, '이야기구먼': 208071, '솔직히': 145795, '재미는': 222295, '없다': 177352, '평점': 271982, '조정': 234711, '사이몬페그의': 133947,
...
'고마움': 28958, '1점도아깝다진짜': 2615, '척편이여서': 253296, '불가50가지': 126113, '찎었냐': 250252, '220215679580': 3645, '크리쳐개그물임ㅋㅋ': 263752, '흥겹다ㅋ': 291905, '높아서ㅋㅋ': 60705, 'carl': 8822, '세이건으로': 143023, '디케이드': 80079, '오즈인데': 190555, '더블은': 71115, '뭔죄인가': 105744, '거들먹거리고': 24486, '혼혈은': 287413, '수간하는': 146244}
X_train_tfidf = naver_re_tv.transform(X_train)
X_train_tfidf # 희소행렬 생성 (293366개의 단어에 대해 Bag of word)
<149995x293366 sparse matrix of type '<class 'numpy.float64'>'
with 1074805 stored elements in Compressed Sparse Row format>
X_test_tfidf = naver_re_tv.transform(X_test)
X_test_tfidf
<49997x293366 sparse matrix of type '<class 'numpy.float64'>'
with 284188 stored elements in Compressed Sparse Row format>
LogisticRegression 머신러닝
from sklearn.linear_model import LogisticRegression
lr_model = LogisticRegression(max_iter=400)
lr_model.fit(X_train_tfidf, y_train)
lr_model.score(X_test_tfidf, y_test)
0.8125087505250315
예측 기반
Word2vec
- CBOW: 입력값으로 여러 개의 단어를 사용, 학습을 위해 하나의 단어와 비교
- Skip-Gram: 입력값이 하나의 단어를 사용, 학습을 위해 주변의 여러 단어와 비교
단어 간의 유사도를 잘 측정한다.
- 한국-서울+도쿄 => 일본
- 단어 한국과 단어 서울의 거리는 단어 ‘일본’과 단어 도쿄의 거리와 같다
한국-서울+베이징 => 중국
맥주-치킨+피자 => 음료수
남자-아빠+엄마 => 여자
Reference
- 은공지능 공작소: 본격 TF-IDF 개념 해부
댓글남기기