데이터 불러오기

우선 번역기를 만드려면 두개의 언어가 병렬적으로 구성된 데이터가 필요하다. 오늘 사용할 데이터는 ‘프랑스어-영어’ 로 구성된 데이터를 사용하겠다.
데이터는 (http://www.manythings.org/anki/fra-eng.zip) 에서 받을 수 있다.
압축을 풀면 파일이 2개 나오는데 fra.txt파일만 사용한다.

import tensorflow as tf
from tensorflow.keras.layers import Embedding, GRU, Dense
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.preprocessing.text import Tokenizer
import unicodedata
import numpy as np
import pandas as pd
import re
2023-02-26 15:33:32.367543: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcudart.so.11.0

파일 구성은 source인 영어문장과 target인 프랑스문장으로 이루어져 있으며, lic는 license(?)를 뜻하는 것 같다. 여기서는 source와 target 데이터만 사용한다.

lines = pd.read_csv('./fra.txt', names=['src', 'tar', 'lic'], sep='\t')
print('전체 샘플의 개수 :',len(lines))
lines.sample(5)
전체 샘플의 개수 : 208906
src tar lic
131326 It fell short of my expectation. Ce ne fut pas à la mesure de mes attentes. CC-BY 2.0 (France) Attribution: tatoeba.org #4...
137695 I've been feeling bad for a week. Je me suis senti mal pendant une semaine. CC-BY 2.0 (France) Attribution: tatoeba.org #4...
46110 More money is needed. Il faut plus d'argent. CC-BY 2.0 (France) Attribution: tatoeba.org #8...
41300 What's your problem? C'est quoi ton problème ? CC-BY 2.0 (France) Attribution: tatoeba.org #3...
169231 This page was intentionally left blank. Cette page a été laissée vide intentionnellement. CC-BY 2.0 (France) Attribution: tatoeba.org #7...

데이터 전처리

def preprocess_sentence(sentence):
    # 악센트 제거
    s = ''.join(c for c in unicodedata.normalize('NFD', sentence.lower()) if unicodedata.category(c) != "Mn")
    
    # 단어와 구두점 사이에 공백추가
    s = re.sub(r"([?.!,¿])", r" \1", s)
    
    # a-z, A-Z, . ! ? , 을 제외하고 전부 공백으로 변환
    s = re.sub(r"[^a-zA-Z!.?,']+", r" ", s)
    
    # 다수의 공백을 하나의 공백으로 변환
    s = re.sub(r"\s+", " ", s)
    
    return s
# 전처리 테스트
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 ?

전체 데이터 중에서 100000개만 사용하겠다. 또한 교사 강요법을 사용할 예정이므로, 훈련시 사용할 디코더의 입력 시퀀스에는 sos토큰을 추가하고 출력 시퀀스에는 eos토큰을 추가한다.

num_samples = 100000
def load_preprocessed_data():
    encoder_input, decoder_input, decoder_target = [], [], []
    with open("./fra.txt", "r") as lines:
        for i, line in enumerate(lines):
            # print(f"원문: {line}")
            src_line, tar_line, _ = line.strip().split('\t')    # 원본문장과 번역문장 데이터로 분리
            # print(f"원본문장: {src_line}")
            # print(f"번역문장: {tar_line}")
            
            src_line = [w for w in preprocess_sentence(src_line).split()]   # 인코더의 입력으로 들어갈 원본문장 전처리
            # print(f"인코더입력: {src_line}")
            
            tar_line = preprocess_sentence(tar_line)    # 디코더에 사용될 번역문장 전처리
            tar_line_in = [w for w in ("<sos> " + tar_line).split()]    # 디코더의 입력으로 들어갈 문장에 <sos>토큰을 포함
            # print(f"디코더입력: {tar_line_in}")
            tar_line_out = [w for w in (tar_line + " <eos>").split()]   # 디코더의 정답으로 쓰일 문장에 <eos>토큰을 포함
            # print(f"디코더레이블: {tar_line_out}")
            
            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
원문: Go.	Va !	CC-BY 2.0 (France) Attribution: tatoeba.org #2877272 (CM) & #1158250 (Wittydev)

원본문장: Go.
번역문장: Va !
인코더입력: ['go', '.']
디코더입력: ['<sos>', 'va', '!']
디코더레이블: ['va', '!', '<eos>']

각각 5개씩 샘플을 출력해보자.

en_in, de_in, de_tar = load_preprocessed_data()
print('인코더의 입력 :',en_in[:5])
print('디코더의 입력 :',de_in[:5])
print('디코더의 레이블 :',de_tar[:5])
인코더의 입력 : [['go', '.'], ['go', '.'], ['go', '.'], ['go', '.'], ['hi', '.']]
디코더의 입력 : [['<sos>', 'va', '!'], ['<sos>', 'marche', '.'], ['<sos>', 'en', 'route', '!'], ['<sos>', 'bouge', '!'], ['<sos>', 'salut', '!']]
디코더의 레이블 : [['va', '!', '<eos>'], ['marche', '.', '<eos>'], ['en', 'route', '!', '<eos>'], ['bouge', '!', '<eos>'], ['salut', '!', '<eos>']]

토크나이저

인코더에 들어갈 데이터를 토큰화 한다.

en_tokenizer = Tokenizer(filters="", lower=False)
# 인코더 단어 집합을 정수 인코딩
en_tokenizer.fit_on_texts(en_in)
# 텍스트 안의 단어를 정수 인코딩으로 매핑
encoder_input = en_tokenizer.texts_to_sequences(en_in)
# 패딩 설정
encoder_input = pad_sequences(encoder_input, padding='post')

디코더에 들어갈 데이터를 토큰화 한다. 디코더에는 input과 output 두개가 있다.
훈련 과정에서는 이전 시점의 디코더 셀을 현재 시점의 디코더 셀의 입력으로 넣지 않고 실제값을 넣는 방법을 사용한다. 그 이유는 만약 이전 시점의 디코더 셀의 예측이 틀렸는데 틀린 값을 다음으로 사용하면 연쇄적으로 틀릴 가능성이 있기 때문이다.

# 디코더 토크나이저
de_tokenizer = Tokenizer(filters="", lower=False)
# 디코더입력 단어 집합을 정수 인코딩
de_tokenizer.fit_on_texts(de_in)
# 디코더출력 단어 집합을 정수 인코딩
de_tokenizer.fit_on_texts(de_tar)

# 디코더 입력 단어를 정수 인코딩으로 매핑
decoder_input = de_tokenizer.texts_to_sequences(de_in)
# 패딩 설정
decoder_input = pad_sequences(decoder_input, padding="post")
# 디코더 출력 단어를 정수 인코딩으로 매핑
decoder_target = de_tokenizer.texts_to_sequences(de_tar)
# 패딩 설정
decoder_target = pad_sequences(decoder_target, padding="post")
print('인코더의 입력의 크기(shape) :',encoder_input.shape)
print('디코더의 입력의 크기(shape) :',decoder_input.shape)
print('디코더의 레이블의 크기(shape) :',decoder_target.shape)
인코더의 입력의 크기(shape) : (100000, 10)
디코더의 입력의 크기(shape) : (100000, 17)
디코더의 레이블의 크기(shape) : (100000, 17)
src_vocab_size = len(en_tokenizer.word_index) + 1
tar_vocab_size = len(de_tokenizer.word_index) + 1
print(f"영어 단어 집합의 크기 : {src_vocab_size} \n프랑스어 단어 집합의 크기 : {tar_vocab_size}")
영어 단어 집합의 크기 : 9065 
프랑스어 단어 집합의 크기 : 16383

decoder_input의 맨앞 2는 sos토큰이고 decoder_target의 맨 뒤 3은 eos토큰을 나타낸다. sos토큰과 eos토큰을 뺀 나머지는 같아야 한다.

print(decoder_input[30997])
print(decoder_target[30997])
[  2 172  14 236 674 395  15   5   0   0   0   0   0   0   0   0   0]
[172  14 236 674 395  15   5   3   0   0   0   0   0   0   0   0   0]

단어로부터 정수를 얻는 딕셔너리와 정수로부터 단어를 얻는 딕셔너리를 만든다.
이것들은 예측값과 실제값을 비교할 때 사용된다.

# 단어 -> 정수
src_to_index = en_tokenizer.word_index
tar_to_index = de_tokenizer.word_index
# 정수 -> 단어
index_to_src = en_tokenizer.index_word
index_to_tar = de_tokenizer.index_word

데이터 분리

데이터를 랜덤으로 셔플한다.

# 데이터 셔플
indices = np.arange(encoder_input.shape[0])
np.random.shuffle(indices)

encoder_input = encoder_input[indices]
decoder_input = decoder_input[indices]
decoder_target = decoder_target[indices]

데이터 100000개 중 30%를 테스트 데이터로 사용한다.

n_of_val = int(num_samples*0.3)

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:]

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 데이터의 크기 : (70000, 10)
훈련 target 데이터의 크기 : (70000, 17)
훈련 target 레이블의 크기 : (70000, 17)
테스트 source 데이터의 크기 : (30000, 10)
테스트 target 데이터의 크기 : (30000, 17)
테스트 target 레이블의 크기 : (30000, 17)

모델 설계

functional API를 사용하여 모델을 설계한다. Sequential 모델로 만드는것 보다 자유로운 커스텀이 가능한 것이 장점이다.

from tensorflow.keras.layers import Input, LSTM, Embedding, Dense, Masking
from tensorflow.keras.models import Model

인코더 모델을 설계한다. 각 라인의 코드마다 설명을 해놓았다.

  1. 인코더 input layer 설정(None은 임의의 스칼라가 될 수 있음을 명시. 테스트 중에 입력shape을 자유롭게 선택가능)
  2. input layer에서 온 데이터를 64차원으로 임베딩
  3. Masking layer는 임베딩 벡터에서 온 값 중, 패딩 0은 연산에서 제외하는 역할을 함
  4. hidden units가 64인 LSTM layer 생성. 인코더의 hidden state를 디코더에 넘겨야 하기 때문에 return_state=True로 설정.
  5. 생성한 LSTM에 Masking layer에서 계산된 데이터를 넣으면 3개의 값을 리턴함.
  6. encoder_outputs은 필요 없으니 사용하지 않고 hidden state와 cell state를 저장. 이것이 컨텍스트 벡터이고, 디코더에 전달됨
encoder_inputs = Input(shape=(None,))
enc_emb = Embedding(src_vocab_size, 64)(encoder_inputs)
enc_masking = Masking(mask_value=0.0)(enc_emb)
encoder_lstm = LSTM(64, return_state=True)
encoder_outputs, state_h, state_c = encoder_lstm(enc_masking)
encoder_states = [state_h, state_c]

디코더 모델을 설계한다.

  1. 디코더 input layer 설정
  2. input layer에서 온 데이터를 64차원으로 임베딩
  3. Masking layer는 임베딩 벡터에서 온 값 중, 패딩 0은 연산에서 제외하는 역할을 함
  4. hidden units이 64인 LSTM을 생성. 상태값과 모든 시점에 대한 값이 필요하므로 return_statereturn_sequencesTrue로 설정
  5. 생성된 LSTM에 Masking layer에서 계산된 데이터를 넣고, 초기상태를 인코더의 컨텍스트 벡터로 사용
  6. 모든 단어집합에 대해 예측해야 하므로 tar_vocab_size만큼 Dense unit 설정. 활성화 함수는 softmax를 사용하여 모든 단어집합에 대한 확률분포를 계산함
  7. Dense layer에 LSTM에서 나온 output을 연결
decoder_inputs = Input(shape=(None,))
dec_emb = Embedding(tar_vocab_size, 64)(decoder_inputs)
dec_masking = Masking(mask_value=0.0)(dec_emb)
decoder_lstm = LSTM(64, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(dec_masking, initial_state=encoder_states)
decoder_dense = Dense(tar_vocab_size, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)

모델의 입출력을 정의한다. 입력은 encoder_inputs, decoder_inputs이고, 출력은 decoder_outputs이 된다.
모델 구조를 보면 이해가 될것이다.
중요한점은 ‘Layer (type)’에서 디코더의 lstm_1이 인코더의 lstm에서 나온 값 lstm[0][1], lstm[0][2]를 받는다는 것이다.

model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['acc'])
model.summary()
Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, None)]       0                                            
__________________________________________________________________________________________________
input_2 (InputLayer)            [(None, None)]       0                                            
__________________________________________________________________________________________________
embedding (Embedding)           (None, None, 64)     580160      input_1[0][0]                    
__________________________________________________________________________________________________
embedding_1 (Embedding)         (None, None, 64)     1048512     input_2[0][0]                    
__________________________________________________________________________________________________
masking (Masking)               (None, None, 64)     0           embedding[0][0]                  
__________________________________________________________________________________________________
masking_1 (Masking)             (None, None, 64)     0           embedding_1[0][0]                
__________________________________________________________________________________________________
lstm (LSTM)                     [(None, 64), (None,  33024       masking[0][0]                    
__________________________________________________________________________________________________
lstm_1 (LSTM)                   [(None, None, 64), ( 33024       masking_1[0][0]                  
                                                                 lstm[0][1]                       
                                                                 lstm[0][2]                       
__________________________________________________________________________________________________
dense (Dense)                   (None, None, 16383)  1064895     lstm_1[0][0]                     
==================================================================================================
Total params: 2,759,615
Trainable params: 2,759,615
Non-trainable params: 0
__________________________________________________________________________________________________

모델 학습

x에는 encoder_input_train, decoder_input_train을 넣고, y에는 target인 decoder_target_train을 넣는다.
RTX 3090ti 기준으로 13분 정도 걸렸다.

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 50/50
547/547 [==============================] - 16s 29ms/step - loss: 0.2761 - acc: 0.9302 - val_loss: 0.8073 - val_acc: 0.8627

예측을 위한 모델 재정의

seq2seq는 훈련과정과 예측과정에서의 동작 방식이 다르다. 훈련 과정에서는 decoder_input으로 정답 데이터가 들어갔지만 예측과정에서는 sos토큰 하나만 들어가기 때문이다.
전체적인 예측과정을 정리하자면,

  1. 번역하고싶은 문장이 인코더로 입력되어 컨텍스트 벡터를 생성
  2. 인코더의 컨텍스트 벡터와 sos토큰을 디코더로 보냄
  3. 디코더는 eos토큰이나 정해진 길이의 문자가 나올 때 까지 단어를 예측

그래서 훈련때 사용한 모델을 조금 수정시켜야 한다.

인코더는 수정할 필요가 없으므로 훈련 때 사용했던 인코더 모델을 그대로 사용한다. 따라서 인코더의 모든 부분은 학습되어 있는 상태이다.

# 인코더
encoder_model = Model(encoder_inputs, encoder_states)

예측 단계에서 디코더는 매 시점별로 다뤄야 한다. 따라서 이전 시점을 다뤄야 하는 decoder_state_input_hdecoder_state_input_c를 정의한다.

# 디코더
decoder_state_input_h = Input(shape=(64,))
decoder_state_input_c = Input(shape=(64,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]

훈련 때 사용했던 임베딩 층을 재사용한다.

dec_emb2 = dec_emb

다음 단어 예측을 위해 이전 시점의 상태를 현 시점의 초기 상태로 사용한다. 훈련 때에는 디코더의 초기 상태를 컨텍스트 벡터로 했던점과 다르다.

decoder_outputs2, state_h2, state_c2 = decoder_lstm(dec_emb2, initial_state=decoder_states_inputs)
decoder_states2 = [state_h2, state_c2]

모든 시점에 대해서 단어를 예측한다. decoder_dense는 훈련 때 학습이 되었다.

decoder_outputs2 = decoder_dense(decoder_outputs2)

마지막으로 디코더 모델을 재정의 한다.

입력은 [decoder_inputs]decoder_states_inputs 값을 합친 것이다.
여기서 [decoder_inputs]는 디코더의 입력 시퀀스이고 decoder_states_inputs은 위에서 정의한 hidden state와 cell state를 나타내는 것이다.

출력은 [decoder_outputs2]decoder_states2을 합친 것이다. [decoder_outputs2]은 lstm을 통해서 나온 결과이고 decoder_states2는 hidden state와 cell state를 나타낸다.

decoder_model = Model(
    [decoder_inputs] + decoder_states_inputs,
    [decoder_outputs2] + decoder_states2)

개념이 약간 어려울 것이다. 이제 예측하는 코드를 하나하나 풀어보면서 어떻게 돌아가는지 살펴보겠다. 일단 코드는 pseudo-code이므로 작동되지 않는다. 마지막에 전체 코드를 올려두겠다!

예측과정

states_value = encoder_model.predict(input_seq)

학습된 encoder_modelinput_seq를 넣어서 states_value를 얻어낸다.
여기서 input_seq는 번역을 위한 문장이다.
encoder_model은 출력값으로 encoder_states을 내보내는데, 이것은 state_h, state_c의 값이다.

target_seq = np.zeros((1,1))
target_seq[0, 0] = tar_to_index['<sos>']

이 코드는 디코더의 첫 입력으로 들어갈 SOS토큰을 생성하는 과정이다.

stop_condition = False
decoded_sentence = ''

stop_condition은 원하는 조건이 나오면 True로 설정된다. False인 동안에는 계속 반복하여 예측을 진행한다.
decoded_sentence는 최종적으로 반환될 예측이 완료된 문장이다.

output_tokens, h, c = decoder_model.predict([target_seq] + states_value)

여기서부터 중요하다. 우선 맨 처음 반복때, decoder_model에 SOS토큰과 인코더에서 나온 states_value를 넣는다. 위의 코드에서 states_valuedecoder_modeldecoder_states_inputs에 해당하므로 모델을 호출했을 때, states_value의 값은
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]으로 할당된다.
그러면 아까 재정의한 디코더 모델에서 이 decoder_states_inputsinitial_state으로 사용하여 decoder_outputs2[state_h2, state_c2]을 반환한다.

sampled_token_index = np.argmax(output_tokens[0, -1, :])
sampled_char = index_to_tar[sampled_token_index]

이 부분은 디코더 모델에서 나온 output_tokens을 단어로 변환하는 과정이다.
output_tokens는 다음을 예측할 때, 디코더 모델의 [target_seq]부분으로 들어가게 된다.

target_seq = np.zeros((1,1))
target_seq[0, 0] = sampled_token_index

이런식으로 말이다.

states_value = [h, c]

이 부분은 디코더 모델에서 나온 h, c를 states_value에 저장한다.
그리고 EOS토큰이 나올 때 까지 값을 갱신하면서 반복한다.

대충 감이 오는가? 2번째 반복부터는, SOS토큰과 인코더의 컨텍스트 벡터가 들어가는게 아니라, 예측을 하고 나온 시퀀스와 상태값들로 반복한다.
결국 맨 처음 번역할 문장이 인코더로 들어오면 컨텍스트 벡터가 생성이 되고, SOS토큰과 함께 첫 번째 예측을 한다. 그 다음부터는 결과값들로만 예측을 진행한다.
즉, 컨텍스트 벡터를 한번 넣었을 때, 그 값을 기준으로 계속 예측을 하기 때문에 컨텍스트 벡터가 얼마나 잘 학습되었는지가 중요하다.
아래에는 예측을 진행하는 총 코드이다.

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가 될 때까지 루프 반복
  while not stop_condition:
    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_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)
입력문장 : i like it . 
정답문장 : j'aime bien . 
번역문장 : j'aime le . 
--------------------------------------------------
입력문장 : we did that yesterday . 
정답문장 : nous avons fait ca hier . 
번역문장 : nous avons fait plein de temps . 
--------------------------------------------------
입력문장 : i figured i'd find you here . 
정답문장 : je me suis imaginee que je te trouverais ici . 
번역문장 : je me suis imaginee que je vous trouverais la . 
--------------------------------------------------
입력문장 : i always believe you . 
정답문장 : je vous crois toujours . 
번역문장 : je vous crois toujours . 
--------------------------------------------------
입력문장 : who cares about facts ? 
정답문장 : qui se soucie des faits ? 
번역문장 : qui vont tout ce ? 
--------------------------------------------------



현재 셀 또는 이전 셀에서 코드를 실행하는 동안 Kernel이 충돌했습니다. 셀의 코드를 검토하여 오류의 가능한 원인을 식별하세요. 자세한 내용을 보려면 <a href='https://aka.ms/vscodeJupyterKernelCrash'> 여기 </a> 를 클릭하세요. 자세한 내용은 Jupyter <a href='command:jupyter.viewOutput'>로그</a>를 참조하세요.

어느정도 잘 예측하는 것 같다. 다음에는 attention을 이용하여 더 정교하게 만들어보자.

참고자료

카테고리:

업데이트:

댓글남기기