본문 바로가기
Natural Language Processing

[Goorm] 딥러닝을 이용한 자연어 처리 1 (토큰화 & 임베딩)

by 자몽먹은토끼 2024. 6. 23.
728x90
반응형
텍스트의 토큰화
# 주어진 문장을 단어로 토큰화 하기
# 케라스의 텍스트 전처리와 관련한 함수 중 text_to_word_sequence 함수를 불러온다
from tensorflow.keras.preprocessing.text import text_to_word_sequence

# 전처리할 텍스트를 정합니다.
text= '해보지 않으면 해낼 수 없다.'

# 해당 텍스트를 토큰화
result= text_to_word_sequence(text)
print('원문', text)
print('토큰화',result)



# 결과
"""
원문 해보지 않으면 해낼 수 없다.
토큰화 ['해보지', '않으면', '해낼', '수', '없다']
"""

> text_to_word_sequence : 문장에서 단어 단위로 토큰화

( Tokenizer()를 선언하지 않고 사용 가능)

 

import numpy
import tensorflow as tf
from numpy import array
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten, Embedding

# 단어 빈도 수 세기
# 전처리 하려는 세개의 문장을 정합니다
docs= ['먼저 먼저 텍스트의 각 단어를 나누어 토큰화 합니다.',
       '텍스트의 단어로 토큰화 해야 딥러닝에서 인식됩니다.',
       '토큰화 한 결과는 딥러닝에서 사용할 수 있습니다.'
       ]

# 토큰화 함수를 이용한 전처리
token= Tokenizer()
token.fit_on_texts(docs)
print('단어별 카운트 : ', token.word_counts)        # OrderedDict형식으로 반환
print('문장 카운트: ',token.document_count)
print('각 단어가 몇개의 문장에 포함되어 있는가', token.word_docs)
print('단어 인덱스: ',token.word_index)              # 많이 나온 순서대로 1번 인덱스부터 부여
print('문장 인덱스: ',token.texts_to_sequences(docs))
# 출력 결과 
단어별 카운트 :  OrderedDict([('먼저', 2), ('텍스트의', 2), ('각', 1), ('단어를', 1), ('나누어', 1), ('토큰화', 3), ('합니다', 1), ('단어로', 1), ('해야', 1), ('딥러닝에서', 2), ('인식됩니다', 1), ('한', 1), ('결과는', 1), ('사용할', 1), ('수', 1), ('있습니다', 1)])
문장 카운트:  3
각 단어가 몇개의 문장에 포함되어 있는가 defaultdict(<class 'int'>, {'토큰화': 3, '단어를': 1, '먼저': 1, '합니다': 1, '텍스트의': 2, '나누어': 1, '각': 1, '해야': 1, '단어로': 1, '인식됩니다': 1, '딥러닝에서': 2, '있습니다': 1, '결과는': 1, '한': 1, '사용할': 1, '수': 1})
단어 인덱스:  {'토큰화': 1, '먼저': 2, '텍스트의': 3, '딥러닝에서': 4, '각': 5, '단어를': 6, '나누어': 7, '합니다': 8, '단어로': 9, '해야': 10, '인식됩니다': 11, '한': 12, '결과는': 13, '사용할': 14, '수': 15, '있습니다': 16}
문장 인덱스:  [[2, 2, 3, 5, 6, 7, 1, 8], [3, 9, 1, 10, 4, 11], [1, 12, 13, 4, 14, 15, 16]]

> token.fit_on_texts(docs) : 입력 문장을 단어단위로 토큰화하여 Tokenizer()에 적용

> token.word_counts : 단어 토큰 별로 몇번 나왔는지 (value_counts)

> token.document_count : 몇개의 문장 시퀀스

> token.word_docs : 단어 토큰이 몇개의 문장에 포함되어 있는가 ('먼저' 라는 단어에서 차이가 있음을 확인할 수 있다.)

> token.word_index : 단어토큰-인덱스 로 구성된 딕셔너리

> token.texts_to_sequences(docs) : 문장별로 어떤 인덱스의 토큰이 들어가 있는가

 

 

 

단어의 원-핫 인코딩

 

from tensorflow.keras.utils import to_categorical
x= token.texts_to_sequences([text])

# 원-핫 인코딩 배열
word_size= len(token.word_index)+1
y= to_categorical(x, num_classes=word_size)
print(y)
# [[1,2,3,4,5,6]]의 원-핫 인코딩 결과

# 출력 결과
[[[0. 1. 0. 0. 0. 0. 0.]
  [0. 0. 1. 0. 0. 0. 0.]
  [0. 0. 0. 1. 0. 0. 0.]
  [0. 0. 0. 0. 1. 0. 0.]
  [0. 0. 0. 0. 0. 1. 0.]
  [0. 0. 0. 0. 0. 0. 1.]]]

> to_categorical( x ) : 각 단어토큰을 인덱스로 변환한 리스트를 원-핫 인코딩

 

 

 

 

단어 임베딩

 

  • 원-핫 인코딩을 그대로 사용하면 벡터의 길이가 너무 길어진다.
  • 공간적 낭비를 해결하기 위해 등장한 것이 단어 임베딩(word embedding) 
  • 단어 임베딩은 주어진 배열을 정해진 길이로 압축시킴

0과 1로 이루어진 벡터를 실수 형식의 벡터로 바꿈

 

단어 임베딩 이란? 텍스트를 구성하는 하나의 단어를 수치화 하는 방법의 일종

  • 단어의 의미를 전혀 고려하지 않는 one-hot encoding과 달리, 단어 임베딩은 단어의 의미를 고려하여 좀 더 조밀한 차원에 단어를 벡터로 표현
  • 비슷한 의미의 단어들은 비슷한 벡터로 표현이 되고, 더 나아가 단어와 단어 간의 관계가 벡터를 통해서 드러난다.
  • 단어를 벡터로 바꾸는 모델을 단어 임베딩 모델(word embedding model)이라고 부른다. word2vec는 단어 임베딩 모델들 중 대표적인 모델이다.
 
 
  원-핫 벡터 임베딩 벡터
차원 고차원 (단어 집합의 크기 = 단어 사전의 길이) 저차원
다른 표현 희소벡터의 일종 (sparse representation) 밀집벡터의 일종 (dense representation)
표현 방법 수동 훈련 데이터로 부터 학습
값의 타입 0과 1 실수

 

  • 임베딩 벡터에서 사용되는 실수 값은 학습데이터를 학습하면서 오차 역전파 과정을 통해 유사도를 계산하게 된다.
  • 임베딩 벡터는 초기에는 랜덤의 값을 가지다가, 인공신경망의 가중치가 학습되는 방법과 같은 방식으로 점차 값이 바뀐다.

 

 

 

 

 

 

실습

 

영화 리뷰 긍/부정 판단하기

(학습)

from numpy import array
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, Flatten, Dense

docs= ['너무 재밌네요','최고예요','참 잘 만든 영화예요','추천하고 싶은 영화입니다.',
       '한번 더 보고싶네요','글쎄요','별로에요','생각보다 지루하네요','연기가 어색해요',
       '재미없어요']

# 긍정리뷰는 1, 부정리뷰는 0
classes = array([1,1,1,1,1,0,0,0,0,0])

# 인덱스화
token= Tokenizer()
token.fit_on_texts(docs)
print('token', token.word_index)

# 토큰화
x= token.texts_to_sequences(docs)
print('토큰화', x)

# 패딩
max_length= len(max(x,key= len))
pad_x= pad_sequences(x,max_length)
print('패딩결과', pad_x)

model = Sequential()
model.add(Embedding(len(token.word_index)+1, 4, input_length=max_length))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(pad_x, classes, epochs=20, batch_size=1)
입출력 데이터 선언 > 텍스트데이터 인덱스화 > 문장 시퀀스별로 인덱스화 적용 > 길이 맞추기 (패딩) > 임베딩 > 학습

"I like a banana" > {"i" : 1, "like" : 2, "a" : 3, "banana" : 4} > [[ 1, 2, 3, 4 ]] > [[0, 1, 2, 3, 4 ]] > [ [0.1, 0.5, 1.0 1.6], [...],[...],[...],[...] ]

 

(테스트)

# 새로운 문장을 사용한 검증
new_reviews=['참 재밌네요','별로였어요','지루하고 재미없어요','퀄리티가 높네요']
# 시퀀스 별로 기존 인덱스 할당
new_sequences= token.texts_to_sequences(new_reviews)
# 패딩작업으로 길이 맞추기
pad_new_data= pad_sequences(new_sequences,max_length)


predictoins= model.predict(pad_new_data)
# 예측결과 출력
for review, prediction in zip(new_reviews,predictoins):
  print(review,'->','긍정' if prediction>0.5 else '부정')
테스트 시에는 문장 시퀀스 별로 학습 인덱스 적용 후 > 패딩 > 예측
(없는 토큰에 경우, 인덱스 부여x ? , 뒤에 나오겠지만 unk 토큰으로 변환)

 

> 패딩 시, 짧은 문장의 경우 0으로 채워 원하는 길이로 맞추고, 긴 문장의 경우 문장 뒤부터 해당길이만큼을 가져온다.

 

 

 

 

 

ETC

 

1. Tokenizer의 파라미터 설정

# 토큰화
token= Tokenizer(
    num_words= None,                                      # 단어 빈도가 많은 상위 몇개만 가져오기
    filters='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n',       # 사용 x
    lower= False,                                         # 소문자로 변환할 것인가 (default : True)
    split=' ',
    char_level=False,                                     # 모든 문자를 토큰으로 처리할 것인가
    oov_token=None,                                       # text_to_sequence 호출 시, 단어사전에 없는 토큰을 어떻게 처리할 것인지 (out-of-vocabulary)
    document_count = 0
)

> num_words : 최다 빈도 단어 상위 n개의 단어를 제외하고 나머지 단어들은 데이터에서 제거한다.

( 따라서 가져온 데이터를 확인해보면 문장 문백상 완전하지 않은 문장일 수도 있다.)

 

 

 

 

2. 해싱으로 벡터 만들기

  • 원-핫 인코딩의 변종 중 하나는 원-핫 해싱(one-hot hashing) 기법이다.
  • 이 방식은 어휘사전에 있는 고유한 토큰의 수가 너무 커서 모두 다루기 어려울 때 사용한다.
  • 각 단어에 명시적으로 인덱스를 할당하고, 이 인덱스를 딕셔너리에 저장하는 대신에
    단어를 해싱하여 고정된 크기의 벡터로 변환한다. (해시함수 사용)
  • 명시적인 단어 인덱스가 필요 없기 때문에 메모리를 절약하고 온라인 방식으로 데이터를 인코딩 할 수 있다. 
  • 하지만 해시 충돌의 위험성이 있다. (hashcollision)
    • 2개 이상의 단어가 같은 해시를 만들면 모델은 이 단어 간의 차이를 인식하지 못한다.
    • 해싱공간의 차원이 해싱될 고유 토큰의 전체 개수보다 훨씬 크면 해시 충돌의 가능성은 감소한다.
      (나누는 값의 크기가 크면 해싱함수 결과 분포가 커짐)
dim= 1000
max_len= 10

results= np.zeros((len(samples), max_len, dim))
for i, sample in enumerate(samples):
    print('sample',sample)
    for j, word in list(enumerate(sample.split()))[:max_len]:
        print('word:',word)
        # 해시함수 사용 (단어를 해싱하여 0과 1000사이의 랜덤한 정수 인덱스로 변환)
        index= abs(hash(word)) % dim
        results[i, j, index] = 1
print(results)

< 해시함수>

  • 데이터의 효율적 관리를 목적으로 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수
  • 이때 매핑 전 원래 데이터의 값을 키(key), 매핑 후 데이터의 값을 해시값(hash value),
    매핑하는 과정 자체를 해싱(hashing) 라고 한다.
  • 해시함수는 해쉬값의 갸수보다 대개 많은 키값을 해쉬값으로 변환하기 때문에 해시함수가 서로 다른 두개의 키에 대해 동일한 해시값을 내는 해시충돌(collision)이 발생한다.

> 암호화 라고 생각하면 쉬움!

 

 

3. 테스트 데이터 검증 시, 해당 토큰이 인덱스화 되어있지 않은 경우

word_index= imdb.get_word_index()

# 인덱스를 단어로 변환하는 딕셔너리 생성
index_to_word= {index + 3: word for word, index in word_index.items()}
# 이미 이렇게 사용되고 있음 (맞춰준거임)
index_to_word[0]= "<PAD>"
index_to_word[1]= "<START>"
index_to_word[2]= "<UNK>"
index_to_word[3]= "<UNUSED>"

# 새로운 리뷰를 사용한 검증
new_reviews= ["It was a boring movie. I don't want to recommenc it.",
              "The movie was extremely disappointing."]

new_sequences= []

# 시퀀스별 인덱스화   # token.texts_to_sequences(new_reviews)
for review in new_reviews:
    # tokens= [word_index[word.lower()] for word in review.split()]
    tokens= [word_index.get(word.lower(), 2) for word in review.split()]       # 찾으려는 값이 없을 경우 2(UNK)를 반환 (get함수: 딕셔너리에서 값 찾기 + 예외처리)
    new_sequences.append(tokens)

# 패딩
pad_seq= preprocessing.sequence.pad_sequences(new_sequences, maxlen= maxlen)

# 예측
model.predict(pad_seq)

> get() 함수를 사용하여 예외처리

> 소문자 형태로 가져오되, 딕셔너리에 해당 키가 없을 시 반환값을 2로 대체함 ( 2는 <UNK> 의 인덱스이다)

728x90
반응형