{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "name": "Sequence to Sequence with Attention Tutorial", "provenance": [], "collapsed_sections": [], "authorship_tag": "ABX9TyMzaWZmEYgAoM2ZBZBQtcQo", "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": [ "\"Open" ] }, { "cell_type": "markdown", "metadata": { "id": "cA1buQp2iaXx" }, "source": [ "#### **Neural Machine Translation by Jointly Learning to Align and Translate (ICLR 2015)** 실습\n", "* 본 코드는 기본적으로 원본 논문의 내용을 최대한 따릅니다.\n", " * 본 논문은 **딥러닝 기반의 자연어 처리** 기법의 기본적인 구성을 이해하고 공부하는 데에 도움을 줍니다.\n", " * 2020년 기준 가장 뛰어난 번역 모델은 Seq2Seq with Attention이 아닌 **Transformer 기반의 모델**입니다.\n", "* 코드 실행 전에 **[런타임]** → **[런타임 유형 변경]** → 유형을 **GPU**로 설정합니다." ] }, { "cell_type": "markdown", "metadata": { "id": "6V6X0LxZitih" }, "source": [ "#### **데이터 전처리(Preprocessing)**\n", "\n", "* **spaCy 라이브러리**: 문장의 토큰화(tokenization), 태깅(tagging) 등의 전처리 기능을 위한 라이브러리\n", " * 영어(Engilsh)와 독일어(Deutsch) 전처리 모듈 설치" ] }, { "cell_type": "code", "metadata": { "id": "0umI87bziUih" }, "source": [ "%%capture\n", "!python -m spacy download en\n", "!python -m spacy download de" ], "execution_count": null, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "CSXmfAWfiupJ" }, "source": [ "import spacy\n", "\n", "spacy_en = spacy.load('en') # 영어 토큰화(tokenization)\n", "spacy_de = spacy.load('de') # 독일어 토큰화(tokenization)" ], "execution_count": null, "outputs": [] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "JVWyGCWfivbO", "outputId": "2412e909-2a45-4fec-dbd3-fad49500bf87" }, "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": null, "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": "gmA4kX0QixwZ" }, "source": [ "* 영어(English) 및 독일어(Deutsch) **토큰화 함수** 정의" ] }, { "cell_type": "code", "metadata": { "id": "e-pEypzUiwnX" }, "source": [ "# 독일어(Deutsch) 문장을 토큰화 하는 함수 (순서를 뒤집지 않음)\n", "def tokenize_de(text):\n", " return [token.text for token in spacy_de.tokenizer(text)]\n", "\n", "# 영어(English) 문장을 토큰화 하는 함수\n", "def tokenize_en(text):\n", " return [token.text for token in spacy_en.tokenizer(text)]" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "vhAMTQyJizo9" }, "source": [ "* **필드(field)** 라이브러리를 이용해 데이터셋에 대한 구체적인 전처리 내용을 명시합니다.\n", "* 번역 목표\n", " * 소스(SRC): 독일어\n", " * 목표(TRG): 영어" ] }, { "cell_type": "code", "metadata": { "id": "aalKxnIyizx3" }, "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": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "A94K670ni2P_" }, "source": [ "* 대표적인 영어-독어 번역 데이터셋인 **Multi30k**를 불러옵니다." ] }, { "cell_type": "code", "metadata": { "id": "FIBPof6BItei", "colab": { "base_uri": "https://localhost:8080/" }, "outputId": "d3526720-1bcf-4f78-f6f3-0320ce2223f6" }, "source": [ "from torchtext.datasets import Multi30k\n", "\n", "train_dataset, valid_dataset, test_dataset = Multi30k.splits(exts=(\".de\", \".en\"), fields=(SRC, TRG))" ], "execution_count": null, "outputs": [ { "output_type": "stream", "text": [ "downloading training.tar.gz\n" ], "name": "stdout" }, { "output_type": "stream", "text": [ "training.tar.gz: 100%|██████████| 1.21M/1.21M [00:02<00:00, 596kB/s]\n" ], "name": "stderr" }, { "output_type": "stream", "text": [ "downloading validation.tar.gz\n" ], "name": "stdout" }, { "output_type": "stream", "text": [ "validation.tar.gz: 100%|██████████| 46.3k/46.3k [00:00<00:00, 172kB/s]\n" ], "name": "stderr" }, { "output_type": "stream", "text": [ "downloading mmt_task1_test2016.tar.gz\n" ], "name": "stdout" }, { "output_type": "stream", "text": [ "mmt_task1_test2016.tar.gz: 100%|██████████| 66.2k/66.2k [00:00<00:00, 163kB/s]\n" ], "name": "stderr" } ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "h17z65VgjFZQ", "outputId": "4e410b7a-fdc6-4f53-e6ac-74d2371ecc00" }, "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": null, "outputs": [ { "output_type": "stream", "text": [ "학습 데이터셋(training dataset) 크기: 29000개\n", "평가 데이터셋(validation dataset) 크기: 1014개\n", "테스트 데이터셋(testing dataset) 크기: 1000개\n" ], "name": "stdout" } ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "5HsqhgGfjGmC", "outputId": "3eb76194-a2eb-48be-dd7c-898eb56250d4" }, "source": [ "# 학습 데이터 중 하나를 선택해 출력\n", "print(vars(train_dataset.examples[30])['src'])\n", "print(vars(train_dataset.examples[30])['trg'])" ], "execution_count": null, "outputs": [ { "output_type": "stream", "text": [ "['ein', 'mann', ',', 'der', 'mit', 'einer', 'tasse', 'kaffee', 'an', 'einem', 'urinal', 'steht', '.']\n", "['a', 'man', 'standing', 'at', 'a', 'urinal', 'with', 'a', 'coffee', 'cup', '.']\n" ], "name": "stdout" } ] }, { "cell_type": "markdown", "metadata": { "id": "Gt6dbR43jMej" }, "source": [ "* **필드(field)** 객체의 **build_vocab** 메서드를 이용해 영어와 독어의 단어 사전을 생성합니다.\n", " * **최소 2번 이상** 등장한 단어만을 선택합니다." ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "NUs-Nf57jIMO", "outputId": "4eb6fa1e-c89d-42d8-8acc-0d23d545e6b9" }, "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": null, "outputs": [ { "output_type": "stream", "text": [ "len(SRC): 7855\n", "len(TRG): 5893\n" ], "name": "stdout" } ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "JkVBmOwljN3H", "outputId": "9b8bf0a2-c23a-4573-e1f4-39e2ad8f55f5" }, "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": null, "outputs": [ { "output_type": "stream", "text": [ "0\n", "1\n", "2\n", "3\n", "4112\n", "1752\n" ], "name": "stdout" } ] }, { "cell_type": "markdown", "metadata": { "id": "TKuySa3njQpB" }, "source": [ "* 한 문장에 포함된 단어가 연속적으로 **RNN**에 입력되어야 합니다.\n", " * 따라서 하나의 배치에 포함된 문장들이 가지는 단어의 개수가 유사하도록 만들면 좋습니다.\n", " * 이를 위해 BucketIterator를 사용합니다.\n", " * **배치 크기(batch size)**: 128" ] }, { "cell_type": "code", "metadata": { "id": "2IXKMVjtjPDm" }, "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": null, "outputs": [] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "M5_krqaxjSPe", "outputId": "7ef2efe6-7561-460c-ce93-6491e3e9153b" }, "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": null, "outputs": [ { "output_type": "stream", "text": [ "첫 번째 배치 크기: torch.Size([35, 128])\n", "인덱스 0: 2\n", "인덱스 1: 8\n", "인덱스 2: 16\n", "인덱스 3: 7123\n", "인덱스 4: 5\n", "인덱스 5: 2390\n", "인덱스 6: 12\n", "인덱스 7: 24\n", "인덱스 8: 317\n", "인덱스 9: 27\n", "인덱스 10: 14\n", "인덱스 11: 50\n", "인덱스 12: 422\n", "인덱스 13: 4\n", "인덱스 14: 3\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", "인덱스 30: 1\n", "인덱스 31: 1\n", "인덱스 32: 1\n", "인덱스 33: 1\n", "인덱스 34: 1\n" ], "name": "stdout" } ] }, { "cell_type": "markdown", "metadata": { "id": "AJuTHG0gjXuU" }, "source": [ "#### **인코더(Encoder) 아키텍처**\n", "\n", "* 주어진 소스 문장을 **문맥 벡터(context vector)로 인코딩**합니다.\n", "* 하이퍼 파라미터(hyperparameter)\n", " * **input_dim**: 하나의 단어에 대한 원핫 인코딩 차원\n", " * **embed_dim**: 임베딩(embedding) 차원\n", " * **enc_hidden_dim**: 인코더의 히든 상태(hidden state) 차원\n", " * **dec_hidden_dim**: 디코더의 히든 상태(hidden state) 차원\n", " * **dropout_ratio**: 드롭아웃(dropout) 비율" ] }, { "cell_type": "code", "metadata": { "id": "XnjffnbYjTQu" }, "source": [ "import torch.nn as nn\n", "\n", "# 인코더(Encoder) 아키텍처 정의\n", "class Encoder(nn.Module):\n", " def __init__(self, input_dim, embed_dim, enc_hidden_dim, dec_hidden_dim, dropout_ratio):\n", " super().__init__()\n", "\n", " # 임베딩(embedding)은 원-핫 인코딩(one-hot encoding)을 특정 차원의 임베딩으로 매핑하는 레이어\n", " self.embedding = nn.Embedding(input_dim, embed_dim)\n", "\n", " # 양방향(bidirectional) GRU 레이어\n", " self.rnn = nn.GRU(embed_dim, enc_hidden_dim, bidirectional=True)\n", "\n", " # FC 레이어\n", " self.fc = nn.Linear(enc_hidden_dim * 2, dec_hidden_dim)\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 = self.rnn(embedded)\n", " # outputs: [단어 개수, 배치 크기, 인코더 히든 차원 * 방향의 수]: 전체 단어의 출력 정보\n", " # hidden: [레이어 개수 * 방향의 수, 배치 크기, 인코더 히든 차원]: 현재까지의 모든 단어의 정보\n", "\n", " # hidden은 [forward_1, backward_1, forward_2, backward_2, ...] 형태로 구성\n", " # 따라서 hidden[-2, :, :]은 forwards의 마지막 값\n", " # 따라서 hidden[-1, :, :]은 backwards의 마지막 값\n", " # 디코더(decoder)의 첫 번째 hidden (context) vector는 인코더의 마지막 hidden을 이용\n", " hidden = torch.tanh(self.fc(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1)))\n", "\n", " # outputs은 Attention 목적으로, hidden은 context vector 목적으로 사용\n", " return outputs, hidden" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "ypx2kLydpu7D" }, "source": [ "#### **어텐션(Attention) 아키텍처**\n", "\n", "* 하나의 어텐션(Attention)은 인코더의 전체 토큰에 대한 출력을 입력으로 받는 FC의 파라미터를 공유하여 사용합니다.\n", "* (전체 인코더 출력 + 현재 디코더의 히든) → 디코더의 히든 → 실제 Attention 값\n", "* 하이퍼 파라미터(hyperparameter)\n", " * **enc_hidden_dim**: 인코더의 히든 상태(hidden state) 차원\n", " * **dec_hidden_dim**: 디코더의 히든 상태(hidden state) 차원" ] }, { "cell_type": "code", "metadata": { "id": "d50749vHphJ3" }, "source": [ "import torch.nn.functional as F\n", "\n", "# 어텐션(Attention) 아키텍처 정의\n", "class Attention(nn.Module):\n", " def __init__(self, enc_hidden_dim, dec_hidden_dim):\n", " super().__init__()\n", "\n", " self.attn = nn.Linear((enc_hidden_dim * 2) + dec_hidden_dim, dec_hidden_dim)\n", " self.v = nn.Linear(dec_hidden_dim, 1, bias=False)\n", "\n", " def forward(self, hidden, enc_outputs):\n", " # hidden: [배치 크기, 히든 차원]: 현재까지의 모든 단어의 정보\n", " # enc_outputs: [단어 개수, 배치 크기, 인코더 히든 차원 * 방향의 수]: 전체 단어의 출력 정보\n", " batch_size = enc_outputs.shape[1]\n", " src_len = enc_outputs.shape[0]\n", "\n", " # 현재 디코더의 히든 상태(hidden state)를 src_len만큼 반복\n", " hidden = hidden.unsqueeze(1).repeat(1, src_len, 1)\n", " enc_outputs = enc_outputs.permute(1, 0, 2)\n", " # hidden: [배치 크기, 단어 개수, 디코더 히든 차원]: 현재까지의 모든 단어의 정보\n", " # enc_outputs: [배치 크기, 단어 개수, 인코더 히든 차원 * 방향의 수]: 전체 단어의 출력 정보\n", "\n", " energy = torch.tanh(self.attn(torch.cat((hidden, enc_outputs), dim=2)))\n", " # energy: [배치 크기, 단어 개수, 디코더 히든 차원]\n", "\n", " attention = self.v(energy).squeeze(2)\n", " # attention: [배치 크기, 단어 개수]: 실제 각 단어에 대한 어텐선(attention) 값들\n", "\n", " return F.softmax(attention, dim=1)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "_ZaTa3VRuOHl" }, "source": [ "#### **디코더(Decoder) 아키텍처**\n", "\n", "* 주어진 문맥 벡터(context vector)를 **타겟 문장으로 디코딩**합니다.\n", "* 단, 디코딩하는 과정에서 매번 인코더의 모든 출력에 대하여 어텐션(attention)합니다.\n", "* 하이퍼 파라미터(hyperparameter)\n", " * **output_dim**: 하나의 단어에 대한 원핫 인코딩 차원\n", " * **embed_dim**: 임베딩(embedding) 차원\n", " * **enc_hidden_dim**: 인코더의 히든 상태(hidden state) 차원\n", " * **dec_hidden_dim**: 디코더의 히든 상태(hidden state) 차원\n", " * **dropout_ratio**: 드롭아웃(dropout) 비율" ] }, { "cell_type": "code", "metadata": { "id": "qiATVJjbuNC2" }, "source": [ "# 디코더(Decoder) 아키텍처 정의\n", "class Decoder(nn.Module):\n", " def __init__(self, output_dim, embed_dim, enc_hidden_dim, dec_hidden_dim, dropout_ratio, attention):\n", " super().__init__()\n", "\n", " self.output_dim = output_dim\n", " self.attention = attention\n", "\n", " # 임베딩(embedding)은 원-핫 인코딩(one-hot encoding) 말고 특정 차원의 임베딩으로 매핑하는 레이어\n", " self.embedding = nn.Embedding(output_dim, embed_dim)\n", "\n", " # GRU 레이어\n", " self.rnn = nn.GRU((enc_hidden_dim * 2) + embed_dim, dec_hidden_dim)\n", "\n", " # FC 레이어\n", " self.fc_out = nn.Linear((enc_hidden_dim * 2) + dec_hidden_dim + embed_dim, output_dim)\n", " \n", " # 드롭아웃(dropout)\n", " self.dropout = nn.Dropout(dropout_ratio)\n", "\n", " # 디코더는 현재까지 출력된 문장에 대한 정보를 입력으로 받아 타겟 문장을 반환 \n", " def forward(self, input, hidden, enc_outputs):\n", " # input: [배치 크기]: 단어의 개수는 항상 1개이도록 구현\n", " # hidden: [배치 크기, 히든 차원]\n", " # enc_outputs: [단어 개수, 배치 크기, 인코더 히든 차원 * 방향의 수]: 전체 단어의 출력 정보\n", " input = input.unsqueeze(0)\n", " # input: [단어 개수 = 1, 배치 크기]\n", "\n", " embedded = self.dropout(self.embedding(input))\n", " # embedded: [단어 개수 = 1, 배치 크기, 임베딩 차원]\n", "\n", " attention = self.attention(hidden, enc_outputs)\n", " # attention: [배치 크기, 단어 개수]: 실제 각 단어에 대한 어텐선(attention) 값들\n", " attention = attention.unsqueeze(1)\n", " # attention: [배치 크기, 1, 단어 개수]: 실제 각 단어에 대한 어텐선(attention) 값들\n", "\n", " enc_outputs = enc_outputs.permute(1, 0, 2)\n", " # enc_outputs: [배치 크기, 단어 개수, 인코더 히든 차원 * 방향의 수]: 전체 단어의 출력 정보\n", "\n", " weighted = torch.bmm(attention, enc_outputs) # 행렬 곱 함수\n", " # weighted: [배치 크기, 1, 인코더 히든 차원 * 방향의 수]\n", "\n", " weighted = weighted.permute(1, 0, 2)\n", " # weighted: [1, 배치 크기, 인코더 히든 차원 * 방향의 수]\n", " \n", " rnn_input = torch.cat((embedded, weighted), dim=2)\n", " # rnn_input: [1, 배치 크기, 인코더 히든 차원 * 방향의 수 + embed_dim]: 어텐션이 적용된 현재 단어 입력 정보\n", " \n", " output, hidden = self.rnn(rnn_input, hidden.unsqueeze(0))\n", " # output: [단어 개수, 배치 크기, 디코더 히든 차원 * 방향의 수]\n", " # hidden: [레이어 개수 * 방향의 수, 배치 크기, 디코더 히든 차원]: 현재까지의 모든 단어의 정보\n", "\n", " # 현재 예제에서는 단어 개수, 레이어 개수, 방향의 수 모두 1의 값을 가짐\n", " # 따라서 output: [1, 배치 크기, 디코더 히든 차원], hidden: [1, 배치 크기, 디코더 히든 차원]\n", " # 다시 말해 output과 hidden의 값 또한 동일\n", " assert (output == hidden).all()\n", "\n", " embedded = embedded.squeeze(0)\n", " output = output.squeeze(0)\n", " weighted = weighted.squeeze(0)\n", " \n", " prediction = self.fc_out(torch.cat((output, weighted, embedded), dim=1))\n", " # prediction = [배치 크기, 출력 차원]\n", " \n", " # (현재 출력 단어, 현재까지의 모든 단어의 정보)\n", " return prediction, hidden.squeeze(0)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "fEyIADZN1K95" }, "source": [ "#### **Seq2Seq with Attention 아키텍처**\n", "\n", "* 앞서 정의한 인코더(encoder)와 디코더(decoder)를 가지고 있는 하나의 아키텍처입니다.\n", " * **인코더(encoder)**: 주어진 소스 문장을 문맥 벡터(context vector)로 인코딩합니다.\n", " * **디코더(decoder)**: 주어진 문맥 벡터(context vector)를 타겟 문장으로 디코딩합니다.\n", " * 단, **디코더는 한 단어씩** 넣어서 한 번씩 결과를 구합니다.\n", " * 또한 디코더는 문맥 벡터 뿐만 아니라 인코더의 모든 출력을 참고하여 어텐션(attention)을 진행합니다.\n", "* **Teacher forcing**: 디코더의 예측(prediction)을 다음 입력으로 사용하지 않고, 실제 목표 출력(ground-truth)을 다음 입력으로 사용하는 기법" ] }, { "cell_type": "code", "metadata": { "id": "XS9NkTCd1I-D" }, "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", " enc_outputs, hidden = 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 = self.decoder(input, hidden, enc_outputs)\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": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "MJG_szt-2MSd" }, "source": [ "#### **학습(Training)**\n", "\n", "* 하이퍼 파라미터 설정 및 모델 초기화" ] }, { "cell_type": "code", "metadata": { "id": "ZCYhFAB83GLx" }, "source": [ "INPUT_DIM = len(SRC.vocab)\n", "OUTPUT_DIM = len(TRG.vocab)\n", "ENCODER_EMBED_DIM = 256\n", "DECODER_EMBED_DIM = 256\n", "ENCODER_HIDDEN_DIM = 512\n", "DECODER_HIDDEN_DIM = 512\n", "ENC_DROPOUT_RATIO = 0.5\n", "DEC_DROPOUT_RATIO = 0.5" ], "execution_count": null, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "K22n1KKM3ww8" }, "source": [ "# 어텐션(attention) 객체 선언\n", "attn = Attention(ENCODER_HIDDEN_DIM, DECODER_HIDDEN_DIM)\n", "\n", "# 인코더(encoder)와 디코더(decoder) 객체 선언\n", "enc = Encoder(INPUT_DIM, ENCODER_EMBED_DIM, ENCODER_HIDDEN_DIM, DECODER_HIDDEN_DIM, ENC_DROPOUT_RATIO)\n", "dec = Decoder(OUTPUT_DIM, DECODER_EMBED_DIM, ENCODER_HIDDEN_DIM, DECODER_HIDDEN_DIM, DEC_DROPOUT_RATIO, attn)\n", "\n", "# Seq2Seq 객체 선언\n", "model = Seq2Seq(enc, dec, device).to(device)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "o4QdXa2v2lgx" }, "source": [ "* **모델 가중치 파라미터 초기화**" ] }, { "cell_type": "code", "metadata": { "id": "3u-a6QGT3K1Q", "colab": { "base_uri": "https://localhost:8080/" }, "outputId": "1efe1fbb-c81f-47ac-bb2e-878121594f3c" }, "source": [ "def init_weights(m):\n", " for name, param in m.named_parameters():\n", " if 'weight' in name:\n", " nn.init.normal_(param.data, mean=0, std=0.01)\n", " else:\n", " nn.init.constant_(param.data, 0)\n", " \n", "model.apply(init_weights)" ], "execution_count": null, "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "Seq2Seq(\n", " (encoder): Encoder(\n", " (embedding): Embedding(7855, 256)\n", " (rnn): GRU(256, 512, bidirectional=True)\n", " (fc): Linear(in_features=1024, out_features=512, bias=True)\n", " (dropout): Dropout(p=0.5, inplace=False)\n", " )\n", " (decoder): Decoder(\n", " (attention): Attention(\n", " (attn): Linear(in_features=1536, out_features=512, bias=True)\n", " (v): Linear(in_features=512, out_features=1, bias=False)\n", " )\n", " (embedding): Embedding(5893, 256)\n", " (rnn): GRU(1280, 512)\n", " (fc_out): Linear(in_features=1792, out_features=5893, bias=True)\n", " (dropout): Dropout(p=0.5, inplace=False)\n", " )\n", ")" ] }, "metadata": { "tags": [] }, "execution_count": 30 } ] }, { "cell_type": "markdown", "metadata": { "id": "XW87m5Me39Al" }, "source": [ "* 학습 및 평가 함수 정의\n", " * 기본적인 Seq2Seq 모델과 완전히 동일하게 작성할 수 있습니다." ] }, { "cell_type": "code", "metadata": { "id": "neIE2upE3tkJ" }, "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": null, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "wofZzAhn3_xn" }, "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": null, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "EAt0uNnm4BVK" }, "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": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "niB7_VVZ4Ptc" }, "source": [ "* 학습(training) 및 검증(validation) 진행\n", " * **학습 횟수(epoch)**: 10" ] }, { "cell_type": "code", "metadata": { "id": "x3Z-IBW84FiK" }, "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": null, "outputs": [] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "22OJL3xx4RJ5", "outputId": "7469e22b-ff2a-4bae-ab4b-818af7d1931d" }, "source": [ "import time\n", "import math\n", "import random\n", "\n", "N_EPOCHS = 10\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_with_attention.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": null, "outputs": [ { "output_type": "stream", "text": [ "Epoch: 01 | Time: 1m 22s\n", "\tTrain Loss: 5.025 | Train PPL: 152.108\n", "\tValidation Loss: 4.942 | Validation PPL: 140.009\n", "Epoch: 02 | Time: 1m 24s\n", "\tTrain Loss: 4.180 | Train PPL: 65.343\n", "\tValidation Loss: 4.581 | Validation PPL: 97.658\n", "Epoch: 03 | Time: 1m 25s\n", "\tTrain Loss: 3.530 | Train PPL: 34.108\n", "\tValidation Loss: 3.809 | Validation PPL: 45.094\n", "Epoch: 04 | Time: 1m 24s\n", "\tTrain Loss: 2.961 | Train PPL: 19.314\n", "\tValidation Loss: 3.429 | Validation PPL: 30.847\n", "Epoch: 05 | Time: 1m 25s\n", "\tTrain Loss: 2.571 | Train PPL: 13.076\n", "\tValidation Loss: 3.211 | Validation PPL: 24.799\n", "Epoch: 06 | Time: 1m 25s\n", "\tTrain Loss: 2.271 | Train PPL: 9.689\n", "\tValidation Loss: 3.159 | Validation PPL: 23.539\n", "Epoch: 07 | Time: 1m 24s\n", "\tTrain Loss: 2.020 | Train PPL: 7.535\n", "\tValidation Loss: 3.178 | Validation PPL: 24.009\n", "Epoch: 08 | Time: 1m 24s\n", "\tTrain Loss: 1.799 | Train PPL: 6.045\n", "\tValidation Loss: 3.294 | Validation PPL: 26.963\n", "Epoch: 09 | Time: 1m 24s\n", "\tTrain Loss: 1.624 | Train PPL: 5.073\n", "\tValidation Loss: 3.262 | Validation PPL: 26.097\n", "Epoch: 10 | Time: 1m 24s\n", "\tTrain Loss: 1.490 | Train PPL: 4.436\n", "\tValidation Loss: 3.319 | Validation PPL: 27.639\n" ], "name": "stdout" } ] }, { "cell_type": "code", "metadata": { "id": "hxMwo05X8JIZ", "colab": { "base_uri": "https://localhost:8080/", "height": 17 }, "outputId": "79f2f55e-6be2-41f4-a85e-9a1e19ac4c76" }, "source": [ "# 학습된 모델 저장\n", "from google.colab import files\n", "\n", "files.download('seq2seq_with_attention.pt')" ], "execution_count": null, "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_b8834770-92df-4df4-97e5-291d17a93cf4\", \"seq2seq_with_sequential.pt\", 82081062)" ], "text/plain": [ "" ] }, "metadata": { "tags": [] } } ] }, { "cell_type": "markdown", "metadata": { "id": "cAAwxrgE9ujD" }, "source": [ "#### **모델 최종 테스트(testing) 결과 확인**" ] }, { "cell_type": "code", "metadata": { "id": "_CHFydtw44xZ", "colab": { "base_uri": "https://localhost:8080/" }, "outputId": "14b04205-5e67-4842-aeea-dc409ed71eae" }, "source": [ "!wget https://postechackr-my.sharepoint.com/:u:/g/personal/dongbinna_postech_ac_kr/EQkAXuEiglpMgofwF0HAp5oBhFspDt7_V3JBtEiCAKNbjw?download=1 -O seq2seq_with_attention.pt" ], "execution_count": null, "outputs": [ { "output_type": "stream", "text": [ "--2020-12-01 08:24:24-- https://postechackr-my.sharepoint.com/:u:/g/personal/dongbinna_postech_ac_kr/EQkAXuEiglpMgofwF0HAp5oBhFspDt7_V3JBtEiCAKNbjw?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_with_attention.pt?originalPath=aHR0cHM6Ly9wb3N0ZWNoYWNrci1teS5zaGFyZXBvaW50LmNvbS86dTovZy9wZXJzb25hbC9kb25nYmlubmFfcG9zdGVjaF9hY19rci9FUWtBWHVFaWdscE1nb2Z3RjBIQXA1b0JoRnNwRHQ3X1YzSkJ0RWlDQUtOYmp3P3J0aW1lPVNwdXZnOUtWMkVn [following]\n", "--2020-12-01 08:24:25-- https://postechackr-my.sharepoint.com/personal/dongbinna_postech_ac_kr/Documents/Research/models/seq2seq_with_attention.pt?originalPath=aHR0cHM6Ly9wb3N0ZWNoYWNrci1teS5zaGFyZXBvaW50LmNvbS86dTovZy9wZXJzb25hbC9kb25nYmlubmFfcG9zdGVjaF9hY19rci9FUWtBWHVFaWdscE1nb2Z3RjBIQXA1b0JoRnNwRHQ3X1YzSkJ0RWlDQUtOYmp3P3J0aW1lPVNwdXZnOUtWMkVn\n", "Reusing existing connection to postechackr-my.sharepoint.com:443.\n", "HTTP request sent, awaiting response... 200 OK\n", "Length: 82081062 (78M) [application/octet-stream]\n", "Saving to: ‘seq2seq_with_attention.pt’\n", "\n", "seq2seq_with_attent 100%[===================>] 78.28M 19.1MB/s in 4.1s \n", "\n", "2020-12-01 08:24:30 (19.1 MB/s) - ‘seq2seq_with_attention.pt’ saved [82081062/82081062]\n", "\n" ], "name": "stdout" } ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "CrMv5cD29yKI", "outputId": "c1c9e981-f6fa-43ff-f559-ee203b32a41e" }, "source": [ "model.load_state_dict(torch.load('seq2seq_with_attention.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": null, "outputs": [ { "output_type": "stream", "text": [ "Test Loss: 3.181 | Test PPL: 24.074\n" ], "name": "stdout" } ] }, { "cell_type": "markdown", "metadata": { "id": "CSjFAq_091iF" }, "source": [ "#### **나만의 데이터로 모델 사용해보기**" ] }, { "cell_type": "code", "metadata": { "id": "-ZuRXGB592W9" }, "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", " enc_outputs, hidden = 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 = model.decoder(trg_tensor, hidden, enc_outputs)\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": null, "outputs": [] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "l5MpIAAo93UE", "outputId": "dd99d305-772f-4886-fef6-4f4f1dc3c35f" }, "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": null, "outputs": [ { "output_type": "stream", "text": [ "소스 문장: ['eine', 'mutter', 'und', 'ihr', 'kleiner', 'sohn', 'genießen', 'einen', 'schönen', 'tag', 'im', 'freien', '.']\n", "타겟 문장: ['a', 'mother', 'and', 'her', 'young', 'song', 'enjoying', 'a', 'beautiful', 'day', 'outside', '.']\n", "전체 소스 토큰: ['', 'eine', 'mutter', 'und', 'ihr', 'kleiner', 'sohn', 'genießen', 'einen', 'schönen', 'tag', 'im', 'freien', '.', '']\n", "소스 문장 인덱스: [2, 8, 364, 10, 134, 70, 624, 565, 19, 780, 200, 20, 88, 4, 3]\n", "모델 출력 결과: a mother and her son enjoying a beautiful day . \n" ], "name": "stdout" } ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "fpZnxDn894jW", "outputId": "b55c3cc0-4a63-45ba-c3e5-f5ac33cded14" }, "source": [ "src = tokenize_de(\"Guten Abend.\")\n", "\n", "print(f'소스 문장: {src}')\n", "print(\"모델 출력 결과:\", \" \".join(translate_sentence(src, SRC, TRG, model, device)))" ], "execution_count": null, "outputs": [ { "output_type": "stream", "text": [ "소스 문장: ['Guten', 'Abend', '.']\n", "전체 소스 토큰: ['', 'guten', 'abend', '.', '']\n", "소스 문장 인덱스: [2, 3799, 1163, 4, 3]\n", "모델 출력 결과: local villagers are preparing to dusk . \n" ], "name": "stdout" } ] } ] }