1. 문장 임베딩
- 2017년 이전의 임베딩 기법들은 대부분 단어 수준의 모델 (Word2Vec, FastText, GloVe)
- 단어 수준의 임베딩 기법은 자연어의 특성인 모호성, 동음이의어를 구분하기 어렵다는 한계가 있음
- 2017년 이후에는 ELMo(Embeddigs from Language Models)와 같은 모델이 발표되고 트랜스포머와 같은 언어 모델에서 문장 수준의 언어 모델링을 고려하면서 한계점들이 해결됨
2. Seq2Seq (Sequence To Sequence)
- 2014년 구글에서 논문으로 제안한 모델
- LSTM(Long Short-Term Memory) 또는 GRU(Gated Recurrent Unit) 기반의 구조를 가지고 고정된 길이의 단어 시퀀스를 입력으로 받아, 입력 시퀀스에 알맞은 길이의 시퀀스를 출력해주는 언어 모델
- 2개의 LSTM을 각각 Encoder와 Decoder로 사용해 가변적인 길이의 입/출력을 처리하고자 했음
- 기계 번역 작업에서 큰 성능 향상을 가져왔고 특히 긴 문장을 처리하는데 강점이 있음
- 논문: https://arxiv.org/abs/1409.3215
1. 등장배경 |
|
2. 인코더 |
|
3. 디코더 |
|
4. 학습방법 |
|
5. 한계점 |
|
1. Seq2Seq 데이터 전처리
◼ import
import os
import re
import shutil
import zipfile
import numpy as np
import pandas as pd
import tensorflow as tf
import unicodedata
import urllib3
from tensorflow.keras.layers import Embedding, GRU, Dense
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.preprocessing.text import Tokenizer
|
◼ fra-eng.zip 다운로드하고 압축해제
: fra-eng.zip 프랑스어-영어 병렬 코퍼스인 파일
( 19만개의 병렬 문장 샘플 / 예) Watch me. Regardez-moi !)
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
def download_zip(url, output_path):
response = requests.get(url, headers=headers, stream=True)
if response.status_code == 200:
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"ZIP file downloaded to {output_path}")
else:
print(f"Failed to download. HTTP Response Code: {response.status_code}")
output_path = "fra-eng.zip"
download_zip(url, output_path)
path = os.getcwd()
zipfilename = os.path.join(path, output_path)
with zipfile.ZipFile(zipfilename, 'r') as zip_ref:
zip_ref.extractall(path)
|
◼ 약 19만개의 데이터 중 33,000개의 샘플만을 사용
num_samples = 33000
|
◼ 전처리 함수 : 구두점 제거 / 단어 구분
def to_ascii(s):
# 프랑스어 악센트(accent) 삭제
# 예시 : 'déjà diné' -> deja dine
return ''.join(c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn')
def preprocess_sentence(sent):
# 악센트 제거 함수 호출
sent = to_ascii(sent.lower())
# 단어와 구두점 사이에 공백 추가.
# ex) "I am a student." => "I am a student ."
sent = re.sub(r"([?.!,¿])", r" \1", sent)
# (a-z, A-Z, ".", "?", "!", ",") 이들을 제외하고는 전부 공백으로 변환.
sent = re.sub(r"[^a-zA-Z!.?]+", r" ", sent)
# 다수 개의 공백을 하나의 공백으로 치환
sent = re.sub(r"\s+", " ", sent)
return sent
|
◼ 임의의 문장 입력테스트
# 전처리 테스트
en_sent = u"Have you had dinner?"
fr_sent = u"Avez-vous déjà diné?"
print('전처리 전 영어 문장 :', en_sent)
print('전처리 후 영어 문장 :',preprocess_sentence(en_sent))
print('전처리 전 프랑스어 문장 :', fr_sent)
print('전처리 후 프랑스어 문장 :', preprocess_sentence(fr_sent))
|
전처리 전 영어 문장 : Have you had dinner? 전처리 후 영어 문장 : have you had dinner ? 전처리 전 프랑스어 문장 : Avez-vous déjà diné? 전처리 후 프랑스어 문장 : avez vous deja dine ? |
◼ 전체 데이터에서 33,000개의 샘플에 대해서 전처리를 수행
훈련 과정에서 교사 강요(Teacher Forcing)을 사용
>> 훈련 시 사용할 디코더의 입력 시퀀스와 실제값. 즉, 레이블에 해당되는 출력 시퀀스를 따로 분리하여 저장
>>입력 시퀀스에는 시작을 의미하는 토큰인 <sos>를 추가,
출력 시퀀스에는 종료를 의미하는 토큰인 <eos>를 추가
def load_preprocessed_data():
encoder_input, decoder_input, decoder_target = [], [], []
with open("fra.txt", "r") as lines:
for i, line in enumerate(lines):
# source 데이터와 target 데이터 분리
src_line, tar_line, _ = line.strip().split('\t')
# source 데이터 전처리
src_line = [w for w in preprocess_sentence(src_line).split()]
# target 데이터 전처리
tar_line = preprocess_sentence(tar_line)
tar_line_in = [w for w in ("<sos> " + tar_line).split()]
tar_line_out = [w for w in (tar_line + " <eos>").split()]
encoder_input.append(src_line)
decoder_input.append(tar_line_in)
decoder_target.append(tar_line_out)
if i == num_samples - 1:
break
return encoder_input, decoder_input, decoder_target
|
◼ 위에서 받은 3개의 데이터셋
인코더의 입력, 디코더의 입력, 디코더의 레이블을 상위 5개 샘플만 출력
sents_en_in, sents_fra_in, sents_fra_out = load_preprocessed_data()
print('인코더의 입력 :',sents_en_in[:5])
print('디코더의 입력 :',sents_fra_in[:5])
print('디코더의 레이블 :',sents_fra_out[:5])
|
인코더의 입력 : [['go', '.'], ['go', '.'], ['go', '.'], ['hi', '.'], ['hi', '.']] 디코더의 입력 : [['<sos>', 'va', '!'], ['<sos>', 'marche', '.'], ['<sos>', 'bouge', '!'], ['<sos>', 'salut', '!'], ['<sos>', 'salut', '.']] 디코더의 레이블 : [['va', '!', '<eos>'], ['marche', '.', '<eos>'], ['bouge', '!', '<eos>'], ['salut', '!', '<eos>'], ['salut', '.', '<eos>']] |
디코더 셀의 입력은 오직 이전 디코더 셀의 출력을 입력으로 받는다고 설명하였는데 디코더의 입력에 해당하는 데이터인 sents_fra_in이 왜 필요할까요? 훈련 과정에서는 이전 시점의 디코더 셀의 출력을 현재 시점의 디코더 셀의 입력으로 넣어주지 않고, 이전 시점의 실제값을 현재 시점의 디코더 셀의 입력값으로 하는 방법을 사용할 겁니다. 그 이유는 이전 시점의 디코더 셀의 예측이 틀렸는데 이를 현재 시점의 디코더 셀의 입력으로 사용하면 현재 시점의 디코더 셀의 예측도 잘못될 가능성이 높고 이는 연쇄 작용으로 디코더 전체의 예측을 어렵게 합니다. 이런 상황이 반복되면 훈련 시간이 느려집니다. 만약 이 상황을 원하지 않는다면 이전 시점의 디코더 셀의 예측값 대신 실제값을 현재 시점의 디코더 셀의 입력으로 사용하는 방법을 사용할 수 있습니다. 이와 같이 RNN의 모든 시점에 대해서 이전 시점의 예측값 대신 실제값을 입력으로 주는 방법을 교사 강요라고 합니다. |
◼ 케라스 토크나이저를 통해 단어 집합을 생성,
정수 인코딩을 진행 후 이어서 패딩을 진행
tokenizer_en = Tokenizer(filters="", lower=False)
tokenizer_en.fit_on_texts(sents_en_in)
encoder_input = tokenizer_en.texts_to_sequences(sents_en_in)
encoder_input = pad_sequences(encoder_input, padding="post")
tokenizer_fra = Tokenizer(filters="", lower=False)
tokenizer_fra.fit_on_texts(sents_fra_in)
tokenizer_fra.fit_on_texts(sents_fra_out)
decoder_input = tokenizer_fra.texts_to_sequences(sents_fra_in)
decoder_input = pad_sequences(decoder_input, padding="post")
decoder_target = tokenizer_fra.texts_to_sequences(sents_fra_out)
decoder_target = pad_sequences(decoder_target, padding="post")
|
◼ 데이터의 크기(shape)를 확인
print('인코더의 입력의 크기(shape) :',encoder_input.shape)
print('디코더의 입력의 크기(shape) :',decoder_input.shape)
print('디코더의 레이블의 크기(shape) :',decoder_target.shape)
|
인코더의 입력의 크기(shape) : (33000, 8) 디코더의 입력의 크기(shape) : (33000, 16) 디코더의 레이블의 크기(shape) : (33000, 16) |
샘플은 총 33,000개 존재하며 영어 문장의 길이는 8, 프랑스어 문장의 길이는 16 |
◼ 단어 집합의 크기를 정의
src_vocab_size = len(tokenizer_en.word_index) + 1
tar_vocab_size = len(tokenizer_fra.word_index) + 1
print("영어 단어 집합의 크기 : {:d}, 프랑스어 단어 집합의 크기 : {:d}".format(src_vocab_size, tar_vocab_size))
|
영어 단어 집합의 크기 : 4647, 프랑스어 단어 집합의 크기 : 8022 |
단어 집합의 크기는 각각 4,647개와 8,022개 단어로부터 정수를 얻는 딕셔너리와 정수로부터 단어를 얻는 딕셔너리를 각각 만들어줍니다. 이들은 훈련을 마치고 예측값과 실제값을 비교하는 단계에서 사용 |
◼ 단어로부터 정수를 얻는 딕셔너리와 정수로부터 단어를 얻는 딕셔너리를 각각 만들어주기
: 훈련을 마치고 예측값과 실제값을 비교하는 단계에서 사용
src_to_index = tokenizer_en.word_index
index_to_src = tokenizer_en.index_word
tar_to_index = tokenizer_fra.word_index
index_to_tar = tokenizer_fra.index_word
|
◼ 테스트 데이터를 분리하기 전 데이터 섞기
순서가 섞인 정수 시퀀스 리스트를 만들기
indices = np.arange(encoder_input.shape[0])
np.random.shuffle(indices)
print('랜덤 시퀀스 :',indices)
|
랜덤 시퀀스 : [16412 5374 8832 ... 5652 24040 10002] |
◼ 데이터셋의 순서로 지정해주면 샘플들이 기존 순서와 다른 순서로 섞이게 됨
encoder_input = encoder_input[indices]
decoder_input = decoder_input[indices]
decoder_target = decoder_target[indices]
|
◼임의로 30,997번째 샘플을 출력
: decoder_input과 decoder_target은 데이터의 구조상으로
앞에 붙은 <sos> 토큰과 뒤에 붙은 <eos>을 제외하면 동일한 정수 시퀀스를 가져야 합니다.
encoder_input[30997]
decoder_input[30997]
decoder_target[30997]
|
array([ 5, 7, 638, 1, 0, 0, 0, 0], dtype=int32) --------------------------------------------------------- array([ 2, 18, 5, 16, 173, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=int32) --------------------------------------------------------- array([ 18, 5, 16, 173, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=int32) |
18, 5, 16, 173, 1이라는 동일 시퀀스를 확인 |
◼ 훈련 데이터의 10%를 테스트 데이터로 분리
n_of_val = int(33000*0.1)
print('검증 데이터의 개수 :',n_of_val)
|
◼ 33,000개의 10%에 해당되는 3,300개의 데이터를 테스트 데이터로 사용
encoder_input_train = encoder_input[:-n_of_val]
decoder_input_train = decoder_input[:-n_of_val]
decoder_target_train = decoder_target[:-n_of_val]
encoder_input_test = encoder_input[-n_of_val:]
decoder_input_test = decoder_input[-n_of_val:]
decoder_target_test = decoder_target[-n_of_val:]
|
◼ 훈련 데이터와 테스트 데이터의 크기(shape)를 출력
print('훈련 source 데이터의 크기 :',encoder_input_train.shape)
print('훈련 target 데이터의 크기 :',decoder_input_train.shape)
print('훈련 target 레이블의 크기 :',decoder_target_train.shape)
print('테스트 source 데이터의 크기 :',encoder_input_test.shape)
print('테스트 target 데이터의 크기 :',decoder_input_test.shape)
print('테스트 target 레이블의 크기 :',decoder_target_test.shape)
|
훈련 source 데이터의 크기 : (29700, 8) 훈련 target 데이터의 크기 : (29700, 16) 훈련 target 레이블의 크기 : (29700, 16) 테스트 source 데이터의 크기 : (3300, 8) 테스트 target 데이터의 크기 : (3300, 16) 테스트 target 레이블의 크기 : (3300, 16) |
훈련 데이터의 샘플은 29,700개, 테스트 데이터의 샘플은 3,300개가 존재 |
2. Seq2Seq 기계 번역기 만들기
◼ import
from tensorflow.keras.layers import Input, LSTM, Embedding, Dense, Masking
from tensorflow.keras.models import Model
|
◼ 임베딩 벡터의 차원과 LSTM의 은닉 상태의 크기를 64로 사용
embedding_dim = 64
hidden_units = 64
|
◼ 인코더 설계
# 인코더
encoder_inputs = Input(shape=(None,))
enc_emb = Embedding(src_vocab_size, embedding_dim)(encoder_inputs) # 임베딩 층
enc_masking = Masking(mask_value=0.0)(enc_emb) # 패딩 0은 연산에서 제외
encoder_lstm = LSTM(hidden_units, return_state=True) # 상태값 리턴을 위해 return_state는 True
encoder_outputs, state_h, state_c = encoder_lstm(enc_masking) # 은닉 상태와 셀 상태를 리턴
encoder_states = [state_h, state_c] # 인코더의 은닉 상태와 셀 상태를 저장
|
◼ 디코더 설계
# 디코더
decoder_inputs = Input(shape=(None,))
dec_emb_layer = Embedding(tar_vocab_size, hidden_units) # 임베딩 층
dec_emb = dec_emb_layer(decoder_inputs) # 패딩 0은 연산에서 제외
dec_masking = Masking(mask_value=0.0)(dec_emb)
# 상태값 리턴을 위해 return_state는 True, 모든 시점에 대해서 단어를 예측하기 위해 return_sequences는 True
decoder_lstm = LSTM(hidden_units, return_sequences=True, return_state=True)
# 인코더의 은닉 상태를 초기 은닉 상태(initial_state)로 사용
decoder_outputs, _, _ = decoder_lstm(dec_masking,
initial_state=encoder_states)
# 모든 시점의 결과에 대해서 소프트맥스 함수를 사용한 출력층을 통해 단어 예측
decoder_dense = Dense(tar_vocab_size, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)
# 모델의 입력과 출력을 정의.
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['acc'])
|
◼ 모델을 훈련 ( 128개의 배치 크기로 총 50 에포크 학습 )
model.fit(x=[encoder_input_train, decoder_input_train], y=decoder_target_train, \
validation_data=([encoder_input_test, decoder_input_test], decoder_target_test),
batch_size=128, epochs=50)
|
Epoch 1/50 233/233 [==============================] - 140s 561ms/step - loss: 3.3757 - acc: 0.6155 - val_loss: 2.0012 - val_acc: 0.6233 Epoch 2/50 233/233 [==============================] - 127s 544ms/step - loss: 1.8427 - acc: 0.6804 - val_loss: 1.7148 - val_acc: 0.7446 Epoch 3/50 233/233 [==============================] - 130s 558ms/step - loss: 1.6393 - acc: 0.7459 - val_loss: 1.5574 - val_acc: 0.7590 ..... Epoch 46/50 233/233 [==============================] - 129s 552ms/step - loss: 0.3386 - acc: 0.9180 - val_loss: 0.7202 - val_acc: 0.8728 Epoch 47/50 233/233 [==============================] - 132s 565ms/step - loss: 0.3304 - acc: 0.9196 - val_loss: 0.7188 - val_acc: 0.8730 Epoch 48/50 233/233 [==============================] - 130s 558ms/step - loss: 0.3231 - acc: 0.9211 - val_loss: 0.7218 - val_acc: 0.8722 Epoch 49/50 233/233 [==============================] - 140s 599ms/step - loss: 0.3165 - acc: 0.9224 - val_loss: 0.7165 - val_acc: 0.8737 Epoch 50/50 233/233 [==============================] - 130s 557ms/step - loss: 0.3092 - acc: 0.9239 - val_loss: 0.7177 - val_acc: 0.8740 <keras.src.callbacks.History at 0x78f3a077db10> |
최종 에포크에서 훈련 데이터는 92%의 정확도를, 테스트 데이터에서는 87%의 정확도를 얻었습니다. |
3. Seq2Seq 기계 번역동작시키기
seq2seq는 훈련 과정(교사 강요)과 테스트 과정에서의 동작 방식이 다릅니다.
그래서 테스트 과정을 위해 모델을 다시 설계해주어야 합니다.
특히 디코더를 수정해야 합니다. 이번에는 번역 단계를 위해 모델을 수정하고 동작시켜보겠습니다.
◼ 모델을 다시 설계
# 인코더
encoder_model = Model(encoder_inputs, encoder_states)
# 디코더 설계 시작
# 이전 시점의 상태를 보관할 텐서
decoder_state_input_h = Input(shape=(hidden_units,))
decoder_state_input_c = Input(shape=(hidden_units,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
# 훈련 때 사용했던 임베딩 층을 재사용
dec_emb2 = dec_emb_layer(decoder_inputs)
# 다음 단어 예측을 위해 이전 시점의 상태를 현 시점의 초기 상태로 사용
decoder_outputs2, state_h2, state_c2 = decoder_lstm(dec_emb2, initial_state=decoder_states_inputs)
decoder_states2 = [state_h2, state_c2]
# 모든 시점에 대해서 단어 예측
decoder_outputs2 = decoder_dense(decoder_outputs2)
# 수정된 디코더
decoder_model = Model(
[decoder_inputs] + decoder_states_inputs,
[decoder_outputs2] + decoder_states2)
|
◼ 테스트 단계에서의 동작을 위한 decode_sequence 함수 구현
def decode_sequence(input_seq):
# 입력으로부터 인코더의 마지막 시점의 상태(은닉 상태, 셀 상태)를 얻음
states_value = encoder_model.predict(input_seq)
# <SOS>에 해당하는 정수 생성
target_seq = np.zeros((1,1))
target_seq[0, 0] = tar_to_index['<sos>']
stop_condition = False
decoded_sentence = ''
# stop_condition이 True가 될 때까지 루프 반복
# 구현의 간소화를 위해서 이 함수는 배치 크기를 1로 가정합니다.
while not stop_condition:
# 이점 시점의 상태 states_value를 현 시점의 초기 상태로 사용
output_tokens, h, c = decoder_model.predict([target_seq] + states_value)
# 예측 결과를 단어로 변환
sampled_token_index = np.argmax(output_tokens[0, -1, :])
sampled_char = index_to_tar[sampled_token_index]
# 현재 시점의 예측 단어를 예측 문장에 추가
decoded_sentence += ' '+sampled_char
# <eos>에 도달하거나 정해진 길이를 넘으면 중단.
if (sampled_char == '<eos>' or
len(decoded_sentence) > 50):
stop_condition = True
# 현재 시점의 예측 결과를 다음 시점의 입력으로 사용하기 위해 저장
target_seq = np.zeros((1,1))
target_seq[0, 0] = sampled_token_index
# 현재 시점의 상태를 다음 시점의 상태로 사용하기 위해 저장
states_value = [h, c]
return decoded_sentence
|
◼ 결과 확인을 위한 함수
# 원문의 정수 시퀀스를 텍스트 시퀀스로 변환
def seq_to_src(input_seq):
sentence = ''
for encoded_word in input_seq:
if(encoded_word != 0):
sentence = sentence + index_to_src[encoded_word] + ' '
return sentence
# 번역문의 정수 시퀀스를 텍스트 시퀀스로 변환
def seq_to_tar(input_seq):
sentence = ''
for encoded_word in input_seq:
if(encoded_word != 0 and encoded_word != tar_to_index['<sos>'] and encoded_word != tar_to_index['<eos>']):
sentence = sentence + index_to_tar[encoded_word] + ' '
return sentence
|
◼ 훈련 데이터에 대해서 임의로 선택한 인덱스의 샘플의 결과를 출력
for seq_index in [3, 50, 100, 300, 1001]:
input_seq = encoder_input_train[seq_index: seq_index + 1]
decoded_sentence = decode_sequence(input_seq)
print("입력문장 :",seq_to_src(encoder_input_train[seq_index]))
print("정답문장 :",seq_to_tar(decoder_input_train[seq_index]))
print("번역문장 :",decoded_sentence[1:-5])
print("-"*50)
|
입력문장 : when does it end ? 정답문장 : quand est ce que ca finit ? 번역문장 : quand est ce que ca marche ? -------------------------------------------------- 입력문장 : it s sand . 정답문장 : c est du sable . 번역문장 : c est de l eau . -------------------------------------------------- 입력문장 : i didn t go . 정답문장 : je n y suis pas allee . 번역문장 : je ne suis pas encore . -------------------------------------------------- 입력문장 : it was a mistake . 정답문장 : ce fut une erreur . 번역문장 : il s agit d une blague . -------------------------------------------------- 입력문장 : it boggles my mind . 정답문장 : ca me laisse perplexe . 번역문장 : ca m en femme . -------------------------------------------------- |
◼ 테스트 데이터에 대해서 임의로 선택한 인덱스의 샘플의 결과를 출력
for seq_index in [3, 50, 100, 300, 1001]:
input_seq = encoder_input_test[seq_index: seq_index + 1]
decoded_sentence = decode_sequence(input_seq)
print("입력문장 :",seq_to_src(encoder_input_test[seq_index]))
print("정답문장 :",seq_to_tar(decoder_input_test[seq_index]))
print("번역문장 :",decoded_sentence[1:-5])
print("-"*50)
|
입력문장 : we are busy men . 정답문장 : nous sommes des hommes occupes . 번역문장 : nous sommes tres vieux . ------------------------------------------------- 입력문장 : it was very ugly . 정답문장 : ce n etait vraiment pas beau a voir . 번역문장 : c etait tres fort . -------------------------------------------------- 입력문장 : tom looks shocked . 정답문장 : tom a l air choque . 번역문장 : tom a l air bien . -------------------------------------------------- 입력문장 : cross the street . 정답문장 : traversez la rue . 번역문장 : la ? -------------------------------------------------- 입력문장 : you nearly died . 정답문장 : tu es presque mort . 번역문장 : tu es presque mort . -------------------------------------------------- |
ㄴ 출처: https://wikidocs.net/86900
◼ Seq2Seq(sequence-to-sequence) 모델을 구현
class Encoder(nn.Module):
def __init__(self, input_size, hidden_size):
super(Encoder, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.embedding = nn.Embedding(input_size, hidden_size)
self.gru = nn.GRU(input_size, hidden_size)
def forward(self, input):
embedded = self.embedding(input).view(1, input.size(0), self.hidden_size)
output, hidden = self.gru(input, embedded)
return output, hidden
class Decoder(nn.Module):
def __init__(self, hidden_size, output_size):
super(Decoder, self).__init__()
self.hidden_size = hidden_size
self.output_size = output_size
self.embedding = nn.Embedding(output_size, hidden_size)
self.gru = nn.GRU(tar_vocab_size, hidden_size)
self.out = nn.Linear(hidden_size, output_size)
def forward(self, input, hidden):
output = self.embedding(input).view(1, 1, -1)
output = F.relu(output)
output, hidden = self.gru(output, hidden)
output = self.out(output[0])
return output, hidden
|
'AI > 자연어처리' 카테고리의 다른 글
14. 문장 임베딩 | ELmo / Transformer (1) | 2024.07.04 |
---|---|
13. 문장 임베딩 | Attention Meshanism (0) | 2024.07.04 |
11. LSTM과 GRU (0) | 2024.07.02 |
10. CNN Text Classification (0) | 2024.07.02 |
09. CBOW Text Classification (0) | 2024.07.01 |