{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "Sequence to Sequence with LSTM Tutorial",
"provenance": [],
"collapsed_sections": [],
"authorship_tag": "ABX9TyOjEU7Igr3zHQ7CG2hxB4aj",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"accelerator": "GPU"
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"
"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "xc2pgM0w9Xa1"
},
"source": [
"#### **Sequence to Sequence Learning with Neural Networks (NIPS 2014)** 실습\n",
"* 본 코드는 기본적으로 **Seq2Seq** 논문의 내용을 따릅니다.\n",
" * 본 논문은 **딥러닝 기반의 자연어 처리** 기법의 기본적인 구성을 이해하고 공부하는 데에 도움을 줍니다.\n",
" * 2020년 기준 가장 뛰어난 번역 모델은 Seq2Seq가 아닌 **Transformer 기반의 모델**입니다.\n",
"* 코드 실행 전에 **[런타임]** → **[런타임 유형 변경]** → 유형을 **GPU**로 설정합니다."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "L_UOI3Xm4U4M"
},
"source": [
"#### **데이터 전처리(Preprocessing)**\n",
"\n",
"* **spaCy 라이브러리**: 문장의 토큰화(tokenization), 태깅(tagging) 등의 전처리 기능을 위한 라이브러리\n",
" * 영어(Engilsh)와 독일어(Deutsch) 전처리 모듈 설치"
]
},
{
"cell_type": "code",
"metadata": {
"id": "x8SEN31g34aX"
},
"source": [
"%%capture\n",
"!python -m spacy download en\n",
"!python -m spacy download de"
],
"execution_count": 1,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "s7u-Xt2c4WV8"
},
"source": [
"import spacy\n",
"\n",
"spacy_en = spacy.load('en') # 영어 토큰화(tokenization)\n",
"spacy_de = spacy.load('de') # 독일어 토큰화(tokenization)"
],
"execution_count": 2,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "-vnjcXO84aRa",
"outputId": "b576cd79-aa5f-4c93-9691-156769a544c9",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"# 간단히 토큰화(tokenization) 기능 써보기\n",
"tokenized = spacy_en.tokenizer(\"I am a graduate student.\")\n",
"\n",
"for i, token in enumerate(tokenized):\n",
" print(f\"인덱스 {i}: {token.text}\")"
],
"execution_count": 3,
"outputs": [
{
"output_type": "stream",
"text": [
"인덱스 0: I\n",
"인덱스 1: am\n",
"인덱스 2: a\n",
"인덱스 3: graduate\n",
"인덱스 4: student\n",
"인덱스 5: .\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "5Hskqq3f4-YT"
},
"source": [
"* 영어(English) 및 독일어(Deutsch) **토큰화 함수** 정의"
]
},
{
"cell_type": "code",
"metadata": {
"id": "1EH0gEEb4iTj"
},
"source": [
"# 독일어(Deutsch) 문장을 토큰화한 뒤에 순서를 뒤집는 함수\n",
"def tokenize_de(text):\n",
" return [token.text for token in spacy_de.tokenizer(text)][::-1]\n",
"\n",
"# 영어(English) 문장을 토큰화 하는 함수\n",
"def tokenize_en(text):\n",
" return [token.text for token in spacy_en.tokenizer(text)]"
],
"execution_count": 4,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "M-fAQN1O6_a9"
},
"source": [
"* **필드(field)** 라이브러리를 이용해 데이터셋에 대한 구체적인 전처리 내용을 명시합니다.\n",
"* 번역 목표\n",
" * 소스(SRC): 독일어\n",
" * 목표(TRG): 영어"
]
},
{
"cell_type": "code",
"metadata": {
"id": "w31HzGx04qmC"
},
"source": [
"from torchtext.data import Field, BucketIterator\n",
"\n",
"SRC = Field(tokenize=tokenize_de, init_token=\"\", eos_token=\"\", lower=True)\n",
"TRG = Field(tokenize=tokenize_en, init_token=\"\", eos_token=\"\", lower=True)"
],
"execution_count": 5,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "a3MN7tLQ8wzG"
},
"source": [
"* 대표적인 영어-독어 번역 데이터셋인 **Multi30k**를 불러옵니다."
]
},
{
"cell_type": "code",
"metadata": {
"id": "PaEW2EpY72jt"
},
"source": [
"from torchtext.datasets import Multi30k\n",
"\n",
"train_dataset, valid_dataset, test_dataset = Multi30k.splits(exts=(\".de\", \".en\"), fields=(SRC, TRG))"
],
"execution_count": 8,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "OgN2KkyJ6kf0",
"outputId": "87c8067a-5ce3-40eb-814f-b8a18ff8c389",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"print(f\"학습 데이터셋(training dataset) 크기: {len(train_dataset.examples)}개\")\n",
"print(f\"평가 데이터셋(validation dataset) 크기: {len(valid_dataset.examples)}개\")\n",
"print(f\"테스트 데이터셋(testing dataset) 크기: {len(test_dataset.examples)}개\")"
],
"execution_count": 9,
"outputs": [
{
"output_type": "stream",
"text": [
"학습 데이터셋(training dataset) 크기: 29000개\n",
"평가 데이터셋(validation dataset) 크기: 1014개\n",
"테스트 데이터셋(testing dataset) 크기: 1000개\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"id": "P6clmgtf7ENF",
"outputId": "1b18ba42-0f44-4ee8-d14c-f5201b03a16c",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"# 학습 데이터 중 하나를 선택해 출력\n",
"print(vars(train_dataset.examples[30])['src'])\n",
"print(vars(train_dataset.examples[30])['trg'])"
],
"execution_count": 10,
"outputs": [
{
"output_type": "stream",
"text": [
"['.', 'steht', 'urinal', 'einem', 'an', 'kaffee', 'tasse', 'einer', 'mit', 'der', ',', 'mann', 'ein']\n",
"['a', 'man', 'standing', 'at', 'a', 'urinal', 'with', 'a', 'coffee', 'cup', '.']\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "vvPZWqQk9Ea1"
},
"source": [
"* **필드(field)** 객체의 **build_vocab** 메서드를 이용해 영어와 독어의 단어 사전을 생성합니다.\n",
" * **최소 2번 이상** 등장한 단어만을 선택합니다."
]
},
{
"cell_type": "code",
"metadata": {
"id": "WR7DqpLU8hiT",
"outputId": "e6d2aa5a-4327-4a8a-9429-1186d1d489a5",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"SRC.build_vocab(train_dataset, min_freq=2)\n",
"TRG.build_vocab(train_dataset, min_freq=2)\n",
"\n",
"print(f\"len(SRC): {len(SRC.vocab)}\")\n",
"print(f\"len(TRG): {len(TRG.vocab)}\")"
],
"execution_count": 11,
"outputs": [
{
"output_type": "stream",
"text": [
"len(SRC): 7855\n",
"len(TRG): 5893\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"id": "M4p3feo19aDP",
"outputId": "9a82a842-7dc3-41f0-e128-54e246b93fc8",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"print(TRG.vocab.stoi[\"abcabc\"]) # 없는 단어: 0\n",
"print(TRG.vocab.stoi[TRG.pad_token]) # 패딩(padding): 1\n",
"print(TRG.vocab.stoi[\"\"]) # : 2\n",
"print(TRG.vocab.stoi[\"\"]) # : 3\n",
"print(TRG.vocab.stoi[\"hello\"])\n",
"print(TRG.vocab.stoi[\"world\"])"
],
"execution_count": 12,
"outputs": [
{
"output_type": "stream",
"text": [
"0\n",
"1\n",
"2\n",
"3\n",
"4112\n",
"1752\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "_4PiQ3HR9WKP"
},
"source": [
"* 한 문장에 포함된 단어가 연속적으로 **LSTM**에 입력되어야 합니다.\n",
" * 따라서 하나의 배치에 포함된 문장들이 가지는 단어의 개수가 유사하도록 만들면 좋습니다.\n",
" * 이를 위해 BucketIterator를 사용합니다.\n",
" * **배치 크기(batch size)**: 128"
]
},
{
"cell_type": "code",
"metadata": {
"id": "KM6EVV2t9BHd"
},
"source": [
"import torch\n",
"\n",
"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
"\n",
"BATCH_SIZE = 128\n",
"\n",
"# 일반적인 데이터 로더(data loader)의 iterator와 유사하게 사용 가능\n",
"train_iterator, valid_iterator, test_iterator = BucketIterator.splits(\n",
" (train_dataset, valid_dataset, test_dataset),\n",
" batch_size=BATCH_SIZE,\n",
" device=device)"
],
"execution_count": 13,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "Y3v8n8XC-snA",
"outputId": "4f71c618-65cd-4a52-ab26-df6f68100636",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"for i, batch in enumerate(train_iterator):\n",
" src = batch.src\n",
" trg = batch.trg\n",
"\n",
" print(f\"첫 번째 배치 크기: {src.shape}\")\n",
"\n",
" # 현재 배치에 있는 하나의 문장에 포함된 정보 출력\n",
" for i in range(src.shape[0]):\n",
" print(f\"인덱스 {i}: {src[i][0].item()}\")\n",
"\n",
" # 첫 번째 배치만 확인\n",
" break"
],
"execution_count": 14,
"outputs": [
{
"output_type": "stream",
"text": [
"첫 번째 배치 크기: torch.Size([30, 128])\n",
"인덱스 0: 2\n",
"인덱스 1: 4\n",
"인덱스 2: 6915\n",
"인덱스 3: 14\n",
"인덱스 4: 47\n",
"인덱스 5: 440\n",
"인덱스 6: 0\n",
"인덱스 7: 78\n",
"인덱스 8: 6\n",
"인덱스 9: 12\n",
"인덱스 10: 131\n",
"인덱스 11: 156\n",
"인덱스 12: 325\n",
"인덱스 13: 3\n",
"인덱스 14: 1\n",
"인덱스 15: 1\n",
"인덱스 16: 1\n",
"인덱스 17: 1\n",
"인덱스 18: 1\n",
"인덱스 19: 1\n",
"인덱스 20: 1\n",
"인덱스 21: 1\n",
"인덱스 22: 1\n",
"인덱스 23: 1\n",
"인덱스 24: 1\n",
"인덱스 25: 1\n",
"인덱스 26: 1\n",
"인덱스 27: 1\n",
"인덱스 28: 1\n",
"인덱스 29: 1\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "oguDx24e-3Db"
},
"source": [
"#### **인코더(Encoder) 아키텍처**\n",
"\n",
"* 주어진 소스 문장을 **문맥 벡터(context vector)로 인코딩**합니다.\n",
"* LSTM은 hidden state과 cell state을 반환합니다.\n",
"* 하이퍼 파라미터(hyperparameter)\n",
" * **input_dim**: 하나의 단어에 대한 원핫 인코딩 차원\n",
" * **embed_dim**: 임베딩(embedding) 차원\n",
" * **hidden_dim**: 히든 상태(hidden state) 차원\n",
" * **n_layers**: RNN 레이어의 개수\n",
" * **dropout_ratio**: 드롭아웃(dropout) 비율"
]
},
{
"cell_type": "code",
"metadata": {
"id": "Ac28d5DL_ceY"
},
"source": [
"import torch.nn as nn\n",
"\n",
"# 인코더(Encoder) 아키텍처 정의\n",
"class Encoder(nn.Module):\n",
" def __init__(self, input_dim, embed_dim, hidden_dim, n_layers, dropout_ratio):\n",
" super().__init__()\n",
"\n",
" # 임베딩(embedding)은 원-핫 인코딩(one-hot encoding)을 특정 차원의 임베딩으로 매핑하는 레이어\n",
" self.embedding = nn.Embedding(input_dim, embed_dim)\n",
"\n",
" # LSTM 레이어\n",
" self.hidden_dim = hidden_dim\n",
" self.n_layers = n_layers\n",
" self.rnn = nn.LSTM(embed_dim, hidden_dim, n_layers, dropout=dropout_ratio)\n",
" \n",
" # 드롭아웃(dropout)\n",
" self.dropout = nn.Dropout(dropout_ratio)\n",
"\n",
" # 인코더는 소스 문장을 입력으로 받아 문맥 벡터(context vector)를 반환 \n",
" def forward(self, src):\n",
" # src: [단어 개수, 배치 크기]: 각 단어의 인덱스(index) 정보\n",
" embedded = self.dropout(self.embedding(src))\n",
" # embedded: [단어 개수, 배치 크기, 임베딩 차원]\n",
"\n",
" outputs, (hidden, cell) = self.rnn(embedded)\n",
" # outputs: [단어 개수, 배치 크기, 히든 차원]: 현재 단어의 출력 정보\n",
" # hidden: [레이어 개수, 배치 크기, 히든 차원]: 현재까지의 모든 단어의 정보\n",
" # cell: [레이어 개수, 배치 크기, 히든 차원]: 현재까지의 모든 단어의 정보\n",
"\n",
" # 문맥 벡터(context vector) 반환\n",
" return hidden, cell"
],
"execution_count": 15,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "0pHj4IlvBzPe"
},
"source": [
"#### **디코더(Decoder) 아키텍처**\n",
"\n",
"* 주어진 문맥 벡터(context vector)를 **타겟 문장으로 디코딩**합니다.\n",
"* LSTM은 hidden state과 cell state을 반환합니다.\n",
"* 하이퍼 파라미터(hyperparameter)\n",
" * **input_dim**: 하나의 단어에 대한 원핫 인코딩 차원\n",
" * **embed_dim**: 임베딩(embedding) 차원\n",
" * **hidden_dim**: 히든 상태(hidden state) 차원\n",
" * **n_layers**: RNN 레이어의 개수\n",
" * **dropout_ratio**: 드롭아웃(dropout) 비율"
]
},
{
"cell_type": "code",
"metadata": {
"id": "fusf6_9yDmfM"
},
"source": [
"# 디코더(Decoder) 아키텍처 정의\n",
"class Decoder(nn.Module):\n",
" def __init__(self, output_dim, embed_dim, hidden_dim, n_layers, dropout_ratio):\n",
" super().__init__()\n",
"\n",
" # 임베딩(embedding)은 원-핫 인코딩(one-hot encoding) 말고 특정 차원의 임베딩으로 매핑하는 레이어\n",
" self.embedding = nn.Embedding(output_dim, embed_dim)\n",
"\n",
" # LSTM 레이어\n",
" self.hidden_dim = hidden_dim\n",
" self.n_layers = n_layers\n",
" self.rnn = nn.LSTM(embed_dim, hidden_dim, n_layers, dropout=dropout_ratio)\n",
" \n",
" # FC 레이어 (인코더와 구조적으로 다른 부분)\n",
" self.output_dim = output_dim\n",
" self.fc_out = nn.Linear(hidden_dim, output_dim)\n",
" \n",
" # 드롭아웃(dropout)\n",
" self.dropout = nn.Dropout(dropout_ratio)\n",
"\n",
" # 디코더는 현재까지 출력된 문장에 대한 정보를 입력으로 받아 타겟 문장을 반환 \n",
" def forward(self, input, hidden, cell):\n",
" # input: [배치 크기]: 단어의 개수는 항상 1개이도록 구현\n",
" # hidden: [레이어 개수, 배치 크기, 히든 차원]\n",
" # cell = context: [레이어 개수, 배치 크기, 히든 차원]\n",
" input = input.unsqueeze(0)\n",
" # input: [단어 개수 = 1, 배치 크기]\n",
" \n",
" embedded = self.dropout(self.embedding(input))\n",
" # embedded: [단어 개수, 배치 크기, 임베딩 차원]\n",
"\n",
" output, (hidden, cell) = self.rnn(embedded, (hidden, cell))\n",
" # output: [단어 개수 = 1, 배치 크기, 히든 차원]: 현재 단어의 출력 정보\n",
" # hidden: [레이어 개수, 배치 크기, 히든 차원]: 현재까지의 모든 단어의 정보\n",
" # cell: [레이어 개수, 배치 크기, 히든 차원]: 현재까지의 모든 단어의 정보\n",
"\n",
" # 단어 개수는 어차피 1개이므로 차원 제거\n",
" prediction = self.fc_out(output.squeeze(0))\n",
" # prediction = [배치 크기, 출력 차원]\n",
" \n",
" # (현재 출력 단어, 현재까지의 모든 단어의 정보, 현재까지의 모든 단어의 정보)\n",
" return prediction, hidden, cell"
],
"execution_count": 17,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "j17WxtpJI_9V"
},
"source": [
"#### **Seq2Seq 아키텍처**\n",
"\n",
"* 앞서 정의한 인코더(encoder)와 디코더(decoder)를 가지고 있는 하나의 아키텍처입니다.\n",
" * **인코더(encoder)**: 주어진 소스 문장을 문맥 벡터(context vector)로 인코딩합니다.\n",
" * **디코더(decoder)**: 주어진 문맥 벡터(context vector)를 타겟 문장으로 디코딩합니다.\n",
" * 단, **디코더는 한 단어씩** 넣어서 한 번씩 결과를 구합니다.\n",
"* **Teacher forcing**: 디코더의 예측(prediction)을 다음 입력으로 사용하지 않고, 실제 목표 출력(ground-truth)을 다음 입력으로 사용하는 기법"
]
},
{
"cell_type": "code",
"metadata": {
"id": "vsA6C6B5Glhc"
},
"source": [
"class Seq2Seq(nn.Module):\n",
" def __init__(self, encoder, decoder, device):\n",
" super().__init__()\n",
"\n",
" self.encoder = encoder\n",
" self.decoder = decoder\n",
" self.device = device\n",
"\n",
" # 학습할 때는 완전한 형태의 소스 문장, 타겟 문장, teacher_forcing_ratio를 넣기\n",
" def forward(self, src, trg, teacher_forcing_ratio=0.5):\n",
" # src: [단어 개수, 배치 크기]\n",
" # trg: [단어 개수, 배치 크기]\n",
" # 먼저 인코더를 거쳐 문맥 벡터(context vector)를 추출\n",
" hidden, cell = self.encoder(src)\n",
"\n",
" # 디코더(decoder)의 최종 결과를 담을 텐서 객체 만들기\n",
" trg_len = trg.shape[0] # 단어 개수\n",
" batch_size = trg.shape[1] # 배치 크기\n",
" trg_vocab_size = self.decoder.output_dim # 출력 차원\n",
" outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)\n",
"\n",
" # 첫 번째 입력은 항상 토큰\n",
" input = trg[0, :]\n",
"\n",
" # 타겟 단어의 개수만큼 반복하여 디코더에 포워딩(forwarding)\n",
" for t in range(1, trg_len):\n",
" output, hidden, cell = self.decoder(input, hidden, cell)\n",
"\n",
" outputs[t] = output # FC를 거쳐서 나온 현재의 출력 단어 정보\n",
" top1 = output.argmax(1) # 가장 확률이 높은 단어의 인덱스 추출\n",
"\n",
" # teacher_forcing_ratio: 학습할 때 실제 목표 출력(ground-truth)을 사용하는 비율\n",
" teacher_force = random.random() < teacher_forcing_ratio\n",
" input = trg[t] if teacher_force else top1 # 현재의 출력 결과를 다음 입력에서 넣기\n",
" \n",
" return outputs"
],
"execution_count": 18,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "WyXRWyDrHYSB"
},
"source": [
"#### **학습(Training)**\n",
"\n",
"* 하이퍼 파라미터 설정 및 모델 초기화"
]
},
{
"cell_type": "code",
"metadata": {
"id": "qVsGIVvzMZ-N"
},
"source": [
"INPUT_DIM = len(SRC.vocab)\n",
"OUTPUT_DIM = len(TRG.vocab)\n",
"ENCODER_EMBED_DIM = 256\n",
"DECODER_EMBED_DIM = 256\n",
"HIDDEN_DIM = 512\n",
"N_LAYERS = 2\n",
"ENC_DROPOUT_RATIO = 0.5\n",
"DEC_DROPOUT_RATIO = 0.5"
],
"execution_count": 19,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "0WM3urPiIE1T"
},
"source": [
"# 인코더(encoder)와 디코더(decoder) 객체 선언\n",
"enc = Encoder(INPUT_DIM, ENCODER_EMBED_DIM, HIDDEN_DIM, N_LAYERS, ENC_DROPOUT_RATIO)\n",
"dec = Decoder(OUTPUT_DIM, DECODER_EMBED_DIM, HIDDEN_DIM, N_LAYERS, DEC_DROPOUT_RATIO)\n",
"\n",
"# Seq2Seq 객체 선언\n",
"model = Seq2Seq(enc, dec, device).to(device)"
],
"execution_count": 20,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "CcnWnXGIMwHk"
},
"source": [
"* 논문의 내용대로 $\\mathcal{U}(-0.08, 0.08)$의 값으로 **모델 가중치 파라미터 초기화**"
]
},
{
"cell_type": "code",
"metadata": {
"id": "Zdfqma4uMaoI",
"outputId": "7c32cde1-299f-450f-9df4-5e621abd23ee",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"def init_weights(m):\n",
" for name, param in m.named_parameters():\n",
" nn.init.uniform_(param.data, -0.08, 0.08)\n",
" \n",
"model.apply(init_weights)"
],
"execution_count": 21,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
"Seq2Seq(\n",
" (encoder): Encoder(\n",
" (embedding): Embedding(7855, 256)\n",
" (rnn): LSTM(256, 512, num_layers=2, dropout=0.5)\n",
" (dropout): Dropout(p=0.5, inplace=False)\n",
" )\n",
" (decoder): Decoder(\n",
" (embedding): Embedding(5893, 256)\n",
" (rnn): LSTM(256, 512, num_layers=2, dropout=0.5)\n",
" (fc_out): Linear(in_features=512, out_features=5893, bias=True)\n",
" (dropout): Dropout(p=0.5, inplace=False)\n",
" )\n",
")"
]
},
"metadata": {
"tags": []
},
"execution_count": 21
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "XQUwj0UgIS6E"
},
"source": [
"* 학습 및 평가 함수 정의"
]
},
{
"cell_type": "code",
"metadata": {
"id": "BeqqI7xfM71V"
},
"source": [
"import torch.optim as optim\n",
"\n",
"# Adam optimizer로 학습 최적화\n",
"optimizer = optim.Adam(model.parameters())\n",
"\n",
"# 뒷 부분의 패딩(padding)에 대해서는 값 무시\n",
"TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]\n",
"criterion = nn.CrossEntropyLoss(ignore_index=TRG_PAD_IDX)"
],
"execution_count": 22,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "SXo7ZOclNG2-"
},
"source": [
"# 모델 학습(train) 함수\n",
"def train(model, iterator, optimizer, criterion, clip):\n",
" model.train() # 학습 모드\n",
" epoch_loss = 0\n",
" \n",
" # 전체 학습 데이터를 확인하며\n",
" for i, batch in enumerate(iterator):\n",
" src = batch.src\n",
" trg = batch.trg\n",
" \n",
" optimizer.zero_grad()\n",
"\n",
" output = model(src, trg)\n",
" # output: [출력 단어 개수, 배치 크기, 출력 차원]\n",
" output_dim = output.shape[-1]\n",
" \n",
" # 출력 단어의 인덱스 0은 사용하지 않음\n",
" output = output[1:].view(-1, output_dim)\n",
" # output = [(출력 단어의 개수 - 1) * batch size, output dim]\n",
" trg = trg[1:].view(-1)\n",
" # trg = [(타겟 단어의 개수 - 1) * batch size]\n",
" \n",
" # 모델의 출력 결과와 타겟 문장을 비교하여 손실 계산\n",
" loss = criterion(output, trg)\n",
" loss.backward() # 기울기(gradient) 계산\n",
" \n",
" # 기울기(gradient) clipping 진행\n",
" torch.nn.utils.clip_grad_norm_(model.parameters(), clip)\n",
" \n",
" # 파라미터 업데이트\n",
" optimizer.step()\n",
" \n",
" # 전체 손실 값 계산\n",
" epoch_loss += loss.item()\n",
" \n",
" return epoch_loss / len(iterator)"
],
"execution_count": 23,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "DzR8hm9HQ1gb"
},
"source": [
"# 모델 평가(evaluate) 함수\n",
"def evaluate(model, iterator, criterion):\n",
" model.eval() # 평가 모드\n",
" epoch_loss = 0\n",
" \n",
" with torch.no_grad():\n",
" # 전체 평가 데이터를 확인하며\n",
" for i, batch in enumerate(iterator):\n",
" src = batch.src\n",
" trg = batch.trg\n",
"\n",
" # 평가할 때 teacher forcing는 사용하지 않음\n",
" output = model(src, trg, 0)\n",
" # output: [출력 단어 개수, 배치 크기, 출력 차원]\n",
" output_dim = output.shape[-1]\n",
" \n",
" # 출력 단어의 인덱스 0은 사용하지 않음\n",
" output = output[1:].view(-1, output_dim)\n",
" # output = [(출력 단어의 개수 - 1) * batch size, output dim]\n",
" trg = trg[1:].view(-1)\n",
" # trg = [(타겟 단어의 개수 - 1) * batch size]\n",
"\n",
" # 모델의 출력 결과와 타겟 문장을 비교하여 손실 계산\n",
" loss = criterion(output, trg)\n",
"\n",
" # 전체 손실 값 계산\n",
" epoch_loss += loss.item()\n",
" \n",
" return epoch_loss / len(iterator)"
],
"execution_count": 24,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "76X7pR1cLtAl"
},
"source": [
"* 학습(training) 및 검증(validation) 진행\n",
" * **학습 횟수(epoch)**: 20"
]
},
{
"cell_type": "code",
"metadata": {
"id": "iVGWe9VtSwx0"
},
"source": [
"def epoch_time(start_time, end_time):\n",
" elapsed_time = end_time - start_time\n",
" elapsed_mins = int(elapsed_time / 60)\n",
" elapsed_secs = int(elapsed_time - (elapsed_mins * 60))\n",
" return elapsed_mins, elapsed_secs"
],
"execution_count": 25,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "EMMkAnGeSyMW",
"outputId": "8e0ecd14-bd38-47ad-e599-2607cbb99eaa",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"import time\n",
"import math\n",
"import random\n",
"\n",
"N_EPOCHS = 20\n",
"CLIP = 1\n",
"best_valid_loss = float('inf')\n",
"\n",
"for epoch in range(N_EPOCHS):\n",
" start_time = time.time() # 시작 시간 기록\n",
"\n",
" train_loss = train(model, train_iterator, optimizer, criterion, CLIP)\n",
" valid_loss = evaluate(model, valid_iterator, criterion)\n",
"\n",
" end_time = time.time() # 종료 시간 기록\n",
" epoch_mins, epoch_secs = epoch_time(start_time, end_time)\n",
"\n",
" if valid_loss < best_valid_loss:\n",
" best_valid_loss = valid_loss\n",
" torch.save(model.state_dict(), 'seq2seq.pt')\n",
"\n",
" print(f'Epoch: {epoch + 1:02} | Time: {epoch_mins}m {epoch_secs}s')\n",
" print(f'\\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):.3f}')\n",
" print(f'\\tValidation Loss: {valid_loss:.3f} | Validation PPL: {math.exp(valid_loss):.3f}')"
],
"execution_count": 26,
"outputs": [
{
"output_type": "stream",
"text": [
"Epoch: 01 | Time: 0m 28s\n",
"\tTrain Loss: 5.048 | Train PPL: 155.759\n",
"\tValidation Loss: 4.957 | Validation PPL: 142.218\n",
"Epoch: 02 | Time: 0m 28s\n",
"\tTrain Loss: 4.511 | Train PPL: 90.994\n",
"\tValidation Loss: 4.742 | Validation PPL: 114.688\n",
"Epoch: 03 | Time: 0m 28s\n",
"\tTrain Loss: 4.168 | Train PPL: 64.588\n",
"\tValidation Loss: 4.550 | Validation PPL: 94.600\n",
"Epoch: 04 | Time: 0m 28s\n",
"\tTrain Loss: 3.966 | Train PPL: 52.756\n",
"\tValidation Loss: 4.510 | Validation PPL: 90.881\n",
"Epoch: 05 | Time: 0m 28s\n",
"\tTrain Loss: 3.797 | Train PPL: 44.583\n",
"\tValidation Loss: 4.303 | Validation PPL: 73.934\n",
"Epoch: 06 | Time: 0m 28s\n",
"\tTrain Loss: 3.645 | Train PPL: 38.297\n",
"\tValidation Loss: 4.216 | Validation PPL: 67.741\n",
"Epoch: 07 | Time: 0m 28s\n",
"\tTrain Loss: 3.489 | Train PPL: 32.749\n",
"\tValidation Loss: 4.114 | Validation PPL: 61.164\n",
"Epoch: 08 | Time: 0m 28s\n",
"\tTrain Loss: 3.361 | Train PPL: 28.812\n",
"\tValidation Loss: 3.979 | Validation PPL: 53.469\n",
"Epoch: 09 | Time: 0m 28s\n",
"\tTrain Loss: 3.224 | Train PPL: 25.118\n",
"\tValidation Loss: 3.804 | Validation PPL: 44.859\n",
"Epoch: 10 | Time: 0m 28s\n",
"\tTrain Loss: 3.071 | Train PPL: 21.568\n",
"\tValidation Loss: 3.852 | Validation PPL: 47.108\n",
"Epoch: 11 | Time: 0m 28s\n",
"\tTrain Loss: 2.970 | Train PPL: 19.489\n",
"\tValidation Loss: 3.841 | Validation PPL: 46.572\n",
"Epoch: 12 | Time: 0m 28s\n",
"\tTrain Loss: 2.858 | Train PPL: 17.430\n",
"\tValidation Loss: 3.798 | Validation PPL: 44.596\n",
"Epoch: 13 | Time: 0m 28s\n",
"\tTrain Loss: 2.765 | Train PPL: 15.879\n",
"\tValidation Loss: 3.661 | Validation PPL: 38.896\n",
"Epoch: 14 | Time: 0m 28s\n",
"\tTrain Loss: 2.678 | Train PPL: 14.558\n",
"\tValidation Loss: 3.656 | Validation PPL: 38.699\n",
"Epoch: 15 | Time: 0m 28s\n",
"\tTrain Loss: 2.580 | Train PPL: 13.199\n",
"\tValidation Loss: 3.680 | Validation PPL: 39.661\n",
"Epoch: 16 | Time: 0m 28s\n",
"\tTrain Loss: 2.486 | Train PPL: 12.015\n",
"\tValidation Loss: 3.655 | Validation PPL: 38.677\n",
"Epoch: 17 | Time: 0m 28s\n",
"\tTrain Loss: 2.427 | Train PPL: 11.325\n",
"\tValidation Loss: 3.646 | Validation PPL: 38.325\n",
"Epoch: 18 | Time: 0m 28s\n",
"\tTrain Loss: 2.355 | Train PPL: 10.534\n",
"\tValidation Loss: 3.659 | Validation PPL: 38.831\n",
"Epoch: 19 | Time: 0m 28s\n",
"\tTrain Loss: 2.301 | Train PPL: 9.981\n",
"\tValidation Loss: 3.617 | Validation PPL: 37.211\n",
"Epoch: 20 | Time: 0m 28s\n",
"\tTrain Loss: 2.210 | Train PPL: 9.119\n",
"\tValidation Loss: 3.673 | Validation PPL: 39.386\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"id": "OvYlZ85ZRUya",
"outputId": "360659df-8f45-47cc-bd65-93be438b6df3",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 17
}
},
"source": [
"# 학습된 모델 저장\n",
"from google.colab import files\n",
"\n",
"files.download('seq2seq.pt')"
],
"execution_count": 27,
"outputs": [
{
"output_type": "display_data",
"data": {
"application/javascript": [
"\n",
" async function download(id, filename, size) {\n",
" if (!google.colab.kernel.accessAllowed) {\n",
" return;\n",
" }\n",
" const div = document.createElement('div');\n",
" const label = document.createElement('label');\n",
" label.textContent = `Downloading \"${filename}\": `;\n",
" div.appendChild(label);\n",
" const progress = document.createElement('progress');\n",
" progress.max = size;\n",
" div.appendChild(progress);\n",
" document.body.appendChild(div);\n",
"\n",
" const buffers = [];\n",
" let downloaded = 0;\n",
"\n",
" const channel = await google.colab.kernel.comms.open(id);\n",
" // Send a message to notify the kernel that we're ready.\n",
" channel.send({})\n",
"\n",
" for await (const message of channel.messages) {\n",
" // Send a message to notify the kernel that we're ready.\n",
" channel.send({})\n",
" if (message.buffers) {\n",
" for (const buffer of message.buffers) {\n",
" buffers.push(buffer);\n",
" downloaded += buffer.byteLength;\n",
" progress.value = downloaded;\n",
" }\n",
" }\n",
" }\n",
" const blob = new Blob(buffers, {type: 'application/binary'});\n",
" const a = document.createElement('a');\n",
" a.href = window.URL.createObjectURL(blob);\n",
" a.download = filename;\n",
" div.appendChild(a);\n",
" a.click();\n",
" div.remove();\n",
" }\n",
" "
],
"text/plain": [
""
]
},
"metadata": {
"tags": []
}
},
{
"output_type": "display_data",
"data": {
"application/javascript": [
"download(\"download_f36e521f-5607-4297-8869-5295596f7768\", \"seq2seq.pt\", 55600205)"
],
"text/plain": [
""
]
},
"metadata": {
"tags": []
}
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "M6d2isoPL_P0"
},
"source": [
"#### **모델 최종 테스트(testing) 결과 확인**"
]
},
{
"cell_type": "code",
"metadata": {
"id": "Rfs5GYSpUGeU",
"outputId": "4c0fef53-496e-458d-9785-223df9e56187",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"!wget https://postechackr-my.sharepoint.com/:u:/g/personal/dongbinna_postech_ac_kr/ERgwTMYWR7FMhApROaNvZREBTjEDi00ttSzt8ZNj1PS_5g?download=1 -O seq2seq.pt"
],
"execution_count": 28,
"outputs": [
{
"output_type": "stream",
"text": [
"--2020-11-08 14:51:26-- https://postechackr-my.sharepoint.com/:u:/g/personal/dongbinna_postech_ac_kr/ERgwTMYWR7FMhApROaNvZREBTjEDi00ttSzt8ZNj1PS_5g?download=1\n",
"Resolving postechackr-my.sharepoint.com (postechackr-my.sharepoint.com)... 13.107.136.9\n",
"Connecting to postechackr-my.sharepoint.com (postechackr-my.sharepoint.com)|13.107.136.9|:443... connected.\n",
"HTTP request sent, awaiting response... 302 Found\n",
"Location: /personal/dongbinna_postech_ac_kr/Documents/Research/models/seq2seq.pt?originalPath=aHR0cHM6Ly9wb3N0ZWNoYWNrci1teS5zaGFyZXBvaW50LmNvbS86dTovZy9wZXJzb25hbC9kb25nYmlubmFfcG9zdGVjaF9hY19rci9FUmd3VE1ZV1I3Rk1oQXBST2FOdlpSRUJUakVEaTAwdHRTenQ4Wk5qMVBTXzVnP3J0aW1lPUJ0NHZ4ZldEMkVn [following]\n",
"--2020-11-08 14:51:27-- https://postechackr-my.sharepoint.com/personal/dongbinna_postech_ac_kr/Documents/Research/models/seq2seq.pt?originalPath=aHR0cHM6Ly9wb3N0ZWNoYWNrci1teS5zaGFyZXBvaW50LmNvbS86dTovZy9wZXJzb25hbC9kb25nYmlubmFfcG9zdGVjaF9hY19rci9FUmd3VE1ZV1I3Rk1oQXBST2FOdlpSRUJUakVEaTAwdHRTenQ4Wk5qMVBTXzVnP3J0aW1lPUJ0NHZ4ZldEMkVn\n",
"Reusing existing connection to postechackr-my.sharepoint.com:443.\n",
"HTTP request sent, awaiting response... 200 OK\n",
"Length: 55600205 (53M) [application/octet-stream]\n",
"Saving to: ‘seq2seq.pt’\n",
"\n",
"seq2seq.pt 100%[===================>] 53.02M 36.7MB/s in 1.4s \n",
"\n",
"2020-11-08 14:51:29 (36.7 MB/s) - ‘seq2seq.pt’ saved [55600205/55600205]\n",
"\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"id": "co3YMQ2NS0Ia",
"outputId": "5a6be10a-8f2a-4b20-db26-417eb20a5789",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"model.load_state_dict(torch.load('seq2seq.pt'))\n",
"\n",
"test_loss = evaluate(model, test_iterator, criterion)\n",
"\n",
"print(f'Test Loss: {test_loss:.3f} | Test PPL: {math.exp(test_loss):.3f}')"
],
"execution_count": 29,
"outputs": [
{
"output_type": "stream",
"text": [
"Test Loss: 3.593 | Test PPL: 36.330\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "CMvcdNR7YqAo"
},
"source": [
"#### **나만의 데이터로 모델 사용해보기**"
]
},
{
"cell_type": "code",
"metadata": {
"id": "OjHJkeznS1oS"
},
"source": [
"# 번역(translation) 함수\n",
"def translate_sentence(sentence, src_field, trg_field, model, device, max_len=50):\n",
" model.eval() # 평가 모드\n",
"\n",
" if isinstance(sentence, str):\n",
" nlp = spacy.load('de')\n",
" tokens = [token.text.lower() for token in nlp(sentence)]\n",
" else:\n",
" tokens = [token.lower() for token in sentence]\n",
"\n",
" # 처음에 토큰, 마지막에 토큰 붙이기\n",
" tokens = [src_field.init_token] + tokens + [src_field.eos_token]\n",
" print(f\"전체 소스 토큰: {tokens}\")\n",
"\n",
" src_indexes = [src_field.vocab.stoi[token] for token in tokens]\n",
" print(f\"소스 문장 인덱스: {src_indexes}\")\n",
"\n",
" src_tensor = torch.LongTensor(src_indexes).unsqueeze(1).to(device)\n",
"\n",
" # 인코더(endocer)에 소스 문장을 넣어 문맥 벡터(context vector) 계산\n",
" with torch.no_grad():\n",
" hidden, cell = model.encoder(src_tensor)\n",
"\n",
" # 처음에는 토큰 하나만 가지고 있도록 하기\n",
" trg_indexes = [trg_field.vocab.stoi[trg_field.init_token]]\n",
"\n",
" for i in range(max_len):\n",
" # 이전에 출력한 단어가 현재 단어로 입력될 수 있도록\n",
" trg_tensor = torch.LongTensor([trg_indexes[-1]]).to(device)\n",
"\n",
" with torch.no_grad():\n",
" output, hidden, cell = model.decoder(trg_tensor, hidden, cell)\n",
"\n",
" pred_token = output.argmax(1).item()\n",
" trg_indexes.append(pred_token) # 출력 문장에 더하기\n",
"\n",
" # 를 만나는 순간 끝\n",
" if pred_token == trg_field.vocab.stoi[trg_field.eos_token]:\n",
" break\n",
"\n",
" # 각 출력 단어 인덱스를 실제 단어로 변환\n",
" trg_tokens = [trg_field.vocab.itos[i] for i in trg_indexes]\n",
"\n",
" # 첫 번째 는 제외하고 출력 문장 반환\n",
" return trg_tokens[1:]"
],
"execution_count": 30,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "1OSgbkh0Vkq7",
"outputId": "9078ecf0-9077-4bfe-ae6c-6509a55ec5d1",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"example_idx = 10\n",
"\n",
"src = vars(test_dataset.examples[example_idx])['src']\n",
"trg = vars(test_dataset.examples[example_idx])['trg']\n",
"\n",
"print(f'소스 문장: {src}')\n",
"print(f'타겟 문장: {trg}')\n",
"print(\"모델 출력 결과:\", \" \".join(translate_sentence(src, SRC, TRG, model, device)))"
],
"execution_count": 39,
"outputs": [
{
"output_type": "stream",
"text": [
"소스 문장: ['.', 'freien', 'im', 'tag', 'schönen', 'einen', 'genießen', 'sohn', 'kleiner', 'ihr', 'und', 'mutter', 'eine']\n",
"타겟 문장: ['a', 'mother', 'and', 'her', 'young', 'song', 'enjoying', 'a', 'beautiful', 'day', 'outside', '.']\n",
"전체 소스 토큰: ['', '.', 'freien', 'im', 'tag', 'schönen', 'einen', 'genießen', 'sohn', 'kleiner', 'ihr', 'und', 'mutter', 'eine', '']\n",
"소스 문장 인덱스: [2, 4, 88, 20, 200, 780, 19, 565, 624, 70, 134, 10, 364, 8, 3]\n",
"모델 출력 결과: a mother and her little boys enjoying a day day day . \n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"id": "TR4mSubPXOJV",
"outputId": "c913c99d-4a3b-428b-9b65-cdeeb90299de",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"src = tokenize_de(\"Guten Abend.\")\n",
"\n",
"print(f'소스 문장: {src}')\n",
"print(\"모델 출력 결과:\", \" \".join(translate_sentence(src, SRC, TRG, model, device)))"
],
"execution_count": 41,
"outputs": [
{
"output_type": "stream",
"text": [
"소스 문장: ['.', 'Abend', 'Guten']\n",
"전체 소스 토큰: ['', '.', 'abend', 'guten', '']\n",
"소스 문장 인덱스: [2, 4, 1163, 3799, 3]\n",
"모델 출력 결과: at night . \n"
],
"name": "stdout"
}
]
}
]
}