텍스트의 토큰화
# 주어진 문장을 단어로 토큰화 하기
# 케라스의 텍스트 전처리와 관련한 함수 중 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)
- 단어 임베딩은 주어진 배열을 정해진 길이로 압축시킴
단어 임베딩 이란? 텍스트를 구성하는 하나의 단어를 수치화 하는 방법의 일종
- 단어의 의미를 전혀 고려하지 않는 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> 의 인덱스이다)
'Natural Language Processing' 카테고리의 다른 글
[Goorm] 딥러닝을 이용한 자연어 처리 3 (IMDB + GloVe 이용) (1) | 2024.07.05 |
---|---|
[Goorm] 딥러닝을 이용한 자연어 처리 2 (IMDB 이용) (0) | 2024.07.05 |
[ChatGPT] 프롬프트 이용하여 시스템 만들기 (0) | 2023.08.04 |
[ChatGPT] Prompt Engineering (0) | 2023.08.03 |
[NLP] Transformer 트랜스포머 모델 (0) | 2023.08.01 |