자연어 처리 공부 연습노트 입니다. 공부를 위해 작성한 것이기 때문에 틀린 내용이 있을 수 있습니다. 발견하면 알려주세요! 아래 내용은 주로 <한국어 임베딩>에서 가져왔다는 것을 밝혀둡니다. 



어떻게 워드 임베딩을 통해 자연어의 의미를 수치화할 수 있는가?


가장 기본적인 방법은 문서에 어떤 단어가 (많이) 등장했는지 그 통계량을 세어 그 통계적 패턴 정보를 통째로 임베딩에 넣는 것이다. 이를 Bag of Words(BoW) 가정이라고 한다.


BoW


BoW 가정은 단어의 사용 여부나 사용 빈도를 통해 문서를 파악할 수 있다는 것을 전제로 한다. 그래서 단어의 등장 순서를 고려하지 않고 문서 내 단어의 등장 빈도를 임베딩으로 사용한다.

Scikit-Learn의 feature_extraction 서브패키지와 feature_extraction.text 서브패키지는 BoW를 만드는 DictVectorizer 클래스와 BoW 인코딩한 벡터를 만드는 CountVectorizer 클래스가 있다.

CountVectorizer 클래스를 사용해 간단하게 문장 하나를 벡터화해보자. 



Untitled
In [1]:
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer 
# CounVectorizer 클래스는 단어의 빈도를 Count해 Vector로 만드는 sklearn의 클래스이다.
In [2]:
corpus = ['삶이란 흐르는 오케스트라 우리는 마에스트로.']
In [3]:
vector = CountVectorizer()
In [9]:
print(vector.fit_transform(corpus).toarray()) # corpus로부터 각 단어의 빈도 수를 기록
[[1 1 1 1 1]]
In [10]:
vector.vocabulary_ # 각 단어의 인덱스가 어떻게 부여됐는지 보여준다.
Out[10]:
{'삶이란': 1, '흐르는': 4, '오케스트라': 2, '우리는': 3, '마에스트로': 0}
In [ ]:
 


흠. 이번에는 문장 여러 개를 벡터화해보자. 벡터라이제이션1_ryu
In [2]:
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer 
# CounVectorizer 클래스는 단어의 빈도를 Count해 Vector로 만드는 sklearn의 클래스이다.
In [19]:
def tf_extractor(corpus):
    '''
    return a frequency-based DTM(Document-Term Matrix. 문서에 등장하는 각 단어들의 빈도를 행렬로 표현한 것)
    '''
    
    # CountVectorizer 클래스를 사용해 텍스트를 벡터화하는 vectorizer를 만든다
    vectorizer = CountVectorizer(min_df=1, ngram_range=(1,1)) 
    # min_df=1dlaus, 최소 적어도 하나의 문서에 사용된 단어들을 모두 포함한다
    # ngram_range(1,1) : 유니그램만 포함
    
    # 만들어둔 vectorizer로 나의 corpus를 벡터화해, 이를 features로 지정한다
    features = vectorizer.fit_transform(corpus) # fit_transform()은 fit()과 transform()을 함께 수행하는 메소드다
    return vectorizer, features
In [20]:
# corpus : a list of sentences
#corpus1 = ['Hi there, I am Ryu Han.']
corpus1 = ['안녕 나는 한수연.',
           '지금 자연어 처리를 공부하고 있어.',
           '돈 걱정 없이 공부하고 싶다.',
           '한탄은 넣어두는 게 좋겠지!']
In [21]:
tf_vectorizer, tf_features = tf_extractor(corpus1)
In [22]:
print(tf_vectorizer)
CountVectorizer(analyzer='word', binary=False, decode_error='strict',
                dtype=<class 'numpy.int64'>, encoding='utf-8', input='content',
                lowercase=True, max_df=1.0, max_features=None, min_df=1,
                ngram_range=(1, 1), preprocessor=None, stop_words=None,
                strip_accents=None, token_pattern='(?u)\\b\\w\\w+\\b',
                tokenizer=None, vocabulary=None)
In [23]:
print(tf_features)
  (0, 5)	1
  (0, 2)	1
  (0, 12)	1
  (1, 10)	1
  (1, 8)	1
  (1, 11)	1
  (1, 1)	1
  (1, 7)	1
  (2, 1)	1
  (2, 0)	1
  (2, 6)	1
  (2, 4)	1
  (3, 13)	1
  (3, 3)	1
  (3, 9)	1
In [30]:
features = tf_features.todense() # todense() returns a matrix
features
Out[30]:
matrix([[0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0],
        [1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]])
In [32]:
dtm_np = np.array(features) # 메트릭스를 nd array로 바꾸기. 텍스트를 벡터화한 결과
dtm_np
Out[32]:
array([[0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
       [0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0],
       [1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0],
       [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]])

numpy에는 배열(ndarray)과 행렬(matrix) 객체가 있는데 둘이 헷갈린다. 짚고가자.
ndarray 객체는 다양한 종류의 수치 연산을 위해 고안된 범용 n차 배열이다. (보다 효율적)
반면 matrix 객체는 선형대수 연산을 위해 특별히 공안된 객체다.
참고 글 : studymake 블로그 (https://studymake.tistory.com/408#:~:text=ndarray%EB%8A%94%20%EB%8B%A4%EC%96%91%ED%95%9C%20%EC%A2%85%EB%A5%98%EC%9D%98,%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80%20%EB%AA%87%20%EA%B0%80%EC%A7%80%20%EC%95%88%EB%90%9C%EB%8B%A4.&text=ndarray%20%EB%8A%94%20'*'%EB%8A%94%20%EC%9A%94%EC%86%8C%EA%B0%84%20%EA%B3%B1%EC%85%88%EC%9D%B4%EB%8B%A4.)

In [33]:
dtm_np[0]
Out[33]:
array([0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0])
In [35]:
dtm_np[1]
Out[35]:
array([0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0])

문장에서 단어의 등장 여부, 등장 빈도로 벡터화됐다.
벡터화된 문장들은 사칙연산이 가능하다.

In [36]:
print(np.linalg.norm(dtm_np[1]-dtm_np[0]))
2.8284271247461903
In [37]:
feature_names = tf_vectorizer.get_feature_names()
feature_names
Out[37]:
['걱정',
 '공부하고',
 '나는',
 '넣어두는',
 '싶다',
 '안녕',
 '없이',
 '있어',
 '자연어',
 '좋겠지',
 '지금',
 '처리를',
 '한수연',
 '한탄은']
In [38]:
def display_features(features, feature_names):
    df = pd.DataFrame(data=features, columns=feature_names)
    print(df)
In [39]:
display_features(features, feature_names)
   걱정  공부하고  나는  넣어두는  싶다  안녕  없이  있어  자연어  좋겠지  지금  처리를  한수연  한탄은
0   0     0   1     0   0   1   0   0    0    0   0    0    1    0
1   0     1   0     0   0   0   0   1    1    0   1    1    0    0
2   1     1   0     0   1   0   1   0    0    0   0    0    0    0
3   0     0   0     1   0   0   0   0    0    1   0    0    0    1



최종적으로 CountVectorizer를 사용해 만들어진 DTM은 다음과 같다.  

걱정 공부하고 나는 넣어두는 싶다 안녕 없이 있어  자연어  좋겠지  지금  처리를  한수연  한탄은
0   0     0   1     0   0   1   0   0    0    0   0    0    1    0
1   0     1   0     0   0   0   0   1    1    0   1    1    0    0
2   1     1   0     0   1   0   1   0    0    0   0    0    0    0
3   0     0   0     1   0   0   0   0    0    1   0    0    0    1

(데이터프레임 정렬이 왜 제대로 안 되는지 모르겠다.. 이 문제는 마이너하니까 일단 넘어가자.)

CountVectorizer의 문제는 행렬 대부분 요소 값이 0이라는 것, 즉 희소 행렬(sparse matrix)이라는 점이다. 이런 희소 행렬을 다른 모델의 입력값으로 쓰면 계산량도, 메모리 소비량도 쓸데없이 커진다.또 조사 등 모든 글에서 많이 등장하는 단어도 모두 따로 count하기 때문에 좋은 분석을 하기 어렵다. 

이런 단점을 보완하기 위해 등장한 기법이 있다.  바로 TF-IDF(Term Frequency-Inverse Document Frequency)다.


TF-IDF

TF-IDF에 대해 더 알아보자.




TF-IDF 역시 단어의 순서를 고려하지 않는 BoW 임베딩 기법이다. 

TF는 어떤 단어가 특정 문서에 얼마나 많이 사용되었는지 빈도를 나타낸다. DF는 특정 단어가 나타난 문서의 수를 뜻한다. IDF는 전체 문서 수(N)를 해당 단어의 DF로 나눈 뒤 로그를 취한 값이다. 


TF-IDF를 이용하면 조사와 같이 대부분 문서에서 많이 등장하지만, 정보성은 없는 단어들의 가중치가 줄게 돼 불필요한 정보가 사라진다. 즉 보다 품질 좋은 임베딩이 가능하다. 

TF-IDF로 임베딩을 해보자. 





벡터라이제이션1_ryu
In [43]:
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer 
In [49]:
def tfidf_extractor(corpus):
    vectorizer = TfidfVectorizer(min_df=1, ngram_range=(1,1))
    features = vectorizer.fit_transform(corpus)
    return vectorizer, features
In [50]:
corpus = ['안녕 나는 한수연.',
           '지금 자연어 처리를 공부하고 있어.',
           '돈 걱정 없이 공부하고 싶다.',
           '한탄은 넣어두는 게 좋겠지!']
In [51]:
tfidf_vectorizer, tfidf_features = tfidf_extractor(corpus)
In [52]:
print(tfidf_vectorizer)
TfidfVectorizer(analyzer='word', binary=False, decode_error='strict',
                dtype=<class 'numpy.float64'>, encoding='utf-8',
                input='content', lowercase=True, max_df=1.0, max_features=None,
                min_df=1, ngram_range=(1, 1), norm='l2', preprocessor=None,
                smooth_idf=True, stop_words=None, strip_accents=None,
                sublinear_tf=False, token_pattern='(?u)\\b\\w\\w+\\b',
                tokenizer=None, use_idf=True, vocabulary=None)
In [53]:
print(tfidf_features)
  (0, 12)	0.5773502691896257
  (0, 2)	0.5773502691896257
  (0, 5)	0.5773502691896257
  (1, 7)	0.4651619335222394
  (1, 1)	0.3667390112974172
  (1, 11)	0.4651619335222394
  (1, 8)	0.4651619335222394
  (1, 10)	0.4651619335222394
  (2, 4)	0.5254727492640658
  (2, 6)	0.5254727492640658
  (2, 0)	0.5254727492640658
  (2, 1)	0.41428875116588965
  (3, 9)	0.5773502691896257
  (3, 3)	0.5773502691896257
  (3, 13)	0.5773502691896257
In [54]:
features = tfidf_features.todense() # todense() returns a matrix
features
Out[54]:
matrix([[0.        , 0.        , 0.57735027, 0.        , 0.        ,
         0.57735027, 0.        , 0.        , 0.        , 0.        ,
         0.        , 0.        , 0.57735027, 0.        ],
        [0.        , 0.36673901, 0.        , 0.        , 0.        ,
         0.        , 0.        , 0.46516193, 0.46516193, 0.        ,
         0.46516193, 0.46516193, 0.        , 0.        ],
        [0.52547275, 0.41428875, 0.        , 0.        , 0.52547275,
         0.        , 0.52547275, 0.        , 0.        , 0.        ,
         0.        , 0.        , 0.        , 0.        ],
        [0.        , 0.        , 0.        , 0.57735027, 0.        ,
         0.        , 0.        , 0.        , 0.        , 0.57735027,
         0.        , 0.        , 0.        , 0.57735027]])
In [55]:
dtm_np = np.array(features) # 메트릭스를 nd array로 바꾸기. 텍스트를 벡터화한 결과
dtm_np
Out[55]:
array([[0.        , 0.        , 0.57735027, 0.        , 0.        ,
        0.57735027, 0.        , 0.        , 0.        , 0.        ,
        0.        , 0.        , 0.57735027, 0.        ],
       [0.        , 0.36673901, 0.        , 0.        , 0.        ,
        0.        , 0.        , 0.46516193, 0.46516193, 0.        ,
        0.46516193, 0.46516193, 0.        , 0.        ],
       [0.52547275, 0.41428875, 0.        , 0.        , 0.52547275,
        0.        , 0.52547275, 0.        , 0.        , 0.        ,
        0.        , 0.        , 0.        , 0.        ],
       [0.        , 0.        , 0.        , 0.57735027, 0.        ,
        0.        , 0.        , 0.        , 0.        , 0.57735027,
        0.        , 0.        , 0.        , 0.57735027]])

numpy에는 배열(ndarray)과 행렬(matrix) 객체가 있는데 둘이 헷갈린다. 짚고가자.
ndarray 객체는 다양한 종류의 수치 연산을 위해 고안된 범용 n차 배열이다. (보다 효율적)
반면 matrix 객체는 선형대수 연산을 위해 특별히 공안된 객체다.
참고 글 : studymake 블로그 (https://studymake.tistory.com/408#:~:text=ndarray%EB%8A%94%20%EB%8B%A4%EC%96%91%ED%95%9C%20%EC%A2%85%EB%A5%98%EC%9D%98,%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80%20%EB%AA%87%20%EA%B0%80%EC%A7%80%20%EC%95%88%EB%90%9C%EB%8B%A4.&text=ndarray%20%EB%8A%94%20'*'%EB%8A%94%20%EC%9A%94%EC%86%8C%EA%B0%84%20%EA%B3%B1%EC%85%88%EC%9D%B4%EB%8B%A4.)

In [56]:
dtm_np[0]
Out[56]:
array([0.        , 0.        , 0.57735027, 0.        , 0.        ,
       0.57735027, 0.        , 0.        , 0.        , 0.        ,
       0.        , 0.        , 0.57735027, 0.        ])
In [57]:
dtm_np[1]
Out[57]:
array([0.        , 0.36673901, 0.        , 0.        , 0.        ,
       0.        , 0.        , 0.46516193, 0.46516193, 0.        ,
       0.46516193, 0.46516193, 0.        , 0.        ])

문장에서 단어의 등장 여부, 등장 빈도로 벡터화됐다.
벡터화된 문장들은 사칙연산이 가능하다.

In [58]:
print(np.linalg.norm(dtm_np[1]-dtm_np[0]))
1.4142135623730951
In [60]:
feature_names = tfidf_vectorizer.get_feature_names()
feature_names
Out[60]:
['걱정',
 '공부하고',
 '나는',
 '넣어두는',
 '싶다',
 '안녕',
 '없이',
 '있어',
 '자연어',
 '좋겠지',
 '지금',
 '처리를',
 '한수연',
 '한탄은']
In [61]:
def display_features(features, feature_names):
    df = pd.DataFrame(data=features, columns=feature_names)
    print(df)
    print(type(df))
In [62]:
display_features(features, feature_names)
         걱정      공부하고       나는     넣어두는        싶다       안녕        없이  \
0  0.000000  0.000000  0.57735  0.00000  0.000000  0.57735  0.000000   
1  0.000000  0.366739  0.00000  0.00000  0.000000  0.00000  0.000000   
2  0.525473  0.414289  0.00000  0.00000  0.525473  0.00000  0.525473   
3  0.000000  0.000000  0.00000  0.57735  0.000000  0.00000  0.000000   

         있어       자연어      좋겠지        지금       처리를      한수연      한탄은  
0  0.000000  0.000000  0.00000  0.000000  0.000000  0.57735  0.00000  
1  0.465162  0.465162  0.00000  0.465162  0.465162  0.00000  0.00000  
2  0.000000  0.000000  0.00000  0.000000  0.000000  0.00000  0.00000  
3  0.000000  0.000000  0.57735  0.000000  0.000000  0.00000  0.57735  
<class 'pandas.core.frame.DataFrame'>
같은 corpus로 벡터화를 진행했을 때 CountVectorizer와 TfidfVectorizer를 사용했을 때 결과가 다른 것을 알 수 있다.

TF-IDF 기법을 사용한 BoW 임베딩은 '잠재 의미 분석'에서 많이 활용된다. 



다시 BoW로 돌아가서, BoW 가정의 뉴럴 네트워크 버전은 Deep Averaging Network이다. 이는 다음에 다루겠다. 

처음 이 기사를 읽었을 때 막연히 어렵다고 느꼈는데, 인생사 모든 게 그렇듯 알고나니 별거 아니다.