├── Week 01 ├── Images │ ├── BOW.png │ └── BOW_weights.png └── README.md ├── Week 03 ├── Images │ ├── CBOW.png │ ├── Circuit.png │ ├── SkipGram.png │ ├── Negative_Sampling.png │ ├── StructuredWord2vec.png │ └── Word2vecExample.jpeg └── README.md ├── Week 02 └── README.md ├── Week 05 └── README.md ├── Week 13 ├── README.md └── Week_13_Dialogue_Systems_(Part_2).ipynb ├── Week 10 └── README.md ├── Week 09 └── README.md ├── Week 04 └── README.md ├── Week 14 └── README.md ├── Week 07 ├── README.md └── Week_07_Language_Models.ipynb ├── Week 12 ├── README.md └── Week_12_Dialogue_Systems_(Part_1).ipynb ├── Week 11 ├── README.md └── Week_11_Transformers.ipynb ├── Week 08 ├── README.md └── Week_08_Language_Models_(Part_2).ipynb ├── Week 06 └── README.md └── README.md /Week 01/Images/BOW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanAnastasyev/DeepNLP-Course/HEAD/Week 01/Images/BOW.png -------------------------------------------------------------------------------- /Week 03/Images/CBOW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanAnastasyev/DeepNLP-Course/HEAD/Week 03/Images/CBOW.png -------------------------------------------------------------------------------- /Week 03/Images/Circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanAnastasyev/DeepNLP-Course/HEAD/Week 03/Images/Circuit.png -------------------------------------------------------------------------------- /Week 03/Images/SkipGram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanAnastasyev/DeepNLP-Course/HEAD/Week 03/Images/SkipGram.png -------------------------------------------------------------------------------- /Week 01/Images/BOW_weights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanAnastasyev/DeepNLP-Course/HEAD/Week 01/Images/BOW_weights.png -------------------------------------------------------------------------------- /Week 03/Images/Negative_Sampling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanAnastasyev/DeepNLP-Course/HEAD/Week 03/Images/Negative_Sampling.png -------------------------------------------------------------------------------- /Week 03/Images/StructuredWord2vec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanAnastasyev/DeepNLP-Course/HEAD/Week 03/Images/StructuredWord2vec.png -------------------------------------------------------------------------------- /Week 03/Images/Word2vecExample.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanAnastasyev/DeepNLP-Course/HEAD/Week 03/Images/Word2vecExample.jpeg -------------------------------------------------------------------------------- /Week 01/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре = 5 баллов 4 | - Лемматизация = +1 балл 5 | - NER = +1 балл 6 | - Логистическая регрессия = +3 балла -------------------------------------------------------------------------------- /Week 02/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Классификация с Bag-of-words: +1 балл 5 | - Классификация с Tf-idf: +1 балл 6 | - Машинный перевод: +3 балла -------------------------------------------------------------------------------- /Week 03/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Доделать CBoW модель: +1 балл 5 | - Реализовать Negative Sampling: +4 балл 6 | - Structured Word2Vec: +2 балла -------------------------------------------------------------------------------- /Week 05/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Добавить тестирование модели + попробовать улучшения - +2.5 балла 5 | - Добавить визуализацию работы модели - +2.5 балл 6 | -------------------------------------------------------------------------------- /Week 13/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Реализовать hard negatives mining: +2 балла 5 | - Реализовать semi-hard negatives mining: +1.5 балла 6 | - Реализовать болталку: +3 балла 7 | -------------------------------------------------------------------------------- /Week 10/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Реализовать другие attention'ы: +2 балла 5 | - Добавить улучшения в модель: +2 балла 6 | - Реализовать подписывание картинок с attention'ом: +2 балла -------------------------------------------------------------------------------- /Week 09/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Scheduled Sampling: +1 балл 5 | - Улучшения модели: +2 баллов 6 | - Byte-pair encoding: +1 балл 7 | - Beam search: +3 балла 8 | - Image captioning: +2 балла -------------------------------------------------------------------------------- /Week 04/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Доделать модель и оценить её качество - +1 балл 5 | - Визуализировать эмбеддинги модели - +1 балл 6 | - Визуализировать свертки модели - +1.5 балла 7 | - Поэкспериментировать с улучшением модели - +1.5 балла 8 | -------------------------------------------------------------------------------- /Week 14/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Реализовать 1NN на представлениях из USE - +1 балл 4 | - Теггер на предобученных эмбеддингах - +1 балл 5 | - Теггер на ELMo - +1 балл 6 | - Теггер с CRF - +1 балл 7 | - Запустить обучение BERT - +0.5 балла 8 | - Запустить обучение с накоплением градиентов - +1 балл -------------------------------------------------------------------------------- /Week 07/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Добавить улучшения в модель: sgd, dropout, попробовать больше юнитов и слоев LSTM - +2 балла 5 | - Реализовать модель для генерации фамилий - +2 балла 6 | - Обучить модель для Toxic Comment Classification Challenge - +3 балла -------------------------------------------------------------------------------- /Week 12/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Реализовать multi-task модель: +1 балл 5 | - Реализовать async training модель: +2 балла 6 | - Улучшить модель, подсмотрев параметры в статье: +0.5 балла 7 | - Добавить декодер в модель: +1.5 балла 8 | - Реализовать POS tagger: +5 баллов -------------------------------------------------------------------------------- /Week 11/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Доделать модель: +1.5 баллов 5 | - Реализовать генератор для модели: +1 балл 6 | - Реализовать оценку модели: +1 балл 7 | - Реализовать визуализатор: +0.5 баллов 8 | - Расшарить эмбеддинги: +1 балл 9 | - Реализовать LabelSmoothing: +1 балл 10 | - Реализовать Pointer-Generator модель: +5 баллов -------------------------------------------------------------------------------- /Week 08/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Добавить обработку метрических шаблонов: +2 балла 5 | - Сделать перенос модели с порошков на пирожки: +1 балл 6 | - Сделать единую модель для пирожков и порошков: +1 балл 7 | - Добавить разные улучшения в модель: +1 балл 8 | - Доделать модель, обрабатывающую леммы и грамматические значения: +2 балла 9 | - Реализовать модель с LDA векторами: +1 балл 10 | -------------------------------------------------------------------------------- /Week 06/README.md: -------------------------------------------------------------------------------- 1 | # Разбалловка 2 | 3 | - Задания, сделанные на семинаре: 5 баллов 4 | - Попробовать модель с предобученными эмбеддингами + инференс с использованием всей матрицы эмбеддингов - +1 балл 5 | - Попробовать дообучать эмбеддинги + использовать регуляризацию предобученных эмбеддингов - +1.5 балл 6 | - Реализовать модель с символьными эмбеддингами и визуализировать эмбеддинги - +1 балл 7 | - Реализовать ещё и альтернативный вариант функции над символьными эмбеддингами - +0.5 баллов 8 | - Реализовать модель со словными и символьными эмбеддингами - +1 балл 9 | - Реализовать энкодер-декодер модель - +5 баллов -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deep NLP Course at ABBYY 2 | 3 | Deep learning for NLP crash course at ABBYY. 4 | 5 | Suggested textbook: [Neural Network Methods in Natural Language Processing by Yoav Goldberg](https://www.amazon.com/Language-Processing-Synthesis-Lectures-Technologies/dp/1627052984) 6 | 7 | *I'm gradually updating and translating the notebooks right now. Stay in touch.* 8 | 9 | ## Materials 10 | ### Week 1: *Introduction* 11 | Sentiment analysis on the IMDB movie review dataset: a short overview of classical machine learning for NLP + indecently brief intro to keras. 12 | 13 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/12nrEX3JXTxsHWC-HpuwkTWyJybjmkZu-#forceEdit=true&offline=true&sandboxMode=true) 14 | 15 | Updated English version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1eW-mN3gEdLluYe1W1unQ-7-71by8eauz#scrollTo=OlqOAQmQGXOL&forceEdit=true&offline=true&sandboxMode=true) 16 | 17 | ### Week 2: *Word Embeddings: Part 1* 18 | Meet the Word Embeddings: an unsupervised method to capture some fun relationships between words. 19 | Phrases similarity with word embeddings model + word based machine translation without parallel data (with MUSE word embeddings). 20 | 21 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1o65wrq6RYgWyyMvNP8r9ZknXBniDoXrn#forceEdit=true&offline=true&sandboxMode=true) 22 | 23 | Updated English version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1ey9NARKvk-c4vfQGdvOkPjp5wNGmxd5o#forceEdit=true&offline=true&sandboxMode=true) 24 | 25 | ### Week 3: *Word Embeddings: Part 2* 26 | Introduction to PyTorch. Implementation of pet linear regression on pure numpy and pytorch. Implementations of CBoW, skip-gram, negative sampling and structured Word2vec models. 27 | 28 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1YruNhE5aEJfLpaCZSKGIaZ1hOQR5qoIG#forceEdit=true&offline=true&sandboxMode=true) 29 | 30 | Updated English version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1Cgdg_jUIbhMBZiL3DtUQMN6xqFUYjKlg#forceEdit=true&offline=true&sandboxMode=true) 31 | 32 | ### Week 4: *Convolutional Neural Networks* 33 | Introduction to convolutional networks. Relations between convolutions and n-grams. Simple surname detector on character-level convolutions + fun visualizations. 34 | 35 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1Vo_yuiA7xLjavUA_7ayLeosGJyMsyDAt#forceEdit=true&offline=true&sandboxMode=true) 36 | 37 | Updated English version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1iwhfaHp_L2loxjvbqW9DhO9BaWlIWzpB#forceEdit=true&offline=true&sandboxMode=true) 38 | 39 | ### Week 5: *RNNs: Part 1* 40 | RNNs for text classification. Simple RNN implementation + memorization test. Surname detector in multilingual setup: character-level LSTM classifier. 41 | 42 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1-FoMnf7s-BYNM7jT9UF3u9m63h7dSq3_#forceEdit=true&offline=true&sandboxMode=true) 43 | 44 | Updated English version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WA9YA30m7xFYfLyptuW7lHROvVGbZWZo#forceEdit=true&offline=true&sandboxMode=true) 45 | 46 | ### Week 6: *RNNs: Part 2* 47 | RNNs for sequence labelling. Part-of-speech tagger implementations based on word embeddings and character-level word embeddings. 48 | 49 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1A7dbNANHg8srCemnwFI8WB1wLhvmuJp0#forceEdit=true&offline=true&sandboxMode=true) 50 | 51 | ### Week 7: *Language Models: Part 1* 52 | Character-level language model for Russian troll tweets generation: fixed-window model via convolutions and RNN model. 53 | Simple conditional language model: surname generation given source language. 54 | And Toxic Comment Classification Challenge - to apply your skills to a real-world problem. 55 | 56 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1W5uaNpKFoaq1gV9N9FpIAEDyrsGGRBBi#forceEdit=true&offline=true&sandboxMode=true) 57 | 58 | ### Week 8: *Language Models: Part 2* 59 | Word-level language model for poetry generation. Pet examples of transfer learning and multi-task learning applied to language models. 60 | 61 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1lUlBsdvAYJc5rLHwkOICyFhvns5Ssp1X#forceEdit=true&offline=true&sandboxMode=true) 62 | 63 | ### Week 9: *Seq2seq* 64 | Seq2seq for machine translation and image captioning. Byte-pair encoding, beam search and other usefull stuff for machine translation. 65 | 66 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1jSYWuEGwik2lnnvGSU_PyXTFtbRKSyz_#forceEdit=true&offline=true&sandboxMode=true) 67 | 68 | ### Week 10: *Seq2seq with Attention* 69 | Seq2seq with attention for machine translation and image captioning. 70 | 71 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1xZed_YAQf20fYacr9anE7T4EsdC_R0Oy#forceEdit=true&offline=true&sandboxMode=true) 72 | 73 | ### Week 11: *Transformers & Text Summarization* 74 | Implementation of Transformer model for text summarization. Discussion of Pointer-Generator Networks for text summarization. 75 | 76 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1wy5BDHZVEm-vSeH8U4Xh0Sm3bArwVWGU#forceEdit=true&offline=true&sandboxMode=true) 77 | 78 | ### Week 12: *Dialogue Systems: Part 1* 79 | Goal-orientied dialogue systems. Implemention of the multi-task model: intent classifier and token tagger for dialogue manager. 80 | 81 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1lNhbHboRYVb-caV7Ktj9cWJnHW7DPD9-#forceEdit=true&offline=true&sandboxMode=true) 82 | 83 | ### Week 13: *Dialogue Systems: Part 2* 84 | General conversation dialogue systems and DSSMs. Implementation of question answering model on SQuAD dataset and chit-chat model on OpenSubtitles dataset. 85 | 86 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/19kQoxDWhv9VOfXxZCHcB39qIM30gziCR#forceEdit=true&offline=true&sandboxMode=true) 87 | 88 | ### Week 14: *Pretrained Models* 89 | Pretrained models for various tasks: Universal Sentence Encoder for sentence similarity, ELMo for sequence tagging (with a bit of CRF), BERT for SWAG - reasoning about possible continuation. 90 | 91 | Russian version: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WMspBJe-m0mJHb7SbDTj64W-5-3AxW7v#forceEdit=true&offline=true&sandboxMode=true) 92 | 93 | ### Final Presentation 94 | [NLP Summary](https://drive.google.com/open?id=16GV-jSGtMAQPJgO_B6q1gLYXL10vM8Ev) - summary of cool stuff that appeared and didn't in the course. 95 | -------------------------------------------------------------------------------- /Week 12/Week_12_Dialogue_Systems_(Part_1).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "Week 12 - Dialogue Systems (Part 1).ipynb", 7 | "version": "0.3.2", 8 | "provenance": [], 9 | "collapsed_sections": [] 10 | }, 11 | "kernelspec": { 12 | "name": "python3", 13 | "display_name": "Python 3" 14 | }, 15 | "accelerator": "GPU" 16 | }, 17 | "cells": [ 18 | { 19 | "metadata": { 20 | "id": "Z4WlMyJVRkzQ", 21 | "colab_type": "code", 22 | "colab": {} 23 | }, 24 | "cell_type": "code", 25 | "source": [ 26 | "!pip3 -qq install torch==0.4.1\n", 27 | "!pip -qq install torchtext==0.3.1\n", 28 | "!git clone https://github.com/MiuLab/SlotGated-SLU.git\n", 29 | "!wget -qq https://raw.githubusercontent.com/yandexdataschool/nlp_course/master/week08_multitask/conlleval.py" 30 | ], 31 | "execution_count": 0, 32 | "outputs": [] 33 | }, 34 | { 35 | "metadata": { 36 | "id": "UvJKy3mtVOpw", 37 | "colab_type": "code", 38 | "colab": {} 39 | }, 40 | "cell_type": "code", 41 | "source": [ 42 | "import numpy as np\n", 43 | "\n", 44 | "import torch\n", 45 | "import torch.nn as nn\n", 46 | "import torch.nn.functional as F\n", 47 | "import torch.optim as optim\n", 48 | "\n", 49 | "\n", 50 | "if torch.cuda.is_available():\n", 51 | " from torch.cuda import FloatTensor, LongTensor\n", 52 | " DEVICE = torch.device('cuda')\n", 53 | "else:\n", 54 | " from torch import FloatTensor, LongTensor\n", 55 | " DEVICE = torch.device('cpu')\n", 56 | "\n", 57 | "np.random.seed(42)" 58 | ], 59 | "execution_count": 0, 60 | "outputs": [] 61 | }, 62 | { 63 | "metadata": { 64 | "id": "4QR5dTAfVhLD", 65 | "colab_type": "text" 66 | }, 67 | "cell_type": "markdown", 68 | "source": [ 69 | "# Диалоговые системы" 70 | ] 71 | }, 72 | { 73 | "metadata": { 74 | "id": "fox5ub_GKSLL", 75 | "colab_type": "text" 76 | }, 77 | "cell_type": "markdown", 78 | "source": [ 79 | "Диалоговые системы делятся на два типа - *goal-orientied* и *general conversation*.\n", 80 | "\n", 81 | "**General conversation** - это болталка, разговор на свободную тему: \n", 82 | "![](https://i.ibb.co/bFwwGpc/alice.jpg =300x)\n", 83 | "\n", 84 | "Сегодня будем говорить не про них, а про **goal-orientied** системы:\n", 85 | "![](https://hsto.org/webt/gj/3y/xl/gj3yxlqbr7ujuqr9r2akacxmkee.jpeg =700x) \n", 86 | "*From [Как устроена Алиса](https://habr.com/company/yandex/blog/349372/)*\n", 87 | "\n", 88 | "Пользователь говорит что-то, это что-то распознается. По распознанному определяется - что, где и когда он хотел. Дальше диалоговый движок решает, действительно ли пользователь знает, чего хотел попросить. Происходит поход в источники - узнать информацию, которую (кажется) запросил пользователь. Исходя из всего этого генерируется некоторый ответ:\n", 89 | "\n", 90 | "![](https://i.ibb.co/8XcdpJ7/goal-orientied.png =900x) \n", 91 | "*From [Как устроена Алиса](https://habr.com/company/yandex/blog/349372/)*\n", 92 | "\n", 93 | "([Клёвая гифка, на которой то же самое, но в динамике](https://raw.githubusercontent.com/yandexdataschool/nlp_course/master/resources/task_oriented_dialog_systems.gif))\n", 94 | "\n", 95 | "Будем учить ту часть, которая посередине - классификатор и теггер. Все остальное обычно - эвристики и захардкоженные ответы." 96 | ] 97 | }, 98 | { 99 | "metadata": { 100 | "id": "nIJt4hPLPYtO", 101 | "colab_type": "text" 102 | }, 103 | "cell_type": "markdown", 104 | "source": [ 105 | "## Загрузка данных" 106 | ] 107 | }, 108 | { 109 | "metadata": { 110 | "id": "iUZ8xjG_PT7C", 111 | "colab_type": "text" 112 | }, 113 | "cell_type": "markdown", 114 | "source": [ 115 | "Есть условно стандартный датасет - atis, который неприлично маленький, на самом деле.\n", 116 | "\n", 117 | "К нему можно взять еще датасет snips - он больше и разнообразнее.\n", 118 | "\n", 119 | "Оба датасета возьмем из репозитория статьи [Slot-Gated Modeling for Joint Slot Filling and Intent Prediction](http://aclweb.org/anthology/N18-2118).\n", 120 | "\n", 121 | "Начнем с atis." 122 | ] 123 | }, 124 | { 125 | "metadata": { 126 | "id": "yw_FnOVOVgdX", 127 | "colab_type": "code", 128 | "colab": {} 129 | }, 130 | "cell_type": "code", 131 | "source": [ 132 | "import os \n", 133 | "\n", 134 | "def read_dataset(path):\n", 135 | " with open(os.path.join(path, 'seq.in')) as f_words, \\\n", 136 | " open(os.path.join(path, 'seq.out')) as f_tags, \\\n", 137 | " open(os.path.join(path, 'label')) as f_intents:\n", 138 | " \n", 139 | " return [\n", 140 | " (words.strip().split(), tags.strip().split(), intent.strip()) \n", 141 | " for words, tags, intent in zip(f_words, f_tags, f_intents)\n", 142 | " ]" 143 | ], 144 | "execution_count": 0, 145 | "outputs": [] 146 | }, 147 | { 148 | "metadata": { 149 | "id": "JrAgjAFVWnh9", 150 | "colab_type": "code", 151 | "colab": {} 152 | }, 153 | "cell_type": "code", 154 | "source": [ 155 | "train_data = read_dataset('SlotGated-SLU/data/atis/train/')\n", 156 | "val_data = read_dataset('SlotGated-SLU/data/atis/valid/')\n", 157 | "test_data = read_dataset('SlotGated-SLU/data/atis/test/')" 158 | ], 159 | "execution_count": 0, 160 | "outputs": [] 161 | }, 162 | { 163 | "metadata": { 164 | "id": "l3zvT5BsWv0p", 165 | "colab_type": "code", 166 | "colab": {} 167 | }, 168 | "cell_type": "code", 169 | "source": [ 170 | "intent_to_example = {example[2]: example for example in train_data}\n", 171 | "for example in intent_to_example.values():\n", 172 | " print('Intent:\\t', example[2])\n", 173 | " print('Text:\\t', '\\t'.join(example[0]))\n", 174 | " print('Tags:\\t', '\\t'.join(example[1]))\n", 175 | " print()" 176 | ], 177 | "execution_count": 0, 178 | "outputs": [] 179 | }, 180 | { 181 | "metadata": { 182 | "id": "4EoT_us7Y23P", 183 | "colab_type": "code", 184 | "colab": {} 185 | }, 186 | "cell_type": "code", 187 | "source": [ 188 | "from torchtext.data import Field, LabelField, Example, Dataset, BucketIterator\n", 189 | "\n", 190 | "tokens_field = Field()\n", 191 | "tags_field = Field(unk_token=None)\n", 192 | "intent_field = LabelField()\n", 193 | "\n", 194 | "fields = [('tokens', tokens_field), ('tags', tags_field), ('intent', intent_field)]\n", 195 | "\n", 196 | "train_dataset = Dataset([Example.fromlist(example, fields) for example in train_data], fields)\n", 197 | "val_dataset = Dataset([Example.fromlist(example, fields) for example in val_data], fields)\n", 198 | "test_dataset = Dataset([Example.fromlist(example, fields) for example in test_data], fields)\n", 199 | "\n", 200 | "tokens_field.build_vocab(train_dataset)\n", 201 | "tags_field.build_vocab(train_dataset)\n", 202 | "intent_field.build_vocab(train_dataset)\n", 203 | "\n", 204 | "print('Vocab size =', len(tokens_field.vocab))\n", 205 | "print('Tags count =', len(tags_field.vocab))\n", 206 | "print('Intents count =', len(intent_field.vocab))\n", 207 | "\n", 208 | "train_iter, val_iter, test_iter = BucketIterator.splits(\n", 209 | " datasets=(train_dataset, val_dataset, test_dataset), batch_sizes=(32, 128, 128), \n", 210 | " shuffle=True, device=DEVICE, sort=False\n", 211 | ")" 212 | ], 213 | "execution_count": 0, 214 | "outputs": [] 215 | }, 216 | { 217 | "metadata": { 218 | "id": "Rx8tY7_xQIpi", 219 | "colab_type": "text" 220 | }, 221 | "cell_type": "markdown", 222 | "source": [ 223 | "## Классификатор интентов\n", 224 | "\n", 225 | "Начнем с классификатора: к какому интенту относится данный запрос.\n", 226 | "\n", 227 | "**Задание** Ничего умного - возьмите rnn'ку и научитесь предсказывать метки-интенты." 228 | ] 229 | }, 230 | { 231 | "metadata": { 232 | "id": "u4pZR9IRckK-", 233 | "colab_type": "code", 234 | "colab": {} 235 | }, 236 | "cell_type": "code", 237 | "source": [ 238 | "class IntentClassifierModel(nn.Module):\n", 239 | " def __init__(self, vocab_size, intents_count, emb_dim=64, lstm_hidden_dim=128, num_layers=1):\n", 240 | " super().__init__()\n", 241 | "\n", 242 | " \n", 243 | "\n", 244 | " def forward(self, inputs):\n", 245 | " " 246 | ], 247 | "execution_count": 0, 248 | "outputs": [] 249 | }, 250 | { 251 | "metadata": { 252 | "id": "ppZMvviI0iXf", 253 | "colab_type": "text" 254 | }, 255 | "cell_type": "markdown", 256 | "source": [ 257 | "**Задание** `ModelTrainer` для подсчета лосса и accuracy." 258 | ] 259 | }, 260 | { 261 | "metadata": { 262 | "id": "MfKFMfeg_RLV", 263 | "colab_type": "code", 264 | "colab": {} 265 | }, 266 | "cell_type": "code", 267 | "source": [ 268 | "class ModelTrainer():\n", 269 | " def __init__(self, model, criterion, optimizer):\n", 270 | " self.model = model\n", 271 | " self.criterion = criterion\n", 272 | " self.optimizer = optimizer\n", 273 | " \n", 274 | " def on_epoch_begin(self, is_train, name, batches_count):\n", 275 | " \"\"\"\n", 276 | " Initializes metrics\n", 277 | " \"\"\"\n", 278 | " self.epoch_loss = 0\n", 279 | " self.correct_count, self.total_count = 0, 0\n", 280 | " self.is_train = is_train\n", 281 | " self.name = name\n", 282 | " self.batches_count = batches_count\n", 283 | " \n", 284 | " self.model.train(is_train)\n", 285 | " \n", 286 | " def on_epoch_end(self):\n", 287 | " \"\"\"\n", 288 | " Outputs final metrics\n", 289 | " \"\"\"\n", 290 | " return '{:>5s} Loss = {:.5f}, Accuracy = {:.2%}'.format(\n", 291 | " self.name, self.epoch_loss / self.batches_count, self.correct_count / self.total_count\n", 292 | " )\n", 293 | " \n", 294 | " def on_batch(self, batch):\n", 295 | " \"\"\"\n", 296 | " Performs forward and (if is_train) backward pass with optimization, updates metrics\n", 297 | " \"\"\"\n", 298 | " " 299 | ], 300 | "execution_count": 0, 301 | "outputs": [] 302 | }, 303 | { 304 | "metadata": { 305 | "id": "IqCvQEByddtj", 306 | "colab_type": "code", 307 | "colab": {} 308 | }, 309 | "cell_type": "code", 310 | "source": [ 311 | "import math\n", 312 | "from tqdm import tqdm\n", 313 | "tqdm.get_lock().locks = []\n", 314 | "\n", 315 | "\n", 316 | "def do_epoch(trainer, data_iter, is_train, name=None):\n", 317 | " trainer.on_epoch_begin(is_train, name, batches_count=len(data_iter))\n", 318 | " \n", 319 | " with torch.autograd.set_grad_enabled(is_train):\n", 320 | " with tqdm(total=trainer.batches_count) as progress_bar:\n", 321 | " for i, batch in enumerate(data_iter):\n", 322 | " batch_progress = trainer.on_batch(batch)\n", 323 | "\n", 324 | " progress_bar.update()\n", 325 | " progress_bar.set_description(batch_progress)\n", 326 | " \n", 327 | " epoch_progress = trainer.on_epoch_end()\n", 328 | " progress_bar.set_description(epoch_progress)\n", 329 | " progress_bar.refresh()\n", 330 | "\n", 331 | " \n", 332 | "def fit(trainer, train_iter, epochs_count=1, val_iter=None):\n", 333 | " best_val_loss = None\n", 334 | " for epoch in range(epochs_count):\n", 335 | " name_prefix = '[{} / {}] '.format(epoch + 1, epochs_count)\n", 336 | " do_epoch(trainer, train_iter, is_train=True, name=name_prefix + 'Train:')\n", 337 | " \n", 338 | " if not val_iter is None:\n", 339 | " do_epoch(trainer, val_iter, is_train=False, name=name_prefix + ' Val:')" 340 | ], 341 | "execution_count": 0, 342 | "outputs": [] 343 | }, 344 | { 345 | "metadata": { 346 | "id": "JQBsP8SHhjqm", 347 | "colab_type": "code", 348 | "colab": {} 349 | }, 350 | "cell_type": "code", 351 | "source": [ 352 | "model = IntentClassifierModel(vocab_size=len(tokens_field.vocab), intents_count=len(intent_field.vocab)).to(DEVICE)\n", 353 | "\n", 354 | "criterion = nn.CrossEntropyLoss().to(DEVICE)\n", 355 | "optimizer = optim.Adam(model.parameters())\n", 356 | "\n", 357 | "trainer = ModelTrainer(model, criterion, optimizer)\n", 358 | "\n", 359 | "fit(trainer, train_iter, epochs_count=30, val_iter=val_iter)" 360 | ], 361 | "execution_count": 0, 362 | "outputs": [] 363 | }, 364 | { 365 | "metadata": { 366 | "id": "O9YhthG30xp2", 367 | "colab_type": "text" 368 | }, 369 | "cell_type": "markdown", 370 | "source": [ 371 | "**Задание** Подсчитайте итоговое качество на тесте." 372 | ] 373 | }, 374 | { 375 | "metadata": { 376 | "id": "JxHshnyZjMuX", 377 | "colab_type": "code", 378 | "colab": {} 379 | }, 380 | "cell_type": "code", 381 | "source": [ 382 | "" 383 | ], 384 | "execution_count": 0, 385 | "outputs": [] 386 | }, 387 | { 388 | "metadata": { 389 | "id": "5zsAJJCEQ8Ti", 390 | "colab_type": "text" 391 | }, 392 | "cell_type": "markdown", 393 | "source": [ 394 | "## Теггер\n", 395 | "\n", 396 | "![](https://commons.bmstu.wiki/images/0/00/NER1.png) \n", 397 | "*From [NER](https://ru.bmstu.wiki/NER_(Named-Entity_Recognition)*\n", 398 | "\n", 399 | "**Задание** Всё ещё ничего умного - простой теггер, как POS, только NER." 400 | ] 401 | }, 402 | { 403 | "metadata": { 404 | "id": "MwphVxmdkChy", 405 | "colab_type": "code", 406 | "colab": {} 407 | }, 408 | "cell_type": "code", 409 | "source": [ 410 | "class TokenTaggerModel(nn.Module):\n", 411 | " def __init__(self, vocab_size, tags_count, emb_dim=64, lstm_hidden_dim=128, num_layers=1):\n", 412 | " super().__init__()\n", 413 | "\n", 414 | " \n", 415 | "\n", 416 | " def forward(self, inputs):\n", 417 | " " 418 | ], 419 | "execution_count": 0, 420 | "outputs": [] 421 | }, 422 | { 423 | "metadata": { 424 | "id": "6mzyxM0502wy", 425 | "colab_type": "text" 426 | }, 427 | "cell_type": "markdown", 428 | "source": [ 429 | "**Задание** Обновите `ModelTrainer`: считать нужно всё те же лосс и accuracy, только теперь немного по-другому." 430 | ] 431 | }, 432 | { 433 | "metadata": { 434 | "id": "cMRwby_NnyvJ", 435 | "colab_type": "code", 436 | "colab": {} 437 | }, 438 | "cell_type": "code", 439 | "source": [ 440 | "" 441 | ], 442 | "execution_count": 0, 443 | "outputs": [] 444 | }, 445 | { 446 | "metadata": { 447 | "id": "3QXaapt3nuF_", 448 | "colab_type": "code", 449 | "colab": {} 450 | }, 451 | "cell_type": "code", 452 | "source": [ 453 | "" 454 | ], 455 | "execution_count": 0, 456 | "outputs": [] 457 | }, 458 | { 459 | "metadata": { 460 | "id": "UrkNHhuTRMQv", 461 | "colab_type": "text" 462 | }, 463 | "cell_type": "markdown", 464 | "source": [ 465 | "NER обычно оценивают по F1-скору угадывания слотов. Для этого все перетаскивают скрипт conlleval друг у друга :)\n", 466 | "\n", 467 | "**Задание** Напишите функцию для оценки теггера." 468 | ] 469 | }, 470 | { 471 | "metadata": { 472 | "id": "cKeXWjs7pE35", 473 | "colab_type": "code", 474 | "colab": {} 475 | }, 476 | "cell_type": "code", 477 | "source": [ 478 | "from conlleval import evaluate\n", 479 | "\n", 480 | "def eval_tagger(model, test_iter):\n", 481 | " true_seqs, pred_seqs = [], []\n", 482 | "\n", 483 | " model.eval()\n", 484 | " with torch.no_grad():\n", 485 | " for batch in test_iter:\n", 486 | " \n", 487 | " print('Precision = {:.2f}%, Recall = {:.2f}%, F1 = {:.2f}%'.format(*evaluate(true_seqs, pred_seqs, verbose=False)))" 488 | ], 489 | "execution_count": 0, 490 | "outputs": [] 491 | }, 492 | { 493 | "metadata": { 494 | "id": "APcntGZ0ReXl", 495 | "colab_type": "text" 496 | }, 497 | "cell_type": "markdown", 498 | "source": [ 499 | "## Multi-task learning\n", 500 | "\n", 501 | "Мы уже обсуждали - multi-task learning крут, моден и молодежен. Давайте ~~будем как он~~ реализуем модель, которая умеет сразу и предсказывать теги и интенты. Идея в том, что в этом всем есть общая информация, которая должна помочь как одной, так и другой задаче: зная интент, можно понять, какие слоты вообще могут быть, а зная слоты, можно угадать и интент.\n", 502 | "\n", 503 | "**Задание** Реализуйте объединенную модель." 504 | ] 505 | }, 506 | { 507 | "metadata": { 508 | "id": "goLcDk-Tu0uM", 509 | "colab_type": "code", 510 | "colab": {} 511 | }, 512 | "cell_type": "code", 513 | "source": [ 514 | "class SharedModel(nn.Module):\n", 515 | " def __init__(self, vocab_size, intents_count, tags_count, emb_dim=64, lstm_hidden_dim=128, num_layers=1):\n", 516 | " super().__init__()\n", 517 | "\n", 518 | " \n", 519 | "\n", 520 | " def forward(self, inputs):\n", 521 | " " 522 | ], 523 | "execution_count": 0, 524 | "outputs": [] 525 | }, 526 | { 527 | "metadata": { 528 | "id": "5semraSfv56f", 529 | "colab_type": "code", 530 | "colab": {} 531 | }, 532 | "cell_type": "code", 533 | "source": [ 534 | "" 535 | ], 536 | "execution_count": 0, 537 | "outputs": [] 538 | }, 539 | { 540 | "metadata": { 541 | "id": "-BP4b-4zxU0v", 542 | "colab_type": "code", 543 | "colab": {} 544 | }, 545 | "cell_type": "code", 546 | "source": [ 547 | "" 548 | ], 549 | "execution_count": 0, 550 | "outputs": [] 551 | }, 552 | { 553 | "metadata": { 554 | "id": "DlTbVSOezMD7", 555 | "colab_type": "code", 556 | "colab": {} 557 | }, 558 | "cell_type": "code", 559 | "source": [ 560 | "" 561 | ], 562 | "execution_count": 0, 563 | "outputs": [] 564 | }, 565 | { 566 | "metadata": { 567 | "id": "9-6XO9lsyhJc", 568 | "colab_type": "code", 569 | "colab": {} 570 | }, 571 | "cell_type": "code", 572 | "source": [ 573 | "" 574 | ], 575 | "execution_count": 0, 576 | "outputs": [] 577 | }, 578 | { 579 | "metadata": { 580 | "id": "rAqVyqZ_SXxc", 581 | "colab_type": "text" 582 | }, 583 | "cell_type": "markdown", 584 | "source": [ 585 | " ## Асинхронное обучение\n", 586 | " \n", 587 | " Вообще, всё затевалось именно из-за этого - асинхронное обучение multi-task модели.\n", 588 | " \n", 589 | "Идея описана в статье [A Bi-model based RNN Semantic Frame Parsing Model for Intent Detection and Slot Filling](http://aclweb.org/anthology/N18-2050).\n", 590 | "\n", 591 | "Начнем с такой модели:\n", 592 | "\n", 593 | "![](https://i.ibb.co/N2T1X2f/2018-11-27-2-11-01.png =x400)\n", 594 | "\n", 595 | "Основное отличие от того, что уже реализовали в том, в каком порядке все оптимизируется. Вместо объединенного обучения всех слоев, сети для теггера и для классификатора обучаются отдельно.\n", 596 | "\n", 597 | "На каждом шаге обучения генерируются последовательности скрытых состояний $h^1$ и $h^2$ - для классификатора и для теггера.\n", 598 | "\n", 599 | "Дальше сначала считаются потери от предсказания интента и делается шаг оптимизатора, а затем потери от предсказания теггов - и опять шаг оптимизатора.\n", 600 | "\n", 601 | "**Задание** Реализуйте это." 602 | ] 603 | }, 604 | { 605 | "metadata": { 606 | "id": "cLlVOYfG-dRY", 607 | "colab_type": "code", 608 | "colab": {} 609 | }, 610 | "cell_type": "code", 611 | "source": [ 612 | "class AsyncSharedModel(nn.Module):\n", 613 | " def __init__(self, vocab_size, intents_count, tags_count, emb_dim=64, lstm_hidden_dim=128, num_layers=1):\n", 614 | " super().__init__()\n", 615 | "\n", 616 | " \n", 617 | " \n", 618 | " " 619 | ], 620 | "execution_count": 0, 621 | "outputs": [] 622 | }, 623 | { 624 | "metadata": { 625 | "id": "vEYVn6IXBRiR", 626 | "colab_type": "code", 627 | "colab": {} 628 | }, 629 | "cell_type": "code", 630 | "source": [ 631 | "" 632 | ], 633 | "execution_count": 0, 634 | "outputs": [] 635 | }, 636 | { 637 | "metadata": { 638 | "id": "X_z6u-dE3s8J", 639 | "colab_type": "text" 640 | }, 641 | "cell_type": "markdown", 642 | "source": [ 643 | "Нужно создать отдельные оптимизаторы для каждой части модели.\n", 644 | "\n", 645 | "Отдельные параметры можно получить так:" 646 | ] 647 | }, 648 | { 649 | "metadata": { 650 | "id": "d9ZkkA8q372v", 651 | "colab_type": "code", 652 | "colab": {} 653 | }, 654 | "cell_type": "code", 655 | "source": [ 656 | "model = AsyncSharedModel(\n", 657 | " vocab_size=len(tokens_field.vocab),\n", 658 | " intents_count=len(intent_field.vocab),\n", 659 | " tags_count=len(tags_field.vocab)\n", 660 | ").to(DEVICE)\n", 661 | "\n", 662 | "tags_parameters = [param for name, param in model.named_parameters() if not name.startswith('_intent')]\n", 663 | "intent_parameters = [param for name, param in model.named_parameters() if not name.startswith('_tags')]" 664 | ], 665 | "execution_count": 0, 666 | "outputs": [] 667 | }, 668 | { 669 | "metadata": { 670 | "id": "aMNscGmZ4APl", 671 | "colab_type": "text" 672 | }, 673 | "cell_type": "markdown", 674 | "source": [ 675 | "Затем их нужно передать в отдельные оптимизаторы и учить отдельно.\n", 676 | "\n", 677 | "*Еще, может быть, пригодится retain_graph параметр метода backward()*." 678 | ] 679 | }, 680 | { 681 | "metadata": { 682 | "id": "zmMt811LBUXb", 683 | "colab_type": "code", 684 | "colab": {} 685 | }, 686 | "cell_type": "code", 687 | "source": [ 688 | "" 689 | ], 690 | "execution_count": 0, 691 | "outputs": [] 692 | }, 693 | { 694 | "metadata": { 695 | "id": "KnMw5L4iG5Io", 696 | "colab_type": "code", 697 | "colab": {} 698 | }, 699 | "cell_type": "code", 700 | "source": [ 701 | "" 702 | ], 703 | "execution_count": 0, 704 | "outputs": [] 705 | }, 706 | { 707 | "metadata": { 708 | "id": "NE-5oyMUU40L", 709 | "colab_type": "text" 710 | }, 711 | "cell_type": "markdown", 712 | "source": [ 713 | "## Улучшения\n", 714 | "\n", 715 | "**Задание** Посмотрите на параметры в статье и попробуйте добиться похожего качества.\n", 716 | "\n", 717 | "**Задание** Попробуйте заменить корпус, с которым работаете.\n", 718 | "\n", 719 | "### Encoder-decoder\n", 720 | "\n", 721 | "Хорошая идея - использовать не просто независимые предсказания тегов, а декодер над ними:\n", 722 | "\n", 723 | "![](https://i.ibb.co/qrgVSqF/2018-11-27-2-11-17.png =600x)\n", 724 | "\n", 725 | "По сути тут добавляется просто еще слой RNN - на этот раз однонаправленной. При этом его вход в случае предсказания тегов - это предыдущий тег, предыдущее скрытое состояние и скрытые состояния из энкодеров теггов и интента. Для интента - простая RNN.\n", 726 | "\n", 727 | "**Задание** Реализуйте такую модель." 728 | ] 729 | }, 730 | { 731 | "metadata": { 732 | "id": "NujuoWDU195f", 733 | "colab_type": "text" 734 | }, 735 | "cell_type": "markdown", 736 | "source": [ 737 | "# Async Multi-task Learning for POS Tagging" 738 | ] 739 | }, 740 | { 741 | "metadata": { 742 | "id": "L9Q0ip3p2a5v", 743 | "colab_type": "text" 744 | }, 745 | "cell_type": "markdown", 746 | "source": [ 747 | "Это были игрушечные датасеты и не самые хорошие статьи (хоть и с NAACL-2018).\n", 748 | "\n", 749 | "Мне больше нравится вот эта: [Morphosyntactic Tagging with a Meta-BiLSTM Model over Context Sensitive Token Encodings](https://arxiv.org/pdf/1805.08237.pdf). Гораздо больше.\n", 750 | "\n", 751 | "Архитектура там такая:\n", 752 | "\n", 753 | "![](https://i.ibb.co/0nSX6CC/2018-11-27-9-26-15.png =x400)\n", 754 | "\n", 755 | "Multi-task задача - обучение отдельных классификаторов более низкого уровня (над символами и словами) для предсказания тегов отдельными оптимизаторами.\n", 756 | "\n", 757 | "**Задание** Попробовать реализовать, о чем в статье пишется." 758 | ] 759 | }, 760 | { 761 | "metadata": { 762 | "id": "n2x9-j4oz08p", 763 | "colab_type": "text" 764 | }, 765 | "cell_type": "markdown", 766 | "source": [ 767 | "# Дополнительные материалы\n", 768 | "\n", 769 | "## Статьи\n", 770 | "A Bi-model based RNN Semantic Frame Parsing Model for Intent Detection and Slot Filling, 2018 [[pdf]](http://aclweb.org/anthology/N18-2050) \n", 771 | "Slot-Gated Modeling for Joint Slot Filling and Intent Prediction, 2018 [[pdf]](http://aclweb.org/anthology/N18-2118) \n", 772 | "Morphosyntactic Tagging with a Meta-BiLSTM Model over Context Sensitive Token Encodings, 2018 [[arxiv]](https://arxiv.org/pdf/1805.08237.pdf)\n", 773 | "\n", 774 | "## Блоги\n", 775 | "[Как устроена Алиса](https://habr.com/company/yandex/blog/349372/) " 776 | ] 777 | }, 778 | { 779 | "metadata": { 780 | "id": "gjkS1Kpkz4Aa", 781 | "colab_type": "text" 782 | }, 783 | "cell_type": "markdown", 784 | "source": [ 785 | "# Сдача\n", 786 | "\n", 787 | "[Форма для сдачи](https://goo.gl/forms/4W7JDuSg3A32Ple72) \n", 788 | "[Feedback](https://goo.gl/forms/9aizSzOUrx7EvGlG3)" 789 | ] 790 | } 791 | ] 792 | } -------------------------------------------------------------------------------- /Week 08/Week_08_Language_Models_(Part_2).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "Week 08 - Language Models (Part 2).ipynb", 7 | "version": "0.3.2", 8 | "provenance": [], 9 | "collapsed_sections": [] 10 | }, 11 | "kernelspec": { 12 | "name": "python3", 13 | "display_name": "Python 3" 14 | }, 15 | "accelerator": "GPU" 16 | }, 17 | "cells": [ 18 | { 19 | "metadata": { 20 | "colab_type": "code", 21 | "cellView": "both", 22 | "id": "adgAwRaDI5p3", 23 | "colab": {} 24 | }, 25 | "cell_type": "code", 26 | "source": [ 27 | "!pip3 -qq install torch==0.4.1\n", 28 | "!pip -qq install torchtext==0.3.1\n", 29 | "!pip -qq install gensim==3.6.0\n", 30 | "!pip -qq install pyldavis==2.1.2\n", 31 | "!pip -qq install attrs==18.2.0\n", 32 | "!wget -qq --no-check-certificate 'https://drive.google.com/uc?export=download&id=1OIU9ICMebvZXJ0Grc2SLlMep3x9EkZtz' -O perashki.txt\n", 33 | "!wget -qq --no-check-certificate 'https://drive.google.com/uc?export=download&id=1v66uAEKL3KunyylYitNKggdl2gCeYgZZ' -O poroshki.txt\n", 34 | "!git clone https://github.com/UniversalDependencies/UD_Russian-SynTagRus.git\n", 35 | "!wget -qq https://raw.githubusercontent.com/DanAnastasyev/neuromorphy/master/neuromorphy/train/corpus_iterator.py" 36 | ], 37 | "execution_count": 0, 38 | "outputs": [] 39 | }, 40 | { 41 | "metadata": { 42 | "id": "uhvfH55PUJ8K", 43 | "colab_type": "code", 44 | "cellView": "both", 45 | "colab": {} 46 | }, 47 | "cell_type": "code", 48 | "source": [ 49 | "import numpy as np\n", 50 | "\n", 51 | "import torch\n", 52 | "import torch.nn as nn\n", 53 | "import torch.nn.functional as F\n", 54 | "import torch.optim as optim\n", 55 | "\n", 56 | "\n", 57 | "if torch.cuda.is_available():\n", 58 | " from torch.cuda import FloatTensor, LongTensor\n", 59 | " DEVICE = torch.device('cuda')\n", 60 | "else:\n", 61 | " from torch import FloatTensor, LongTensor\n", 62 | " DEVICE = torch.device('cpu')\n", 63 | "\n", 64 | "np.random.seed(42)" 65 | ], 66 | "execution_count": 0, 67 | "outputs": [] 68 | }, 69 | { 70 | "metadata": { 71 | "id": "txWqIO_74A4s", 72 | "colab_type": "text" 73 | }, 74 | "cell_type": "markdown", 75 | "source": [ 76 | "# Word-Level Text Generation" 77 | ] 78 | }, 79 | { 80 | "metadata": { 81 | "id": "KOD_3I7d4oDV", 82 | "colab_type": "text" 83 | }, 84 | "cell_type": "markdown", 85 | "source": [ 86 | "Сегодня занимаемся, в основном, тем, что генерируем *пирожки* и *порошки*.\n", 87 | "\n", 88 | "*(Данные без спросу скачаны с сайта http://poetory.ru)*\n", 89 | "\n", 90 | "Пирожки - это вот:" 91 | ] 92 | }, 93 | { 94 | "metadata": { 95 | "id": "d2vMrlrRQpuJ", 96 | "colab_type": "code", 97 | "colab": {} 98 | }, 99 | "cell_type": "code", 100 | "source": [ 101 | "!head perashki.txt" 102 | ], 103 | "execution_count": 0, 104 | "outputs": [] 105 | }, 106 | { 107 | "metadata": { 108 | "id": "0Lm0-PeG5Dh9", 109 | "colab_type": "text" 110 | }, 111 | "cell_type": "markdown", 112 | "source": [ 113 | "Порошки вот:" 114 | ] 115 | }, 116 | { 117 | "metadata": { 118 | "id": "2-Jf88bxVTGj", 119 | "colab_type": "code", 120 | "colab": {} 121 | }, 122 | "cell_type": "code", 123 | "source": [ 124 | "!head poroshki.txt" 125 | ], 126 | "execution_count": 0, 127 | "outputs": [] 128 | }, 129 | { 130 | "metadata": { 131 | "id": "AgYh4FNP5FyX", 132 | "colab_type": "text" 133 | }, 134 | "cell_type": "markdown", 135 | "source": [ 136 | "Не перепутайте!\n", 137 | "\n", 138 | "Вообще, пирожок - это четверостишие, написанное четырехстопным ямбом по схеме 9-8-9-8. У порошка схема 9-8-9-2." 139 | ] 140 | }, 141 | { 142 | "metadata": { 143 | "id": "bSBpLFRgGaXS", 144 | "colab_type": "code", 145 | "colab": {} 146 | }, 147 | "cell_type": "code", 148 | "source": [ 149 | "vowels = 'ёуеыаоэяию'\n", 150 | "\n", 151 | "odd_pattern = '-+-+-+-+-'\n", 152 | "even_pattern = '-+-+-+-+'" 153 | ], 154 | "execution_count": 0, 155 | "outputs": [] 156 | }, 157 | { 158 | "metadata": { 159 | "id": "Hl9BFoug519c", 160 | "colab_type": "text" 161 | }, 162 | "cell_type": "markdown", 163 | "source": [ 164 | "Считываем данные:" 165 | ] 166 | }, 167 | { 168 | "metadata": { 169 | "id": "O3aFzzOQKLlD", 170 | "colab_type": "code", 171 | "colab": {} 172 | }, 173 | "cell_type": "code", 174 | "source": [ 175 | "def read_poem(path):\n", 176 | " poem = []\n", 177 | " with open(path, encoding='utf8') as f:\n", 178 | " for line in f:\n", 179 | " line = line.rstrip()\n", 180 | " if len(line) == 0:\n", 181 | " yield poem\n", 182 | " poem = []\n", 183 | " continue\n", 184 | " \n", 185 | " poem.extend(line.split() + ['\\\\n'])\n", 186 | " \n", 187 | "perashki = list(read_poem('perashki.txt'))\n", 188 | "poroshki = list(read_poem('poroshki.txt'))" 189 | ], 190 | "execution_count": 0, 191 | "outputs": [] 192 | }, 193 | { 194 | "metadata": { 195 | "id": "xiRq1vbf55qN", 196 | "colab_type": "text" 197 | }, 198 | "cell_type": "markdown", 199 | "source": [ 200 | "Построим датасет для порошков:" 201 | ] 202 | }, 203 | { 204 | "metadata": { 205 | "id": "ZOBgLAgVTrk1", 206 | "colab_type": "code", 207 | "colab": {} 208 | }, 209 | "cell_type": "code", 210 | "source": [ 211 | "from torchtext.data import Field, Example, Dataset, BucketIterator\n", 212 | "\n", 213 | "text_field = Field(init_token='', eos_token='')\n", 214 | " \n", 215 | "fields = [('text', text_field)]\n", 216 | "examples = [Example.fromlist([poem], fields) for poem in poroshki]\n", 217 | "dataset = Dataset(examples, fields)\n", 218 | "\n", 219 | "text_field.build_vocab(dataset, min_freq=7)\n", 220 | "\n", 221 | "print('Vocab size =', len(text_field.vocab))\n", 222 | "train_dataset, test_dataset = dataset.split(split_ratio=0.9)\n", 223 | "\n", 224 | "train_iter, test_iter = BucketIterator.splits(datasets=(train_dataset, test_dataset), batch_sizes=(32, 128), \n", 225 | " shuffle=True, device=DEVICE, sort=False)" 226 | ], 227 | "execution_count": 0, 228 | "outputs": [] 229 | }, 230 | { 231 | "metadata": { 232 | "id": "8FYJe2CA8GcY", 233 | "colab_type": "text" 234 | }, 235 | "cell_type": "markdown", 236 | "source": [ 237 | "**Задание** Напишите класс языковой модели." 238 | ] 239 | }, 240 | { 241 | "metadata": { 242 | "id": "x8ndCRZLl4ZZ", 243 | "colab_type": "code", 244 | "colab": {} 245 | }, 246 | "cell_type": "code", 247 | "source": [ 248 | "class LMModel(nn.Module):\n", 249 | " def __init__(self, vocab_size, emb_dim=256, lstm_hidden_dim=256, num_layers=1):\n", 250 | " super().__init__()\n", 251 | "\n", 252 | " self._emb = nn.Embedding(vocab_size, emb_dim)\n", 253 | " self._rnn = nn.LSTM(input_size=emb_dim, hidden_size=lstm_hidden_dim)\n", 254 | " \n", 255 | " self._out_layer = nn.Linear(lstm_hidden_dim, vocab_size)\n", 256 | " \n", 257 | " self._init_weights()\n", 258 | "\n", 259 | " def _init_weights(self, init_range=0.1):\n", 260 | " self._emb.weight.data.uniform_(-init_range, init_range)\n", 261 | " self._out_layer.bias.data.zero_()\n", 262 | " self._out_layer.weight.data.uniform_(-init_range, init_range)\n", 263 | "\n", 264 | " def forward(self, inputs, hidden=None):\n", 265 | " " 266 | ], 267 | "execution_count": 0, 268 | "outputs": [] 269 | }, 270 | { 271 | "metadata": { 272 | "id": "ySJ4tUAqvFvB", 273 | "colab_type": "code", 274 | "colab": {} 275 | }, 276 | "cell_type": "code", 277 | "source": [ 278 | "batch = next(iter(train_iter))" 279 | ], 280 | "execution_count": 0, 281 | "outputs": [] 282 | }, 283 | { 284 | "metadata": { 285 | "id": "5_qVuSL8QJg4", 286 | "colab_type": "code", 287 | "colab": {} 288 | }, 289 | "cell_type": "code", 290 | "source": [ 291 | "model = LMModel(vocab_size=len(train_iter.dataset.fields['text'].vocab)).to(DEVICE)\n", 292 | "\n", 293 | "model(batch.text)" 294 | ], 295 | "execution_count": 0, 296 | "outputs": [] 297 | }, 298 | { 299 | "metadata": { 300 | "id": "Rsh3_eR08PqQ", 301 | "colab_type": "text" 302 | }, 303 | "cell_type": "markdown", 304 | "source": [ 305 | "**Задание** Добавьте подсчет потерей с маскингом паддингов." 306 | ] 307 | }, 308 | { 309 | "metadata": { 310 | "id": "_E2JxfRuphch", 311 | "colab_type": "code", 312 | "colab": {} 313 | }, 314 | "cell_type": "code", 315 | "source": [ 316 | "import math\n", 317 | "from tqdm import tqdm\n", 318 | "tqdm.get_lock().locks = []\n", 319 | "\n", 320 | "\n", 321 | "def do_epoch(model, criterion, data_iter, unk_idx, pad_idx, optimizer=None, name=None):\n", 322 | " epoch_loss = 0\n", 323 | " \n", 324 | " is_train = not optimizer is None\n", 325 | " name = name or ''\n", 326 | " model.train(is_train)\n", 327 | " \n", 328 | " batches_count = len(data_iter)\n", 329 | " \n", 330 | " with torch.autograd.set_grad_enabled(is_train):\n", 331 | " with tqdm(total=batches_count) as progress_bar:\n", 332 | " for i, batch in enumerate(data_iter): \n", 333 | " logits, _ = model(batch.text)\n", 334 | "\n", 335 | " \n", 336 | "\n", 337 | " epoch_loss += loss.item()\n", 338 | "\n", 339 | " if optimizer:\n", 340 | " optimizer.zero_grad()\n", 341 | " loss.backward()\n", 342 | " nn.utils.clip_grad_norm_(model.parameters(), 1.)\n", 343 | " optimizer.step()\n", 344 | "\n", 345 | " progress_bar.update()\n", 346 | " progress_bar.set_description('{:>5s} Loss = {:.5f}, PPX = {:.2f}'.format(name, loss.item(), \n", 347 | " math.exp(loss.item())))\n", 348 | " \n", 349 | " progress_bar.set_description('{:>5s} Loss = {:.5f}, PPX = {:.2f}'.format(\n", 350 | " name, epoch_loss / batches_count, math.exp(epoch_loss / batches_count))\n", 351 | " )\n", 352 | " progress_bar.refresh()\n", 353 | "\n", 354 | " return epoch_loss / batches_count\n", 355 | "\n", 356 | "\n", 357 | "def fit(model, criterion, optimizer, train_iter, epochs_count=1, unk_idx=0, pad_idx=1, val_iter=None):\n", 358 | " best_val_loss = None\n", 359 | " for epoch in range(epochs_count):\n", 360 | " name_prefix = '[{} / {}] '.format(epoch + 1, epochs_count)\n", 361 | " train_loss = do_epoch(model, criterion, train_iter, unk_idx, pad_idx, optimizer, name_prefix + 'Train:')\n", 362 | " \n", 363 | " if not val_iter is None:\n", 364 | " val_loss = do_epoch(model, criterion, val_iter, unk_idx, pad_idx, None, name_prefix + ' Val:')\n", 365 | " \n", 366 | " if best_val_loss and val_loss > best_val_loss:\n", 367 | " optimizer.param_groups[0]['lr'] /= 4.\n", 368 | " print('Optimizer lr = {:g}'.format(optimizer.param_groups[0]['lr']))\n", 369 | " else:\n", 370 | " best_val_loss = val_loss\n", 371 | " print()\n", 372 | " generate(model)\n", 373 | " print()" 374 | ], 375 | "execution_count": 0, 376 | "outputs": [] 377 | }, 378 | { 379 | "metadata": { 380 | "id": "ufpoSwQ-8bcN", 381 | "colab_type": "text" 382 | }, 383 | "cell_type": "markdown", 384 | "source": [ 385 | "**Задание** Напишите функцию-генератор для модели." 386 | ] 387 | }, 388 | { 389 | "metadata": { 390 | "colab_type": "code", 391 | "id": "BYoHY1se2bhB", 392 | "colab": {} 393 | }, 394 | "cell_type": "code", 395 | "source": [ 396 | "def sample(probs, temp):\n", 397 | " probs = F.log_softmax(probs.squeeze(), dim=0)\n", 398 | " probs = (probs / temp).exp()\n", 399 | " probs /= probs.sum()\n", 400 | " probs = probs.cpu().numpy()\n", 401 | "\n", 402 | " return np.random.choice(np.arange(len(probs)), p=probs)\n", 403 | "\n", 404 | "\n", 405 | "def generate(model, temp=0.6):\n", 406 | " model.eval()\n", 407 | " with torch.no_grad(): \n", 408 | " prev_token = train_iter.dataset.fields['text'].vocab.stoi['']\n", 409 | " end_token = train_iter.dataset.fields['text'].vocab.stoi['']\n", 410 | " \n", 411 | " hidden = None\n", 412 | " for _ in range(150):\n", 413 | " \n", 414 | " \n", 415 | "generate(model)" 416 | ], 417 | "execution_count": 0, 418 | "outputs": [] 419 | }, 420 | { 421 | "metadata": { 422 | "id": "5X2kYDU_rCjP", 423 | "colab_type": "code", 424 | "colab": {} 425 | }, 426 | "cell_type": "code", 427 | "source": [ 428 | "model = LMModel(vocab_size=len(train_iter.dataset.fields['text'].vocab)).to(DEVICE)\n", 429 | "\n", 430 | "pad_idx = train_iter.dataset.fields['text'].vocab.stoi['']\n", 431 | "unk_idx = train_iter.dataset.fields['text'].vocab.stoi['']\n", 432 | "criterion = nn.CrossEntropyLoss(...).to(DEVICE)\n", 433 | "\n", 434 | "optimizer = optim.SGD(model.parameters(), lr=20., weight_decay=1e-6)\n", 435 | "\n", 436 | "fit(model, criterion, optimizer, train_iter, epochs_count=300, unk_idx=unk_idx, pad_idx=pad_idx, val_iter=test_iter)" 437 | ], 438 | "execution_count": 0, 439 | "outputs": [] 440 | }, 441 | { 442 | "metadata": { 443 | "id": "r_YtM4ms8v--", 444 | "colab_type": "text" 445 | }, 446 | "cell_type": "markdown", 447 | "source": [ 448 | "**Задание** Добавьте маскинг `` токенов при тренировке модели." 449 | ] 450 | }, 451 | { 452 | "metadata": { 453 | "id": "LzGwmgVf9Dkg", 454 | "colab_type": "text" 455 | }, 456 | "cell_type": "markdown", 457 | "source": [ 458 | "## Улучшаем модель" 459 | ] 460 | }, 461 | { 462 | "metadata": { 463 | "id": "BHneb8br9WXh", 464 | "colab_type": "text" 465 | }, 466 | "cell_type": "markdown", 467 | "source": [ 468 | "### Tying input and output embeddings\n", 469 | "\n", 470 | "В модели есть два эмбеддинга - входной и выходной. Красивая и полезная в жизни идея - учить только одну матрицу, расшаренную между ними: [Using the Output Embedding to Improve Language Models](http://www.aclweb.org/anthology/E17-2025).\n", 471 | "\n", 472 | "От идеи одни плюсы: получается намного меньше обучаемых параметров и при этом достаточно заметно более высокое качество.\n", 473 | "\n", 474 | "**Задание** Реализуйте это. Достаточно написать что-то типа этого в конструкторе:\n", 475 | "\n", 476 | "`self._out_layer.weight = self._emb.weight`" 477 | ] 478 | }, 479 | { 480 | "metadata": { 481 | "id": "N8I3QC4a_a8q", 482 | "colab_type": "text" 483 | }, 484 | "cell_type": "markdown", 485 | "source": [ 486 | "### Добавление информации в выборку\n", 487 | "\n", 488 | "Сейчас у нас каждое слово предствляется одним индексом. Модели очень сложно узнать, сколько в нем слогов - а значит, сложно генерировать корректное стихотворение.\n", 489 | "\n", 490 | "На самом деле к каждому слову можно приписать кусочек из метрического шаблона:\n", 491 | "\n", 492 | "![](https://hsto.org/web/59a/b39/bd0/59ab39bd020c49a78a12cbab62c80181.png =x200)\n", 493 | "\n", 494 | "**Задание** Обновите функцию `read_poem`, пусть она генерирует два списка - список слов и список кусков шаблона. \n", 495 | "Добавьте в модель вход - последовательности шаблонов, конкатенируйте их эмбеддинги со словами. \n", 496 | "Дополнительная идея - заставьте модель угадывать, какой шаблон должен идти следующим (где-то половина будет подходящими, остальные - нет). Добавьте дополнительные потери от угадывания шаблона." 497 | ] 498 | }, 499 | { 500 | "metadata": { 501 | "id": "MBX4NjzZ-0Hc", 502 | "colab_type": "text" 503 | }, 504 | "cell_type": "markdown", 505 | "source": [ 506 | "### Увеличиваем выборку\n", 507 | "\n", 508 | "У нас есть выборка для пирожков, которая заметно больше.\n", 509 | "\n", 510 | "**Задание** Обучитесь на ней.\n", 511 | "\n", 512 | "### Transfer learning\n", 513 | "\n", 514 | "Простой и приятный способ улучшения модели - сделать перенос обученной на большом корпусе модели на меньшего объема датасет.\n", 515 | "\n", 516 | "Популярен этот способ больше в компьютерном зрении: [Transfer learning, cs231n](http://cs231n.github.io/transfer-learning/) - там есть огромный ImageNet, на котором предобучают модель, чтобы потом заморозить нижние слои и заменить выходные. В итоге модель использует универсальные представления данных, выученные на большом корпусе, но для предсказания совсем других меток - и качество очень здорово растет.\n", 517 | "\n", 518 | "Нам такие извращения пока не нужны (хотя потом пригодятся, ключевые слова: ULMFiT, ELMo и компания). Просто возьмем обученную на большем корпусе модель и поучим ее на меньшем корпусе. Ей всего-то нужно новый матрический шаблон последней строки выучить.\n", 519 | "\n", 520 | "**Задание** Обученную в прошлом пункте модель дообучите на порошки.\n", 521 | "\n", 522 | "### Conditional language model\n", 523 | "\n", 524 | "Ещё лучше - просто учиться на обоих корпусах сразу. Объедините пирожки и порошки, для каждого храните индекс 0/1 - был ли это пирожок или порошок. Добавьте вход - этот индекс и конкатенируйте его либо к каждому эмбеддингу слов, либо к каждому выходу из LSTM.\n", 525 | "\n", 526 | "**Задание** Научите единую модель, у которой можно просить сгенерировать пирожок или порошок." 527 | ] 528 | }, 529 | { 530 | "metadata": { 531 | "id": "WnP743CM-bY6", 532 | "colab_type": "text" 533 | }, 534 | "cell_type": "markdown", 535 | "source": [ 536 | "### Variational & word dropout\n", 537 | "\n", 538 | "**Задание** На прошлом занятии приводились примеры более приспособленных к RNN'ам dropout'ов. Добавьте их.\n", 539 | "\n", 540 | "**Задание** Кроме этого, попробуйте увеличивать размер модели или количество слоев в ней, чтобы улучшить качество." 541 | ] 542 | }, 543 | { 544 | "metadata": { 545 | "id": "Ejqx6BC0JcG2", 546 | "colab_type": "text" 547 | }, 548 | "cell_type": "markdown", 549 | "source": [ 550 | "## Multi-task learning\n", 551 | "\n", 552 | "Ещё один важный способ улучшения модели - multi-task learning. Это когда одна модель учится делать предсказания сразу для нескольких задач.\n", 553 | "\n", 554 | "В нашем случае это может быть предсказанием отдельно леммы слова и отдельно - его грамматического значения:\n", 555 | "![](https://hsto.org/web/e97/8a8/6e8/e978a86e8a874d8d946bb15e6a49a713.png =x350)\n", 556 | "\n", 557 | "В итоге модель выучивает как языковую модель по леммам, так и модель POS tagging'а. Одновременно!\n", 558 | "\n", 559 | "Возьмем корпус из universal dependencies - он уже размечен, как нужно.\n", 560 | "\n", 561 | "Почитаем его:" 562 | ] 563 | }, 564 | { 565 | "metadata": { 566 | "id": "YT-kzC2_KuLX", 567 | "colab_type": "code", 568 | "colab": {} 569 | }, 570 | "cell_type": "code", 571 | "source": [ 572 | "from corpus_iterator import Token, CorpusIterator\n", 573 | "\n", 574 | "fields = [('word', Field()), ('lemma', Field()), ('gram_val', Field())]\n", 575 | "examples = []\n", 576 | "\n", 577 | "with CorpusIterator('UD_Russian-SynTagRus/ru_syntagrus-ud-train.conllu') as corpus_iter:\n", 578 | " for sent in corpus_iter:\n", 579 | " words = [''] + [tok.token.lower() for tok in sent] + ['']\n", 580 | " lemmas = [''] + [tok.lemma.lower() for tok in sent] + ['']\n", 581 | " gr_vals = [''] + [tok.grammar_value for tok in sent] + ['']\n", 582 | " examples.append(Example.fromlist([words, lemmas, gr_vals], fields))" 583 | ], 584 | "execution_count": 0, 585 | "outputs": [] 586 | }, 587 | { 588 | "metadata": { 589 | "id": "l_3xaD-2KwNW", 590 | "colab_type": "code", 591 | "colab": {} 592 | }, 593 | "cell_type": "code", 594 | "source": [ 595 | "print('Words:', examples[1].word)\n", 596 | "print('Lemmas:', examples[1].lemma)\n", 597 | "print('Grammar vals:', examples[1].gram_val)" 598 | ], 599 | "execution_count": 0, 600 | "outputs": [] 601 | }, 602 | { 603 | "metadata": { 604 | "id": "HcGm5fPsLESH", 605 | "colab_type": "text" 606 | }, 607 | "cell_type": "markdown", 608 | "source": [ 609 | "Таким образом, размер словаря может быть существенно сокращен - лемм меньше, чем слов, а предсказание грамматики вынуждает модель быть более осведомленной о согласовании слов." 610 | ] 611 | }, 612 | { 613 | "metadata": { 614 | "id": "xZe5HimdLb9i", 615 | "colab_type": "code", 616 | "colab": {} 617 | }, 618 | "cell_type": "code", 619 | "source": [ 620 | "dataset = Dataset(examples, fields)\n", 621 | "\n", 622 | "dataset.fields['word'].build_vocab(dataset, min_freq=3)\n", 623 | "print('Word vocab size =', len(dataset.fields['word'].vocab))\n", 624 | "dataset.fields['lemma'].build_vocab(dataset, min_freq=3)\n", 625 | "print('Lemma vocab size =', len(dataset.fields['lemma'].vocab))\n", 626 | "dataset.fields['gram_val'].build_vocab(dataset)\n", 627 | "print('Grammar val vocab size =', len(dataset.fields['gram_val'].vocab))\n", 628 | "\n", 629 | "train_dataset, test_dataset = dataset.split(split_ratio=0.75)\n", 630 | "\n", 631 | "train_iter, test_iter = BucketIterator.splits(datasets=(train_dataset, test_dataset), batch_sizes=(32, 128), \n", 632 | " shuffle=True, device=DEVICE, sort=False)" 633 | ], 634 | "execution_count": 0, 635 | "outputs": [] 636 | }, 637 | { 638 | "metadata": { 639 | "id": "Y7xlr15lLm78", 640 | "colab_type": "text" 641 | }, 642 | "cell_type": "markdown", 643 | "source": [ 644 | "Построим маппинг из пары (лемма, грамматическое значение) в слово - если бы у нас под рукой был морфологический словарь, маппинг можно было бы пополнить, добавить слова для лемм из корпуса, которые не встретились в обучении." 645 | ] 646 | }, 647 | { 648 | "metadata": { 649 | "id": "_AvT2MgeLmP8", 650 | "colab_type": "code", 651 | "colab": {} 652 | }, 653 | "cell_type": "code", 654 | "source": [ 655 | "dictionary = {\n", 656 | " (lemma, gr_val): word\n", 657 | " for example in train_iter.dataset.examples \n", 658 | " for word, lemma, gr_val in zip(example.word, example.lemma, example.gram_val)\n", 659 | "}" 660 | ], 661 | "execution_count": 0, 662 | "outputs": [] 663 | }, 664 | { 665 | "metadata": { 666 | "id": "VaP8Krx1LeJl", 667 | "colab_type": "text" 668 | }, 669 | "cell_type": "markdown", 670 | "source": [ 671 | "**Задание** Обновите генератор - например, можно сэмплировать лемму и находить самое вероятное грамматическое значение, которое встречается в паре с этой леммой в `dictionary`." 672 | ] 673 | }, 674 | { 675 | "metadata": { 676 | "id": "PeBH0WYjMQ5h", 677 | "colab_type": "code", 678 | "colab": {} 679 | }, 680 | "cell_type": "code", 681 | "source": [ 682 | "def generate(model, temp=0.7):\n", 683 | " ..." 684 | ], 685 | "execution_count": 0, 686 | "outputs": [] 687 | }, 688 | { 689 | "metadata": { 690 | "id": "w3GzOZ8dMVMJ", 691 | "colab_type": "text" 692 | }, 693 | "cell_type": "markdown", 694 | "source": [ 695 | "**Задание** Обновите модель и функцию обучения.\n", 696 | "\n", 697 | "Модель должна принимать пары `lemma, gr_val`, конкатенировать их эмбеддинги и предсказывать следующие `lemma, gr_val` по выходу из LSTM.\n", 698 | "\n", 699 | "Функция `do_epoch` должна суммировать потери по предсказанию леммы (делая маскинг для `` и ``) + потери по предсказанию грамматического значения (с маскингом по ``)." 700 | ] 701 | }, 702 | { 703 | "metadata": { 704 | "id": "vL2xPe-BNRhu", 705 | "colab_type": "text" 706 | }, 707 | "cell_type": "markdown", 708 | "source": [ 709 | "## Контролируемая генерация\n", 710 | "\n", 711 | "Хочется сделать генерацию более контролируемой - в идеале, задавать тему.\n", 712 | "\n", 713 | "Простой способ - сделать тематическое моделирование и найти в текстах какие-то темы - а потом передавать вектор тем вместе с эмбеддингом слова, чтобы модель училась генерировать тематически-согласованный текст." 714 | ] 715 | }, 716 | { 717 | "metadata": { 718 | "id": "exopH1jlN4fc", 719 | "colab_type": "code", 720 | "colab": {} 721 | }, 722 | "cell_type": "code", 723 | "source": [ 724 | "from gensim import corpora, models\n", 725 | "\n", 726 | "docs = [[word for word in poem if word != '\\\\n'] for poem in perashki]\n", 727 | "\n", 728 | "dictionary = corpora.Dictionary(docs)\n", 729 | "dictionary.filter_n_most_frequent(100)\n", 730 | "\n", 731 | "bow_corpus = [dictionary.doc2bow(doc) for doc in docs]\n", 732 | "\n", 733 | "lda_model = models.LdaModel(bow_corpus, num_topics=5, id2word=dictionary, passes=5)" 734 | ], 735 | "execution_count": 0, 736 | "outputs": [] 737 | }, 738 | { 739 | "metadata": { 740 | "id": "LLPO9U1-Pakp", 741 | "colab_type": "text" 742 | }, 743 | "cell_type": "markdown", 744 | "source": [ 745 | "Посмотреть, что выучилось, можно так:" 746 | ] 747 | }, 748 | { 749 | "metadata": { 750 | "id": "hzEg-8SZs8t7", 751 | "colab_type": "code", 752 | "colab": {} 753 | }, 754 | "cell_type": "code", 755 | "source": [ 756 | "import pyLDAvis\n", 757 | "import pyLDAvis.gensim\n", 758 | "\n", 759 | "pyLDAvis.enable_notebook()\n", 760 | "pyLDAvis.gensim.prepare(lda_model, bow_corpus, dictionary)" 761 | ], 762 | "execution_count": 0, 763 | "outputs": [] 764 | }, 765 | { 766 | "metadata": { 767 | "id": "mHx1GJrWPkM8", 768 | "colab_type": "text" 769 | }, 770 | "cell_type": "markdown", 771 | "source": [ 772 | "Предсказывает распределение модель как-то так:" 773 | ] 774 | }, 775 | { 776 | "metadata": { 777 | "id": "rTD0CGMdPsF5", 778 | "colab_type": "code", 779 | "colab": {} 780 | }, 781 | "cell_type": "code", 782 | "source": [ 783 | "for word in perashki[10]:\n", 784 | " if word == '\\\\n':\n", 785 | " print()\n", 786 | " else:\n", 787 | " print(word, end=' ')" 788 | ], 789 | "execution_count": 0, 790 | "outputs": [] 791 | }, 792 | { 793 | "metadata": { 794 | "id": "0m0b6i2MPlKD", 795 | "colab_type": "code", 796 | "colab": {} 797 | }, 798 | "cell_type": "code", 799 | "source": [ 800 | "lda_model.get_document_topics(bow_corpus[10])" 801 | ], 802 | "execution_count": 0, 803 | "outputs": [] 804 | }, 805 | { 806 | "metadata": { 807 | "id": "-imKaGGUQM5K", 808 | "colab_type": "text" 809 | }, 810 | "cell_type": "markdown", 811 | "source": [ 812 | "**Задание** Посчитайте для всех текстов вектора тем, передавайте их вместе со словами (конкатенируя к эмбеддингам). Посмотрите, вдруг чего получится." 813 | ] 814 | }, 815 | { 816 | "metadata": { 817 | "id": "w8V0KAz_CNf0", 818 | "colab_type": "text" 819 | }, 820 | "cell_type": "markdown", 821 | "source": [ 822 | "# Дополнительные материалы\n", 823 | "\n", 824 | "## Статьи\n", 825 | "\n", 826 | "Regularizing and Optimizing LSTM Language Models, 2017 [[arxiv]](https://arxiv.org/abs/1708.02182), [[github]](https://github.com/salesforce/awd-lstm-lm) - одна из самых полезных статей про языковые модели + репозиторий, в котором реализовано много полезного, стоит заглянуть\n", 827 | "\n", 828 | "Exploring the Limits of Language Modeling, 2016 [[arxiv]](https://arxiv.org/abs/1602.02410)\n", 829 | "\n", 830 | "Using the Output Embedding to Improve Language Models, 2017 [[pdf]](http://www.aclweb.org/anthology/E17-2025)\n", 831 | "\n", 832 | "\n", 833 | "## Transfer learning\n", 834 | "[Transfer learning, cs231n](http://cs231n.github.io/transfer-learning/) \n", 835 | "[Transfer learning, Ruder](http://ruder.io/transfer-learning/) - очень подробная статья от чувака из NLP\n", 836 | "\n", 837 | "## Multi-task learning\n", 838 | "[An Overview of Multi-Task Learning in Deep Neural Networks, Ruder](http://ruder.io/multi-task/) \n", 839 | "[Multi-Task Learning Objectives for Natural Language Processing, Ruder](http://ruder.io/multi-task-learning-nlp/)" 840 | ] 841 | }, 842 | { 843 | "metadata": { 844 | "id": "Vwb5e5hPQebd", 845 | "colab_type": "text" 846 | }, 847 | "cell_type": "markdown", 848 | "source": [ 849 | "# Сдача\n", 850 | "\n", 851 | "[Форма для сдачи](https://goo.gl/forms/ASLLjYncKUcIHmuO2) \n", 852 | "[Feedback](https://goo.gl/forms/9aizSzOUrx7EvGlG3)" 853 | ] 854 | } 855 | ] 856 | } -------------------------------------------------------------------------------- /Week 13/Week_13_Dialogue_Systems_(Part_2).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "Week 13 - Dialogue Systems (Part 2).ipynb", 7 | "version": "0.3.2", 8 | "provenance": [], 9 | "collapsed_sections": [] 10 | }, 11 | "kernelspec": { 12 | "name": "python3", 13 | "display_name": "Python 3" 14 | }, 15 | "accelerator": "GPU" 16 | }, 17 | "cells": [ 18 | { 19 | "metadata": { 20 | "id": "Z4WlMyJVRkzQ", 21 | "colab_type": "code", 22 | "colab": {} 23 | }, 24 | "cell_type": "code", 25 | "source": [ 26 | "!pip3 -qq install torch==0.4.1\n", 27 | "!pip -qq install torchtext==0.3.1\n", 28 | "!pip -qq install spacy==2.0.16\n", 29 | "!pip install -qq gensim==3.6.0\n", 30 | "!python -m spacy download en\n", 31 | "!wget -O squad.zip -qq --no-check-certificate \"https://drive.google.com/uc?export=download&id=1h8dplcVzRkbrSYaTAbXYEAjcbApMxYQL\"\n", 32 | "!unzip squad.zip\n", 33 | "!wget -O opensubs.zip -qq --no-check-certificate \"https://drive.google.com/uc?export=download&id=1x1mNHweP95IeGFbDJPAI7zffgxrbqb7b\"\n", 34 | "!unzip opensubs.zip" 35 | ], 36 | "execution_count": 0, 37 | "outputs": [] 38 | }, 39 | { 40 | "metadata": { 41 | "id": "UvJKy3mtVOpw", 42 | "colab_type": "code", 43 | "colab": {} 44 | }, 45 | "cell_type": "code", 46 | "source": [ 47 | "import numpy as np\n", 48 | "\n", 49 | "import torch\n", 50 | "import torch.nn as nn\n", 51 | "import torch.nn.functional as F\n", 52 | "import torch.optim as optim\n", 53 | "\n", 54 | "\n", 55 | "if torch.cuda.is_available():\n", 56 | " from torch.cuda import FloatTensor, LongTensor\n", 57 | " DEVICE = torch.device('cuda')\n", 58 | "else:\n", 59 | " from torch import FloatTensor, LongTensor\n", 60 | " DEVICE = torch.device('cpu')\n", 61 | "\n", 62 | "np.random.seed(42)" 63 | ], 64 | "execution_count": 0, 65 | "outputs": [] 66 | }, 67 | { 68 | "metadata": { 69 | "id": "LKCD9Pt4Wupj", 70 | "colab_type": "text" 71 | }, 72 | "cell_type": "markdown", 73 | "source": [ 74 | "# General Conversation" 75 | ] 76 | }, 77 | { 78 | "metadata": { 79 | "id": "C3fbABnaXFyk", 80 | "colab_type": "text" 81 | }, 82 | "cell_type": "markdown", 83 | "source": [ 84 | "Сегодня разбираем, как устроена болталка.\n", 85 | "\n", 86 | "![](https://meduza.io/image/attachments/images/002/547/612/large/RLnxN4VdUmWFcBp8GjxUmA.jpg =x200) \n", 87 | "*From [«Алиса, за мной следит ФСБ?»: в соцсетях продолжают издеваться над голосовым помощником «Яндекса»](https://meduza.io/shapito/2017/10/11/alisa-za-mnoy-sledit-fsb-v-sotssetyah-prodolzhayut-izdevatsya-nad-golosovym-pomoschnikom-yandeksa)*\n", 88 | "\n", 89 | "Вообще, мы уже обсудили Seq2Seq модели, которые могут быть использованы для реализации болталки - однако, у них недостаток: высока вероятность сгенерировать что-то неграмматичное. Ну, как те пирожки.\n", 90 | "\n", 91 | "Поэтому почти всегда идут другим путем - вместо генерации применяют ранжирование. Нужно заранее составить большую базу ответов и просто выбирать наиболее подходящий к контексту каждый раз." 92 | ] 93 | }, 94 | { 95 | "metadata": { 96 | "id": "3mOxTaTjD0-p", 97 | "colab_type": "text" 98 | }, 99 | "cell_type": "markdown", 100 | "source": [ 101 | "## DSSM\n", 102 | "\n", 103 | "Для этого используют DSSM (Deep Structured Semantic Models):\n", 104 | "\n", 105 | "![](https://qph.fs.quoracdn.net/main-qimg-b90431ff9b4c60c5d69069d7bc048ff0) \n", 106 | "*From [What are Siamese neural networks, what applications are they good for, and why?](https://www.quora.com/What-are-Siamese-neural-networks-what-applications-are-they-good-for-and-why)*\n", 107 | "\n", 108 | "Эта сеть состоит из (обычно) пары башен: левая кодирует запрос, правая - ответ. Задача - научиться считать близость между запросом и ответом.\n", 109 | "\n", 110 | "Дальше набирают большой корпус из пар запрос-ответ (запрос может быть как одним вопросом, так и контекстом - несколькими последними вопросами/ответами). \n", 111 | "\n", 112 | "Для ответов предпосчитывают их векторы, каждый новый запрос кодируют с помощью правой башни и находят среди предпосчитанных векторов ближайший." 113 | ] 114 | }, 115 | { 116 | "metadata": { 117 | "id": "JSz9ALe3w1v1", 118 | "colab_type": "text" 119 | }, 120 | "cell_type": "markdown", 121 | "source": [ 122 | "## Данные\n", 123 | "\n", 124 | "Будем использовать для начала [Stanford Question Answering Dataset (SQuAD)](https://rajpurkar.github.io/SQuAD-explorer/). Вообще, там задача - найти в тексте ответ на вопрос. Но мы будем просто выбирать среди предложений текста наиболее близкое к вопросу.\n", 125 | "\n", 126 | "*Эта часть ноутбука сильно основана на [шадовском ноутбуке](https://github.com/yandexdataschool/nlp_course/blob/master/week10_dialogue/seminar.ipynb)*." 127 | ] 128 | }, 129 | { 130 | "metadata": { 131 | "id": "_6OqXUJxjnX4", 132 | "colab_type": "code", 133 | "colab": {} 134 | }, 135 | "cell_type": "code", 136 | "source": [ 137 | "import pandas as pd\n", 138 | "\n", 139 | "train_data = pd.read_json('train.json')\n", 140 | "test_data = pd.read_json('test.json')" 141 | ], 142 | "execution_count": 0, 143 | "outputs": [] 144 | }, 145 | { 146 | "metadata": { 147 | "id": "s0DPvn5djkha", 148 | "colab_type": "code", 149 | "colab": {} 150 | }, 151 | "cell_type": "code", 152 | "source": [ 153 | "row = train_data.iloc[40]\n", 154 | "print('QUESTION:', row.question, '\\n')\n", 155 | "for i, cand in enumerate(row.options):\n", 156 | " print('[ ]' if i not in row.correct_indices else '[v]', cand)" 157 | ], 158 | "execution_count": 0, 159 | "outputs": [] 160 | }, 161 | { 162 | "metadata": { 163 | "id": "KIeJIQ7ex4nB", 164 | "colab_type": "text" 165 | }, 166 | "cell_type": "markdown", 167 | "source": [ 168 | "Токенизируем предложения:" 169 | ] 170 | }, 171 | { 172 | "metadata": { 173 | "id": "udkrpaSHq7mg", 174 | "colab_type": "code", 175 | "colab": {} 176 | }, 177 | "cell_type": "code", 178 | "source": [ 179 | "import spacy\n", 180 | "\n", 181 | "spacy = spacy.load('en')\n", 182 | "\n", 183 | "train_data.question = train_data.question.apply(lambda text: [tok.text.lower() for tok in spacy.tokenizer(text)])\n", 184 | "train_data.options = train_data.options.apply(lambda options: [[tok.text.lower() for tok in spacy.tokenizer(text)] for text in options])\n", 185 | "\n", 186 | "test_data.question = test_data.question.apply(lambda text: [tok.text.lower() for tok in spacy.tokenizer(text)])\n", 187 | "test_data.options = test_data.options.apply(lambda options: [[tok.text.lower() for tok in spacy.tokenizer(text)] for text in options])" 188 | ], 189 | "execution_count": 0, 190 | "outputs": [] 191 | }, 192 | { 193 | "metadata": { 194 | "id": "tU5jRHpax7Ot", 195 | "colab_type": "text" 196 | }, 197 | "cell_type": "markdown", 198 | "source": [ 199 | "У нас не так-то много данных, чтобы учить всё с нуля, поэтому будем сразу использовать предобученные эмбеддинги:" 200 | ] 201 | }, 202 | { 203 | "metadata": { 204 | "id": "2mQjmbvs-jFQ", 205 | "colab_type": "code", 206 | "colab": {} 207 | }, 208 | "cell_type": "code", 209 | "source": [ 210 | "import gensim.downloader as api\n", 211 | "\n", 212 | "w2v_model = api.load('glove-wiki-gigaword-100')" 213 | ], 214 | "execution_count": 0, 215 | "outputs": [] 216 | }, 217 | { 218 | "metadata": { 219 | "id": "dC8Db3DKyDSz", 220 | "colab_type": "text" 221 | }, 222 | "cell_type": "markdown", 223 | "source": [ 224 | "**Задание** Постройте матрицу предобученных эмбеддингов для самых частотных слов в выборке." 225 | ] 226 | }, 227 | { 228 | "metadata": { 229 | "id": "AinFu7nb6DSf", 230 | "colab_type": "code", 231 | "colab": {} 232 | }, 233 | "cell_type": "code", 234 | "source": [ 235 | "from collections import Counter\n", 236 | "\n", 237 | "\n", 238 | "def build_word_embeddings(data, w2v_model, min_freq=5):\n", 239 | " words = Counter()\n", 240 | " \n", 241 | " for text in data.question:\n", 242 | " for word in text:\n", 243 | " words[word] += 1\n", 244 | " \n", 245 | " for options in data.options:\n", 246 | " for text in options:\n", 247 | " for word in text:\n", 248 | " words[word] += 1\n", 249 | " \n", 250 | " word2ind = {\n", 251 | " '': 0,\n", 252 | " '': 1\n", 253 | " }\n", 254 | " \n", 255 | " embeddings = [\n", 256 | " np.zeros(w2v_model.vectors.shape[1]),\n", 257 | " np.zeros(w2v_model.vectors.shape[1])\n", 258 | " ]\n", 259 | " \n", 260 | " \n", 261 | "\n", 262 | " return word2ind, np.array(embeddings)" 263 | ], 264 | "execution_count": 0, 265 | "outputs": [] 266 | }, 267 | { 268 | "metadata": { 269 | "id": "rKJhqyV__jhl", 270 | "colab_type": "code", 271 | "colab": {} 272 | }, 273 | "cell_type": "code", 274 | "source": [ 275 | "word2ind, embeddings = build_word_embeddings(train_data, w2v_model, min_freq=8)\n", 276 | "print('Vocab size =', len(word2ind))" 277 | ], 278 | "execution_count": 0, 279 | "outputs": [] 280 | }, 281 | { 282 | "metadata": { 283 | "id": "M-_xdNG9yYka", 284 | "colab_type": "text" 285 | }, 286 | "cell_type": "markdown", 287 | "source": [ 288 | "Для генерации батчей будем использовать такой класс:" 289 | ] 290 | }, 291 | { 292 | "metadata": { 293 | "id": "4IoQrJ5rTSSN", 294 | "colab_type": "code", 295 | "colab": {} 296 | }, 297 | "cell_type": "code", 298 | "source": [ 299 | "import random\n", 300 | "import math\n", 301 | "\n", 302 | "\n", 303 | "def to_matrix(lines, word2ind):\n", 304 | " max_sent_len = max(len(line) for line in lines)\n", 305 | " matrix = np.zeros((len(lines), max_sent_len))\n", 306 | "\n", 307 | " for batch_ind, line in enumerate(lines):\n", 308 | " matrix[batch_ind, :len(line)] = [word2ind.get(word, 1) for word in line]\n", 309 | "\n", 310 | " return LongTensor(matrix)\n", 311 | "\n", 312 | "\n", 313 | "class BatchIterator():\n", 314 | " def __init__(self, data, batch_size, word2ind, shuffle=True):\n", 315 | " self._data = data\n", 316 | " self._num_samples = len(data)\n", 317 | " self._batch_size = batch_size\n", 318 | " self._word2ind = word2ind\n", 319 | " self._shuffle = shuffle\n", 320 | " self._batches_count = int(math.ceil(len(data) / batch_size))\n", 321 | " \n", 322 | " def __len__(self):\n", 323 | " return self._batches_count\n", 324 | " \n", 325 | " def __iter__(self):\n", 326 | " return self._iterate_batches()\n", 327 | "\n", 328 | " def _iterate_batches(self):\n", 329 | " indices = np.arange(self._num_samples)\n", 330 | " if self._shuffle:\n", 331 | " np.random.shuffle(indices)\n", 332 | "\n", 333 | " for start in range(0, self._num_samples, self._batch_size):\n", 334 | " end = min(start + self._batch_size, self._num_samples)\n", 335 | "\n", 336 | " batch_indices = indices[start: end]\n", 337 | "\n", 338 | " batch = self._data.iloc[batch_indices]\n", 339 | " questions = batch['question'].values\n", 340 | " correct_answers = np.array([\n", 341 | " row['options'][random.choice(row['correct_indices'])]\n", 342 | " for i, row in batch.iterrows()\n", 343 | " ])\n", 344 | " wrong_answers = np.array([\n", 345 | " row['options'][random.choice(row['wrong_indices'])]\n", 346 | " for i, row in batch.iterrows()\n", 347 | " ])\n", 348 | "\n", 349 | " yield {\n", 350 | " 'questions': to_matrix(questions, self._word2ind),\n", 351 | " 'correct_answers': to_matrix(correct_answers, self._word2ind),\n", 352 | " 'wrong_answers': to_matrix(wrong_answers, self._word2ind)\n", 353 | " }" 354 | ], 355 | "execution_count": 0, 356 | "outputs": [] 357 | }, 358 | { 359 | "metadata": { 360 | "id": "as5kgtjLyRE6", 361 | "colab_type": "code", 362 | "colab": {} 363 | }, 364 | "cell_type": "code", 365 | "source": [ 366 | "train_iter = BatchIterator(train_data, 64, word2ind)\n", 367 | "test_iter = BatchIterator(test_data, 128, word2ind)" 368 | ], 369 | "execution_count": 0, 370 | "outputs": [] 371 | }, 372 | { 373 | "metadata": { 374 | "id": "27zeQlTJzU1x", 375 | "colab_type": "text" 376 | }, 377 | "cell_type": "markdown", 378 | "source": [ 379 | "Он просто сэмплирует последовательности из вопросов, правильных и неправильных ответов на них:" 380 | ] 381 | }, 382 | { 383 | "metadata": { 384 | "id": "F-ohzmkXzO0e", 385 | "colab_type": "code", 386 | "colab": {} 387 | }, 388 | "cell_type": "code", 389 | "source": [ 390 | "batch = next(iter(train_iter))\n", 391 | "\n", 392 | "batch" 393 | ], 394 | "execution_count": 0, 395 | "outputs": [] 396 | }, 397 | { 398 | "metadata": { 399 | "id": "BSXFV1CTyfGo", 400 | "colab_type": "text" 401 | }, 402 | "cell_type": "markdown", 403 | "source": [ 404 | "## Модель\n", 405 | "\n", 406 | "**Задание** Реализуйте модель энкодера для текстов - башни DSSM модели.\n", 407 | "\n", 408 | "*Это не обязательно должна быть сложная модель, вполне сойдет сверточная, которая будет учиться гораздо быстрее.*" 409 | ] 410 | }, 411 | { 412 | "metadata": { 413 | "id": "Ve7WZ4-dvbuw", 414 | "colab_type": "code", 415 | "colab": {} 416 | }, 417 | "cell_type": "code", 418 | "source": [ 419 | "class Encoder(nn.Module):\n", 420 | " def __init__(self, embeddings, hidden_dim=128, output_dim=128):\n", 421 | " super().__init__()\n", 422 | " \n", 423 | " \n", 424 | " \n", 425 | " def forward(self, inputs):\n", 426 | " " 427 | ], 428 | "execution_count": 0, 429 | "outputs": [] 430 | }, 431 | { 432 | "metadata": { 433 | "id": "ZFBAUGGfzFwF", 434 | "colab_type": "text" 435 | }, 436 | "cell_type": "markdown", 437 | "source": [ 438 | "### Triplet Loss\n", 439 | "\n", 440 | "Мы хотим не просто научить энкодер строить эмбеддинги для предложений. Мы хотим, чтобы притягивать векторы правильных ответов к вопросам и отталкивать неправильные. Для этого используют, например, *Triplet Loss*:\n", 441 | "\n", 442 | "$$ L = \\frac 1N \\underset {q, a^+, a^-} \\sum max(0, \\space \\delta - sim[V_q(q), V_a(a^+)] + sim[V_q(q), V_a(a^-)] ),$$\n", 443 | "\n", 444 | "где\n", 445 | "* $sim[a, b]$ функция похожести (например, dot product или cosine similarity)\n", 446 | "* $\\delta$ - гиперпараметр модели. Если $sim[a, b]$ линейно по $b$, то все $\\delta > 0$ эквиватентны.\n", 447 | "\n", 448 | "![img](https://raw.githubusercontent.com/yandexdataschool/nlp_course/master/resources/margin.png)\n", 449 | "\n", 450 | "**Задание** Реализуйте triplet loss, а также подсчет recall - процента случаев, когда правильный ответ был ближе неправильного." 451 | ] 452 | }, 453 | { 454 | "metadata": { 455 | "id": "GdHJK51dz0CB", 456 | "colab_type": "code", 457 | "colab": {} 458 | }, 459 | "cell_type": "code", 460 | "source": [ 461 | "class DSSM(nn.Module):\n", 462 | " def __init__(self, question_encoder, answer_encoder):\n", 463 | " super().__init__()\n", 464 | " self.question_encoder = question_encoder\n", 465 | " self.answer_encoder = answer_encoder\n", 466 | " \n", 467 | " def forward(self, questions, correct_answers, wrong_answers):\n", 468 | " \n", 469 | "\n", 470 | " def calc_triplet_loss(self, question_embeddings, correct_answer_embeddings, wrong_answer_embeddings, delta=1.0):\n", 471 | " \"\"\"Returns the triplet loss based on the equation above\"\"\"\n", 472 | " \n", 473 | " \n", 474 | " def calc_recall_at_1(self, question_embeddings, correct_answer_embeddings, wrong_answer_embeddings):\n", 475 | " \"\"\"Returns the number of cases when the correct answer were more similar than incorrect one\"\"\"\n", 476 | " \n", 477 | " \n", 478 | " @staticmethod\n", 479 | " def similarity(question_embeddings, answer_embeddings):\n", 480 | " \"\"\"Returns sim[a, b]\"\"\"\n", 481 | " " 482 | ], 483 | "execution_count": 0, 484 | "outputs": [] 485 | }, 486 | { 487 | "metadata": { 488 | "id": "ZWvlMTWpxTvJ", 489 | "colab_type": "code", 490 | "colab": {} 491 | }, 492 | "cell_type": "code", 493 | "source": [ 494 | "class ModelTrainer():\n", 495 | " def __init__(self, model, optimizer):\n", 496 | " self._model = model\n", 497 | " self._optimizer = optimizer\n", 498 | " \n", 499 | " def on_epoch_begin(self, is_train, name, batches_count):\n", 500 | " \"\"\"\n", 501 | " Initializes metrics\n", 502 | " \"\"\"\n", 503 | " self._epoch_loss = 0\n", 504 | " self._correct_count, self._total_count = 0, 0\n", 505 | " self._is_train = is_train\n", 506 | " self._name = name\n", 507 | " self._batches_count = batches_count\n", 508 | " \n", 509 | " self._model.train(is_train)\n", 510 | " \n", 511 | " def on_epoch_end(self):\n", 512 | " \"\"\"\n", 513 | " Outputs final metrics\n", 514 | " \"\"\"\n", 515 | " return '{:>5s} Loss = {:.5f}, Recall@1 = {:.2%}'.format(\n", 516 | " self._name, self._epoch_loss / self._batches_count, self._correct_count / self._total_count\n", 517 | " )\n", 518 | " \n", 519 | " def on_batch(self, batch):\n", 520 | " \"\"\"\n", 521 | " Performs forward and (if is_train) backward pass with optimization, updates metrics\n", 522 | " \"\"\"\n", 523 | " \n", 524 | " question_embs, correct_answer_embs, wrong_answer_embs = self._model(\n", 525 | " batch['questions'], batch['correct_answers'], batch['wrong_answers']\n", 526 | " )\n", 527 | " loss = self._model.calc_triplet_loss(question_embs, correct_answer_embs, wrong_answer_embs)\n", 528 | " correct_count = self._model.calc_recall_at_1(question_embs, correct_answer_embs, wrong_answer_embs)\n", 529 | " total_count = len(batch['questions'])\n", 530 | " \n", 531 | " self._correct_count += correct_count\n", 532 | " self._total_count += total_count\n", 533 | " self._epoch_loss += loss.item()\n", 534 | " \n", 535 | " if self._is_train:\n", 536 | " self._optimizer.zero_grad()\n", 537 | " loss.backward()\n", 538 | " nn.utils.clip_grad_norm_(self._model.parameters(), 1.)\n", 539 | " self._optimizer.step()\n", 540 | "\n", 541 | " return '{:>5s} Loss = {:.5f}, Recall@1 = {:.2%}'.format(\n", 542 | " self._name, loss.item(), correct_count / total_count\n", 543 | " )" 544 | ], 545 | "execution_count": 0, 546 | "outputs": [] 547 | }, 548 | { 549 | "metadata": { 550 | "id": "ecI_vVBgzpVn", 551 | "colab_type": "code", 552 | "colab": {} 553 | }, 554 | "cell_type": "code", 555 | "source": [ 556 | "import math\n", 557 | "from tqdm import tqdm\n", 558 | "tqdm.get_lock().locks = []\n", 559 | "\n", 560 | "\n", 561 | "def do_epoch(trainer, data_iter, is_train, name=None):\n", 562 | " trainer.on_epoch_begin(is_train, name, batches_count=len(data_iter))\n", 563 | " \n", 564 | " with torch.autograd.set_grad_enabled(is_train):\n", 565 | " with tqdm(total=len(data_iter)) as progress_bar:\n", 566 | " for i, batch in enumerate(data_iter):\n", 567 | " batch_progress = trainer.on_batch(batch)\n", 568 | "\n", 569 | " progress_bar.update()\n", 570 | " progress_bar.set_description(batch_progress)\n", 571 | " \n", 572 | " epoch_progress = trainer.on_epoch_end()\n", 573 | " progress_bar.set_description(epoch_progress)\n", 574 | " progress_bar.refresh()\n", 575 | "\n", 576 | " \n", 577 | "def fit(trainer, train_iter, epochs_count=1, val_iter=None):\n", 578 | " best_val_loss = None\n", 579 | " for epoch in range(epochs_count):\n", 580 | " name_prefix = '[{} / {}] '.format(epoch + 1, epochs_count)\n", 581 | " do_epoch(trainer, train_iter, is_train=True, name=name_prefix + 'Train:')\n", 582 | " \n", 583 | " if not val_iter is None:\n", 584 | " do_epoch(trainer, val_iter, is_train=False, name=name_prefix + ' Val:')" 585 | ], 586 | "execution_count": 0, 587 | "outputs": [] 588 | }, 589 | { 590 | "metadata": { 591 | "id": "K1GvkkLh70S6", 592 | "colab_type": "text" 593 | }, 594 | "cell_type": "markdown", 595 | "source": [ 596 | "Запустим, наконец, учиться модель:" 597 | ] 598 | }, 599 | { 600 | "metadata": { 601 | "id": "ipXj9pOD2afY", 602 | "colab_type": "code", 603 | "colab": {} 604 | }, 605 | "cell_type": "code", 606 | "source": [ 607 | "embeddings = FloatTensor(embeddings)\n", 608 | "\n", 609 | "model = DSSM(\n", 610 | " Encoder(embeddings),\n", 611 | " Encoder(embeddings)\n", 612 | ").to(DEVICE)\n", 613 | "\n", 614 | "optimizer = optim.Adam(model.parameters())\n", 615 | "\n", 616 | "trainer = ModelTrainer(model, optimizer)\n", 617 | "\n", 618 | "fit(trainer, train_iter, epochs_count=30, val_iter=test_iter)" 619 | ], 620 | "execution_count": 0, 621 | "outputs": [] 622 | }, 623 | { 624 | "metadata": { 625 | "id": "g_AHIoCB73XA", 626 | "colab_type": "text" 627 | }, 628 | "cell_type": "markdown", 629 | "source": [ 630 | "### Точность предсказаний\n", 631 | "\n", 632 | "Оценим, насколько хорошо модель предсказывает правильный ответ.\n", 633 | "\n", 634 | "**Задание** Для каждого вопроса найдите индекс ответа, генерируемого сетью:" 635 | ] 636 | }, 637 | { 638 | "metadata": { 639 | "id": "QrA8N0zDJczj", 640 | "colab_type": "code", 641 | "colab": {} 642 | }, 643 | "cell_type": "code", 644 | "source": [ 645 | "predictions = []\n", 646 | "\n", 647 | " \n", 648 | "accuracy = np.mean([\n", 649 | " answer in correct_ind\n", 650 | " for answer, correct_ind in zip(predictions, test_data['correct_indices'].values)\n", 651 | "])\n", 652 | "print(\"Accuracy: %0.5f\" % accuracy)" 653 | ], 654 | "execution_count": 0, 655 | "outputs": [] 656 | }, 657 | { 658 | "metadata": { 659 | "id": "HZijVoOTLcCD", 660 | "colab_type": "code", 661 | "colab": {} 662 | }, 663 | "cell_type": "code", 664 | "source": [ 665 | "def draw_results(question, possible_answers, predicted_index, correct_indices):\n", 666 | " print(\"Q:\", ' '.join(question), end='\\n\\n')\n", 667 | " for i, answer in enumerate(possible_answers):\n", 668 | " print(\"#%i: %s %s\" % (i, '[*]' if i == predicted_index else '[ ]', ' '.join(answer)))\n", 669 | " \n", 670 | " print(\"\\nVerdict:\", \"CORRECT\" if predicted_index in correct_indices else \"INCORRECT\", \n", 671 | " \"(ref: %s)\" % correct_indices, end='\\n' * 3)" 672 | ], 673 | "execution_count": 0, 674 | "outputs": [] 675 | }, 676 | { 677 | "metadata": { 678 | "id": "-_pYbNcsLfli", 679 | "colab_type": "code", 680 | "colab": {} 681 | }, 682 | "cell_type": "code", 683 | "source": [ 684 | "for i in [1, 100, 1000, 2000, 3000, 4000, 5000]:\n", 685 | " draw_results(test_data.iloc[i].question, test_data.iloc[i].options,\n", 686 | " predictions[i], test_data.iloc[i].correct_indices)" 687 | ], 688 | "execution_count": 0, 689 | "outputs": [] 690 | }, 691 | { 692 | "metadata": { 693 | "id": "T5OC4EGt8HAo", 694 | "colab_type": "text" 695 | }, 696 | "cell_type": "markdown", 697 | "source": [ 698 | "## Hard-negatives mining\n", 699 | "\n", 700 | "На самом деле, в большинстве случаев у нас отрицательных примеров.\n", 701 | "\n", 702 | "Например, есть база диалогов - и где брать отрицательные примеры к ответам?\n", 703 | "\n", 704 | "Для этого используют *hard-negatives mining*. Берут в качестве отрицательного примера самый близкий из неправильных примеров в батче:\n", 705 | "$$a^-_{hard} = \\underset {a^-} {argmax} \\space sim[V_q(q), V_a(a^-)]$$\n", 706 | "\n", 707 | "Неправильные в данном случае - все, кроме правильного :)\n", 708 | "\n", 709 | "Реализуется это как-то так:\n", 710 | "* Батч состоит из правильных пар вопрос-ответ.\n", 711 | "* Для всех вопросов и всех ответов считают эмбеддинги.\n", 712 | "* Положительные примеры у нас есть - осталось найти для каждого вопроса наиболее похожие на него ответы, которые предназначались другим вопросам.\n", 713 | "\n", 714 | "**Задание** Обновите `DSSM`, чтобы делать hard-negatives mining внутри него.\n", 715 | "\n", 716 | "*Может понадобиться нормализовывать векторы с помощью `F.normalize` перед подсчетом `similarity`*" 717 | ] 718 | }, 719 | { 720 | "metadata": { 721 | "id": "tY_BebgAOY70", 722 | "colab_type": "code", 723 | "colab": {} 724 | }, 725 | "cell_type": "code", 726 | "source": [ 727 | "class DSSM(nn.Module):\n", 728 | " def __init__(self, question_encoder, answer_encoder):\n", 729 | " super().__init__()\n", 730 | " self.question_encoder = question_encoder\n", 731 | " self.answer_encoder = answer_encoder\n", 732 | " \n", 733 | " def forward(self, questions, correct_answers, wrong_answers):\n", 734 | " \"\"\"Ignore wrong_answers, they are here just for compatibility sake\"\"\"\n", 735 | " \n", 736 | "\n", 737 | " def calc_triplet_loss(self, question_embeddings, answer_embeddings, delta=1.0):\n", 738 | " \"\"\"Returns the triplet loss based on the equation above\"\"\"\n", 739 | " \n", 740 | " \n", 741 | " def calc_recall_at_1(self, question_embeddings, correct_answer_embeddings, wrong_answer_embeddings):\n", 742 | " \"\"\"Returns the number of cases when the correct answer were more similar than incorrect one\"\"\"\n", 743 | " \n", 744 | " \n", 745 | " @staticmethod\n", 746 | " def similarity(question_embeddings, answer_embeddings):\n", 747 | " " 748 | ], 749 | "execution_count": 0, 750 | "outputs": [] 751 | }, 752 | { 753 | "metadata": { 754 | "id": "0AIvf3Zvtabs", 755 | "colab_type": "code", 756 | "colab": {} 757 | }, 758 | "cell_type": "code", 759 | "source": [ 760 | "model = DSSM(\n", 761 | " question_encoder=Encoder(embeddings),\n", 762 | " answer_encoder=Encoder(embeddings)\n", 763 | ").to(DEVICE)\n", 764 | "\n", 765 | "optimizer = optim.Adam(model.parameters())\n", 766 | "\n", 767 | "trainer = ModelTrainer(model, optimizer)\n", 768 | "\n", 769 | "fit(trainer, train_iter, epochs_count=30, val_iter=test_iter)" 770 | ], 771 | "execution_count": 0, 772 | "outputs": [] 773 | }, 774 | { 775 | "metadata": { 776 | "id": "pXaL1iRz-zEG", 777 | "colab_type": "text" 778 | }, 779 | "cell_type": "markdown", 780 | "source": [ 781 | "**Задание** Есть также вариант с semi-hard negatives - когда в качестве отрицательного примера берется наилучший среди тех, чья similarity меньше similarity вопроса с положительным примером. Попробуйте реализовать его." 782 | ] 783 | }, 784 | { 785 | "metadata": { 786 | "id": "edUVeXG0_lCa", 787 | "colab_type": "text" 788 | }, 789 | "cell_type": "markdown", 790 | "source": [ 791 | "# Болталка\n", 792 | "\n", 793 | "Чтобы реализовать болталку, нужен нормальный корпус с диалогами. Например, OpenSubtitles." 794 | ] 795 | }, 796 | { 797 | "metadata": { 798 | "id": "s3YmDo9Z_xG-", 799 | "colab_type": "code", 800 | "colab": {} 801 | }, 802 | "cell_type": "code", 803 | "source": [ 804 | "!head train.txt" 805 | ], 806 | "execution_count": 0, 807 | "outputs": [] 808 | }, 809 | { 810 | "metadata": { 811 | "id": "A0R77ezW_1Wd", 812 | "colab_type": "text" 813 | }, 814 | "cell_type": "markdown", 815 | "source": [ 816 | "Ну, примерно нормальный.\n", 817 | "\n", 818 | "Считаем датасет." 819 | ] 820 | }, 821 | { 822 | "metadata": { 823 | "id": "SwHYmb28HPUn", 824 | "colab_type": "code", 825 | "colab": {} 826 | }, 827 | "cell_type": "code", 828 | "source": [ 829 | "from nltk import wordpunct_tokenize\n", 830 | "\n", 831 | "def read_dataset(path):\n", 832 | " data = []\n", 833 | " with open(path) as f:\n", 834 | " for line in tqdm(f):\n", 835 | " query, response = line.strip().split('\\t')\n", 836 | " data.append((\n", 837 | " wordpunct_tokenize(query.strip()),\n", 838 | " wordpunct_tokenize(response.strip())\n", 839 | " ))\n", 840 | " return data\n", 841 | "\n", 842 | "train_data = read_dataset('train.txt')\n", 843 | "val_data = read_dataset('valid.txt')\n", 844 | "test_data = read_dataset('test.txt')" 845 | ], 846 | "execution_count": 0, 847 | "outputs": [] 848 | }, 849 | { 850 | "metadata": { 851 | "id": "WTfkv17tHM3I", 852 | "colab_type": "code", 853 | "colab": {} 854 | }, 855 | "cell_type": "code", 856 | "source": [ 857 | "from torchtext.data import Field, Example, Dataset, BucketIterator\n", 858 | "\n", 859 | "query_field = Field(lower=True)\n", 860 | "response_field = Field(lower=True)\n", 861 | "\n", 862 | "fields = [('query', query_field), ('response', response_field)]\n", 863 | "\n", 864 | "train_dataset = Dataset([Example.fromlist(example, fields) for example in train_data], fields)\n", 865 | "val_dataset = Dataset([Example.fromlist(example, fields) for example in val_data], fields)\n", 866 | "test_dataset = Dataset([Example.fromlist(example, fields) for example in test_data], fields)\n", 867 | "\n", 868 | "query_field.build_vocab(train_dataset, min_freq=5)\n", 869 | "response_field.build_vocab(train_dataset, min_freq=5)\n", 870 | "\n", 871 | "print('Query vocab size =', len(query_field.vocab))\n", 872 | "print('Response vocab size =', len(response_field.vocab))\n", 873 | "\n", 874 | "train_iter, val_iter, test_iter = BucketIterator.splits(\n", 875 | " datasets=(train_dataset, val_dataset, test_dataset), batch_sizes=(512, 1024, 1024), \n", 876 | " shuffle=True, device=DEVICE, sort=False\n", 877 | ")" 878 | ], 879 | "execution_count": 0, 880 | "outputs": [] 881 | }, 882 | { 883 | "metadata": { 884 | "id": "M-Ok7--NHUX0", 885 | "colab_type": "text" 886 | }, 887 | "cell_type": "markdown", 888 | "source": [ 889 | "**Задание** Реализовать болталку по аналогии с тем, что уже написали." 890 | ] 891 | }, 892 | { 893 | "metadata": { 894 | "id": "n2x9-j4oz08p", 895 | "colab_type": "text" 896 | }, 897 | "cell_type": "markdown", 898 | "source": [ 899 | "# Дополнительные материалы\n", 900 | "\n", 901 | "## Статьи\n", 902 | "Learning Deep Structured Semantic Models for Web Search using Clickthrough Data, 2013 [[pdf]](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/cikm2013_DSSM_fullversion.pdf) \n", 903 | "Deep Learning and Continuous Representations for Natural Language Processing, Microsoft tutorial [[pdf]](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/NAACL-HLT-2015_tutorial.pdf)\n", 904 | "\n", 905 | "## Блоги\n", 906 | "[Neural conversational models: как научить нейронную сеть светской беседе](https://habr.com/company/yandex/blog/333912/) \n", 907 | "[Искусственный интеллект в поиске. Как Яндекс научился применять нейронные сети, чтобы искать по смыслу, а не по словам](https://habr.com/company/yandex/blog/314222/) \n", 908 | "[Triplet loss, Olivier Moindrot](https://omoindrot.github.io/triplet-loss)" 909 | ] 910 | }, 911 | { 912 | "metadata": { 913 | "id": "gjkS1Kpkz4Aa", 914 | "colab_type": "text" 915 | }, 916 | "cell_type": "markdown", 917 | "source": [ 918 | "# Сдача\n", 919 | "\n", 920 | "[Форма для сдачи](https://goo.gl/forms/bf2auPe8FL5C0jzp2) \n", 921 | "[Feedback](https://goo.gl/forms/9aizSzOUrx7EvGlG3)" 922 | ] 923 | } 924 | ] 925 | } -------------------------------------------------------------------------------- /Week 07/Week_07_Language_Models.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "Week 07 - Language Models.ipynb", 7 | "version": "0.3.2", 8 | "provenance": [], 9 | "collapsed_sections": [ 10 | "oTrSUkqEhZzh" 11 | ] 12 | }, 13 | "kernelspec": { 14 | "name": "python3", 15 | "display_name": "Python 3" 16 | }, 17 | "accelerator": "GPU" 18 | }, 19 | "cells": [ 20 | { 21 | "metadata": { 22 | "id": "OE7fXh-OSJYF", 23 | "colab_type": "code", 24 | "colab": {} 25 | }, 26 | "cell_type": "code", 27 | "source": [ 28 | "!pip3 -qq install torch==0.4.1\n", 29 | "!pip -qq install torchtext==0.3.1\n", 30 | "!wget -qq --no-check-certificate 'https://drive.google.com/uc?export=download&id=1Pq4aklVdj-sOnQw68e1ZZ_ImMiC8IR1V' -O tweets.csv.zip\n", 31 | "!wget -qq --no-check-certificate \"https://drive.google.com/uc?export=download&id=1ji7dhr9FojPeV51dDlKRERIqr3vdZfhu\" -O surnames.txt\n", 32 | "!unzip tweets.csv.zip" 33 | ], 34 | "execution_count": 0, 35 | "outputs": [] 36 | }, 37 | { 38 | "metadata": { 39 | "id": "uhvfH55PUJ8K", 40 | "colab_type": "code", 41 | "colab": {} 42 | }, 43 | "cell_type": "code", 44 | "source": [ 45 | "import numpy as np\n", 46 | "\n", 47 | "import torch\n", 48 | "import torch.nn as nn\n", 49 | "import torch.nn.functional as F\n", 50 | "import torch.optim as optim\n", 51 | "\n", 52 | "\n", 53 | "if torch.cuda.is_available():\n", 54 | " from torch.cuda import FloatTensor, LongTensor\n", 55 | " DEVICE = torch.device('cuda')\n", 56 | "else:\n", 57 | " from torch import FloatTensor, LongTensor\n", 58 | " DEVICE = torch.device('cpu')\n", 59 | "\n", 60 | "np.random.seed(42)" 61 | ], 62 | "execution_count": 0, 63 | "outputs": [] 64 | }, 65 | { 66 | "metadata": { 67 | "id": "jVcnkGDgxfNx", 68 | "colab_type": "text" 69 | }, 70 | "cell_type": "markdown", 71 | "source": [ 72 | "# Языковые модели" 73 | ] 74 | }, 75 | { 76 | "metadata": { 77 | "id": "8Kjg1Z3xxmEP", 78 | "colab_type": "text" 79 | }, 80 | "cell_type": "markdown", 81 | "source": [ 82 | "*Языковая модель* - это штука, которая умеет оценивать вероятности встретить последовательность слов $w_1, \\ldots, w_n$: \n", 83 | "$$\\mathbf{P}(w_1, \\ldots, w_n) = \\prod_k \\mathbf{P}(w_k|w_{k-1}, \\ldots, w_{1}).$$\n", 84 | "\n", 85 | "Интерпретируемы и интересны тут именно условные вероятности - какое слово языковая модель ожидает вслед за данными. У нас у всех такая языковая модель есть, так-то. Например, в таком контексте\n", 86 | "\n", 87 | "![](https://hsto.org/web/956/239/601/95623960157b4e15a1b3f599aed62ed2.png =x170)\n", 88 | "\n", 89 | "моя языковая модель говорит - после *честных* навряд ли пойдёт *мой*. А вот *и* или, конечно, *правил* - очень даже.\n", 90 | "\n", 91 | "А задача такая: научиться генерировать политические твиты по образу и подобию `Russian Troll Tweets`. Датасет взят отсюда: https://www.kaggle.com/vikasg/russian-troll-tweets" 92 | ] 93 | }, 94 | { 95 | "metadata": { 96 | "id": "JpjfUoN4_WY7", 97 | "colab_type": "code", 98 | "colab": {} 99 | }, 100 | "cell_type": "code", 101 | "source": [ 102 | "import pandas as pd\n", 103 | "\n", 104 | "data = pd.read_csv('tweets.csv')\n", 105 | "\n", 106 | "data.text.sample(15).tolist()" 107 | ], 108 | "execution_count": 0, 109 | "outputs": [] 110 | }, 111 | { 112 | "metadata": { 113 | "id": "WAQ4d__2_sAz", 114 | "colab_type": "text" 115 | }, 116 | "cell_type": "markdown", 117 | "source": [ 118 | "Да, результаты будут упороты, сразу предупреждаю." 119 | ] 120 | }, 121 | { 122 | "metadata": { 123 | "id": "7Qvqidof7Fsi", 124 | "colab_type": "text" 125 | }, 126 | "cell_type": "markdown", 127 | "source": [ 128 | "## Чтение данных" 129 | ] 130 | }, 131 | { 132 | "metadata": { 133 | "id": "OSu56oDX-KY5", 134 | "colab_type": "text" 135 | }, 136 | "cell_type": "markdown", 137 | "source": [ 138 | "Кого-нибудь уже достало писать все эти построения батчей, словари - вот это всё? Лично меня - да!\n", 139 | "\n", 140 | "В pytorch есть специальный класс для генерации батчей - `Dataset`. Вместо того, чтобы писать функцию типа `iterate_batches`, можно отнаследовать от него и переопределить методы `__len__` и `__getitem__`... и реализовать в них почти всё то, что было в `iterate_batches`. Пока не впечатляет, да?\n", 141 | "\n", 142 | "Ещё там есть `DataLoader`, умеющий работать с датасетом. Он позволяет делать shuffle батчей и генерацию их в отдельных процессах - это особенно важно, когда генерация батча - долгая операция. Например, в картинках. Почитать про это всё можно здесь: [Data Loading and Processing Tutorial](https://pytorch.org/tutorials/beginner/data_loading_tutorial.html).\n", 143 | "\n", 144 | "Но пока что всё равно не особо круто, мне кажется. Интересно другое - у pytorch в репозитории живет отдельная библиотечка - [torchtext](https://github.com/pytorch/text). Вот она уже даст нам специальные реализации `Dataset` для работы с текстом и всякие тулзы, делающие жизнь чуточку проще.\n", 145 | "\n", 146 | "Библиотеке, на мой взгляд, недостает туториалов, в которых бы показывалось, как с ней работать - но можно читать исходный код, он приятный.\n", 147 | "\n", 148 | "План такой: построить класс `torchtext.data.Dataset`, для него создать итератор, и учить модель.\n", 149 | "\n", 150 | "Данный датасет инициализируется двумя параметрами:\n", 151 | "```\n", 152 | " examples: List of Examples.\n", 153 | " fields (List(tuple(str, Field))): The Fields to use in this tuple. The\n", 154 | " string is a field name, and the Field is the associated field.\n", 155 | "```\n", 156 | "Разберемся сначала со вторым.\n", 157 | "\n", 158 | "`Field` - это такая мета-информация для датасета + обработчик сэмплов. \n", 159 | "\n", 160 | "Он имеет кучу параметров, на которые проще посмотреть [здесь](https://github.com/pytorch/text/blob/master/torchtext/data/field.py). Если коротко, то он может предобрабатывать (например, токенизировать) предложения, строить словарь (отображение из слова в индекс), строить батчи - добавлять паддинги и конвертировать в тензоры. Что ещё нужно в жизни?\n", 161 | "\n", 162 | "Мы будем делать character-level языковую модель, поэтому токенизация для нас - превращение строки в набор символов. Попросим также добавлять в начало и конец спец-символы `` и ``." 163 | ] 164 | }, 165 | { 166 | "metadata": { 167 | "id": "ilAMVxA8Xy4L", 168 | "colab_type": "code", 169 | "colab": {} 170 | }, 171 | "cell_type": "code", 172 | "source": [ 173 | "from torchtext.data import Field\n", 174 | "\n", 175 | "text_field = Field(init_token='', eos_token='', lower=True, tokenize=lambda line: list(line))" 176 | ], 177 | "execution_count": 0, 178 | "outputs": [] 179 | }, 180 | { 181 | "metadata": { 182 | "id": "L_i0Z6JhF0rA", 183 | "colab_type": "text" 184 | }, 185 | "cell_type": "markdown", 186 | "source": [ 187 | "Препроцессинг будет выглядеть так:" 188 | ] 189 | }, 190 | { 191 | "metadata": { 192 | "id": "B-8IPlPHFyKa", 193 | "colab_type": "code", 194 | "colab": {} 195 | }, 196 | "cell_type": "code", 197 | "source": [ 198 | "text_field.preprocess(data.text.iloc[0])" 199 | ], 200 | "execution_count": 0, 201 | "outputs": [] 202 | }, 203 | { 204 | "metadata": { 205 | "id": "19NFhTSNF1_1", 206 | "colab_type": "text" 207 | }, 208 | "cell_type": "markdown", 209 | "source": [ 210 | "Сконвертируем всё и посмотрим на распределение длин:" 211 | ] 212 | }, 213 | { 214 | "metadata": { 215 | "id": "wz1QnivMBmU3", 216 | "colab_type": "code", 217 | "colab": {} 218 | }, 219 | "cell_type": "code", 220 | "source": [ 221 | "import matplotlib.pyplot as plt\n", 222 | "%matplotlib inline\n", 223 | "\n", 224 | "data['text'] = data['text'].fillna('')\n", 225 | "lines = data.apply(lambda row: text_field.preprocess(row['text']), axis=1).tolist()\n", 226 | "\n", 227 | "lengths = [len(line) for line in lines]\n", 228 | "\n", 229 | "plt.hist(lengths, bins=30)[-1]" 230 | ], 231 | "execution_count": 0, 232 | "outputs": [] 233 | }, 234 | { 235 | "metadata": { 236 | "id": "dE9rPW9UHE7d", 237 | "colab_type": "text" 238 | }, 239 | "cell_type": "markdown", 240 | "source": [ 241 | "Отсечем слишком короткие строки и преобразуем оставшиеся в `Example`'ы:" 242 | ] 243 | }, 244 | { 245 | "metadata": { 246 | "id": "jfTlpBxODBg8", 247 | "colab_type": "code", 248 | "colab": {} 249 | }, 250 | "cell_type": "code", 251 | "source": [ 252 | "from torchtext.data import Example\n", 253 | "\n", 254 | "lines = [line for line in lines if len(line) >= 50]\n", 255 | "\n", 256 | "fields = [('text', text_field)]\n", 257 | "examples = [Example.fromlist([line], fields) for line in lines]" 258 | ], 259 | "execution_count": 0, 260 | "outputs": [] 261 | }, 262 | { 263 | "metadata": { 264 | "id": "7z1wPlz_HeEP", 265 | "colab_type": "text" 266 | }, 267 | "cell_type": "markdown", 268 | "source": [ 269 | "По `Example` можно получить обратно все поля, которые мы туда запихнули. Например, сейчас мы создали одно поле `text`:" 270 | ] 271 | }, 272 | { 273 | "metadata": { 274 | "id": "iGMRSuk_HYCm", 275 | "colab_type": "code", 276 | "colab": {} 277 | }, 278 | "cell_type": "code", 279 | "source": [ 280 | "examples[0].text" 281 | ], 282 | "execution_count": 0, 283 | "outputs": [] 284 | }, 285 | { 286 | "metadata": { 287 | "id": "Yef1bv2MQcEA", 288 | "colab_type": "text" 289 | }, 290 | "cell_type": "markdown", 291 | "source": [ 292 | "Построим, наконец, датасет:" 293 | ] 294 | }, 295 | { 296 | "metadata": { 297 | "id": "gSccEmVIHAaQ", 298 | "colab_type": "code", 299 | "colab": {} 300 | }, 301 | "cell_type": "code", 302 | "source": [ 303 | "from torchtext.data import Dataset\n", 304 | "\n", 305 | "dataset = Dataset(examples, fields)" 306 | ], 307 | "execution_count": 0, 308 | "outputs": [] 309 | }, 310 | { 311 | "metadata": { 312 | "id": "vEe5YXIpRCYD", 313 | "colab_type": "text" 314 | }, 315 | "cell_type": "markdown", 316 | "source": [ 317 | "Датасет можно разбить на части:" 318 | ] 319 | }, 320 | { 321 | "metadata": { 322 | "id": "21whmJDFRBV1", 323 | "colab_type": "code", 324 | "colab": {} 325 | }, 326 | "cell_type": "code", 327 | "source": [ 328 | "train_dataset, test_dataset = dataset.split(split_ratio=0.75)" 329 | ], 330 | "execution_count": 0, 331 | "outputs": [] 332 | }, 333 | { 334 | "metadata": { 335 | "id": "14CyhugSQsOf", 336 | "colab_type": "text" 337 | }, 338 | "cell_type": "markdown", 339 | "source": [ 340 | "По нему можно построить словарь:" 341 | ] 342 | }, 343 | { 344 | "metadata": { 345 | "id": "NQs3jbhyQkJD", 346 | "colab_type": "code", 347 | "colab": {} 348 | }, 349 | "cell_type": "code", 350 | "source": [ 351 | "text_field.build_vocab(train_dataset, min_freq=30)\n", 352 | "\n", 353 | "print('Vocab size =', len(text_field.vocab))\n", 354 | "print(text_field.vocab.itos)" 355 | ], 356 | "execution_count": 0, 357 | "outputs": [] 358 | }, 359 | { 360 | "metadata": { 361 | "id": "-_EAdgsWRTzj", 362 | "colab_type": "text" 363 | }, 364 | "cell_type": "markdown", 365 | "source": [ 366 | "Наконец, по нему можно итерироваться:" 367 | ] 368 | }, 369 | { 370 | "metadata": { 371 | "id": "qaEMoxdVG98p", 372 | "colab_type": "code", 373 | "colab": {} 374 | }, 375 | "cell_type": "code", 376 | "source": [ 377 | "from torchtext.data import BucketIterator\n", 378 | "\n", 379 | "train_iter, test_iter = BucketIterator.splits(datasets=(train_dataset, test_dataset), batch_sizes=(32, 128), \n", 380 | " shuffle=True, device=DEVICE, sort=False)" 381 | ], 382 | "execution_count": 0, 383 | "outputs": [] 384 | }, 385 | { 386 | "metadata": { 387 | "id": "RMG4L1-5RXnb", 388 | "colab_type": "code", 389 | "colab": {} 390 | }, 391 | "cell_type": "code", 392 | "source": [ 393 | "batch = next(iter(train_iter))\n", 394 | "\n", 395 | "batch" 396 | ], 397 | "execution_count": 0, 398 | "outputs": [] 399 | }, 400 | { 401 | "metadata": { 402 | "id": "7ZZgplOkReeq", 403 | "colab_type": "code", 404 | "colab": {} 405 | }, 406 | "cell_type": "code", 407 | "source": [ 408 | "batch.text" 409 | ], 410 | "execution_count": 0, 411 | "outputs": [] 412 | }, 413 | { 414 | "metadata": { 415 | "id": "oTrSUkqEhZzh", 416 | "colab_type": "text" 417 | }, 418 | "cell_type": "markdown", 419 | "source": [ 420 | "## Перплексия" 421 | ] 422 | }, 423 | { 424 | "metadata": { 425 | "id": "gqc9HpTM-FwD", 426 | "colab_type": "text" 427 | }, 428 | "cell_type": "markdown", 429 | "source": [ 430 | "Нашу задачу, как всегда, нужно начинать с двух вопросов - какую метрику оптимизируем и какой бейзлайн.\n", 431 | "\n", 432 | "С метрикой всё просто - мы хотим, чтобы модель как можно лучше умела приближать распределение слов языка. Всего языка у нас нету, поэтому обойдёмся тестовой выборкой.\n", 433 | "\n", 434 | "На ней можно посчитать кросс-энтропийные потери: \n", 435 | "$$H(w_1, \\ldots, w_n) = - \\frac 1n \\sum_k \\log\\mathbf{P}(w_k | w_{k-1}, \\ldots, w_1).$$\n", 436 | "\n", 437 | "Здесь вероятность $\\mathbf{P}$ - это вероятность, оцененная нашей языковой моделью. Идеальная модель давала бы вероятность равную 1 для слов в тексте и потери были бы нулевыми - хотя это, конечно, невозможно, даже вы же не можете предсказать следующее слово, что уж про бездушную машину говорить.\n", 438 | "\n", 439 | "Таким образом, всё как всегда - оптимизируем кросс-энтропию и стремимся сделать её как можно ниже.\n", 440 | "\n", 441 | "Ну, почти всё. Ещё есть отдельная метрика для языковых моделей - *перплексия*. Это просто возведенные в экспоненту кросс-энтропийные потери:\n", 442 | "\n", 443 | "$$PP(w_1, \\ldots, w_n) = e^{H(w_1, \\ldots, w_n)} = e^{- \\frac 1n \\sum_k \\log\\mathbf{P}(w_k | w_{k-1}, \\ldots, w_1)} = \\left(\\mathbf{P}(w_1, \\ldots, w_n) \\right)^{-\\frac 1n}.$$\n", 444 | "\n", 445 | "У её измерения есть некоторый сакральный смысл кроме банальной интепретируемости: представим модель, предсказывающую слова из словаря равновероятно вне зависимости от контекста. Для неё $\\mathbf{P}(w) = \\frac 1 N$, где $N$ — размер словаря, а перплексия будет равна размеру словаря — $N$. Конечно, это совершенно глупая модель, но оглядываясь на неё, можно трактовать перплексию реальных моделей как уровень неоднозначности генерации слова.\n", 446 | "\n", 447 | "Скажем, в модели с перплексией 100 выбор следующего слова также неоднозначен, как выбор из равномерного распределения среди 100 слов. И если такой перплексии удалось достичь на словаре в 100 000, получается, что удалось сократить эту неоднозначность на три порядка по сравнению с тупым рандомом." 448 | ] 449 | }, 450 | { 451 | "metadata": { 452 | "id": "xW8I0lKv9y1H", 453 | "colab_type": "text" 454 | }, 455 | "cell_type": "markdown", 456 | "source": [ 457 | "## Бейзлайн" 458 | ] 459 | }, 460 | { 461 | "metadata": { 462 | "id": "7wInBuBn-DIf", 463 | "colab_type": "text" 464 | }, 465 | "cell_type": "markdown", 466 | "source": [ 467 | "Вообще, бейзлайн тут тоже очень простой. Мы, на самом деле, даже смотрели его на курсе концепций: [N-граммная языковая модель](https://colab.research.google.com/drive/1lz9vO6Ue5zOiowEx0-koXNiejBrrnbj0). Можно подсчитывать вероятности N-грамм слов по частотностям их появления в обучающем корпусе. А дальше использовать аппроксимацию $\\mathbf{P}(w_k|w_1, \\ldots, w_{k-1}) \\approx \\mathbf{P}(w_k|w_{k-1}, \\ldots, w_{k-N + 1})$.\n", 468 | "\n", 469 | "Применим лучше сеточки для реализации того же.\n", 470 | "\n", 471 | "![](https://image.ibb.co/buMnLf/2018-10-22-00-22-56.png =x450) \n", 472 | "*From cs224n, Lecture 8 [pdf](http://web.stanford.edu/class/cs224n/lectures/lecture8.pdf)*\n", 473 | "\n", 474 | "На вход приходит последовательность слов, они эмбеддятся, а дальше с помощью выходного слоя считается наиболее вероятное следующее слово.\n", 475 | "\n", 476 | "Стоп... Но мы же уже реализовывали такое! В Word2vec CBoW модели мы по контексту предсказывали центральное слово - единственное отличие в том, что теперь мы имеем только левый контекст. Значит, всё, идём к следующей модели?\n", 477 | "\n", 478 | "Нет! Тут ещё есть с чем развлечься. В Word2vec мы формировали батчи таким образом:\n", 479 | "![](https://image.ibb.co/bs3wgV/training-data.png =x350) \n", 480 | "*From [Word2Vec Tutorial - The Skip-Gram Model, Chris McCormic](http://mccormickml.com/2016/04/19/word2vec-tutorial-the-skip-gram-model/)*\n", 481 | "\n", 482 | "То есть нарезали из текста набор пар <контекст, слово> (и как-то их использовали в зависимости от метода).\n", 483 | "\n", 484 | "Это нерационально - каждое слово повторяется много раз. Но можно использовать сверточные сети - они за нас применят операцию умножения на $W$ к каждому окну. В результате размер входного батча будет сильно меньше.\n", 485 | "\n", 486 | "Чтобы правильно всё обработать, нужно добавить паддинг в начало последовательности размером `window_size - 1` - тогда первое слово будет предсказываться по `...`.\n", 487 | "\n", 488 | "**Задание** Реализуйте языковую модель с фиксированным окном." 489 | ] 490 | }, 491 | { 492 | "metadata": { 493 | "colab_type": "code", 494 | "id": "A-tn_Gmi3pU0", 495 | "colab": {} 496 | }, 497 | "cell_type": "code", 498 | "source": [ 499 | "class ConvLM(nn.Module):\n", 500 | " def __init__(self, vocab_size, window_size=5, emb_dim=16, filters_count=128):\n", 501 | " super().__init__()\n", 502 | " \n", 503 | " self._window_size = window_size\n", 504 | " \n", 505 | " \n", 506 | " \n", 507 | " def forward(self, inputs):\n", 508 | " \n", 509 | " \n", 510 | " return output, None # hacky way to use training cycle for RNN and Conv simultaneously" 511 | ], 512 | "execution_count": 0, 513 | "outputs": [] 514 | }, 515 | { 516 | "metadata": { 517 | "id": "bjpOLKBH5yS5", 518 | "colab_type": "text" 519 | }, 520 | "cell_type": "markdown", 521 | "source": [ 522 | "Проверим, что оно работает:" 523 | ] 524 | }, 525 | { 526 | "metadata": { 527 | "id": "ks_RTZ14nMRz", 528 | "colab_type": "code", 529 | "colab": {} 530 | }, 531 | "cell_type": "code", 532 | "source": [ 533 | "model = ConvLM(vocab_size=len(train_iter.dataset.fields['text'].vocab)).to(DEVICE)\n", 534 | "\n", 535 | "model(batch.text)" 536 | ], 537 | "execution_count": 0, 538 | "outputs": [] 539 | }, 540 | { 541 | "metadata": { 542 | "id": "Lb_2VTBW5v_7", 543 | "colab_type": "text" 544 | }, 545 | "cell_type": "markdown", 546 | "source": [ 547 | "**Задание** Реализуйте функцию для сэмплирования последовательности из языковой модели." 548 | ] 549 | }, 550 | { 551 | "metadata": { 552 | "id": "0oUg0BjV2JjE", 553 | "colab_type": "code", 554 | "colab": {} 555 | }, 556 | "cell_type": "code", 557 | "source": [ 558 | "def sample(probs, temp):\n", 559 | " probs = F.log_softmax(probs.squeeze(), dim=0)\n", 560 | " probs = (probs / temp).exp()\n", 561 | " probs /= probs.sum()\n", 562 | " probs = probs.cpu().numpy()\n", 563 | "\n", 564 | " return np.random.choice(np.arange(len(probs)), p=probs)\n", 565 | "\n", 566 | "\n", 567 | "def generate(model, temp=0.7):\n", 568 | " model.eval()\n", 569 | " \n", 570 | " history = [train_dataset.fields['text'].vocab.stoi['']]\n", 571 | " \n", 572 | " with torch.no_grad():\n", 573 | " for _ in range(150):\n", 574 | " \n", 575 | "\n", 576 | "generate(model)" 577 | ], 578 | "execution_count": 0, 579 | "outputs": [] 580 | }, 581 | { 582 | "metadata": { 583 | "id": "CXuN871a852l", 584 | "colab_type": "text" 585 | }, 586 | "cell_type": "markdown", 587 | "source": [ 588 | "**Задание** Мы до сих пор не задали ни какой target. А предсказывать нам будет нужно следующие слова - то есть просто сдвинутый на 1 входной тензор. Реализуйте построение target'а и подсчет потерь." 589 | ] 590 | }, 591 | { 592 | "metadata": { 593 | "id": "CGLkcXARjhTM", 594 | "colab_type": "code", 595 | "colab": {} 596 | }, 597 | "cell_type": "code", 598 | "source": [ 599 | "import math\n", 600 | "from tqdm import tqdm\n", 601 | "\n", 602 | "\n", 603 | "def do_epoch(model, criterion, data_iter, unk_idx, pad_idx, optimizer=None, name=None):\n", 604 | " epoch_loss = 0\n", 605 | " \n", 606 | " is_train = not optimizer is None\n", 607 | " name = name or ''\n", 608 | " model.train(is_train)\n", 609 | " \n", 610 | " batches_count = len(data_iter)\n", 611 | " \n", 612 | " with torch.autograd.set_grad_enabled(is_train):\n", 613 | " with tqdm(total=batches_count) as progress_bar:\n", 614 | " for i, batch in enumerate(data_iter): \n", 615 | " logits, _ = model(batch.text)\n", 616 | "\n", 617 | " \n", 618 | " \n", 619 | " epoch_loss += loss.item()\n", 620 | "\n", 621 | " if optimizer:\n", 622 | " optimizer.zero_grad()\n", 623 | " loss.backward()\n", 624 | " nn.utils.clip_grad_norm_(model.parameters(), 1.)\n", 625 | " optimizer.step()\n", 626 | "\n", 627 | " progress_bar.update()\n", 628 | " progress_bar.set_description('{:>5s} Loss = {:.5f}, PPX = {:.2f}'.format(name, loss.item(), \n", 629 | " math.exp(loss.item())))\n", 630 | " \n", 631 | " progress_bar.set_description('{:>5s} Loss = {:.5f}, PPX = {:.2f}'.format(\n", 632 | " name, epoch_loss / batches_count, math.exp(epoch_loss / batches_count))\n", 633 | " )\n", 634 | "\n", 635 | " return epoch_loss / batches_count\n", 636 | "\n", 637 | "\n", 638 | "def fit(model, criterion, optimizer, train_iter, epochs_count=1, unk_idx=0, pad_idx=1, val_iter=None):\n", 639 | " for epoch in range(epochs_count):\n", 640 | " name_prefix = '[{} / {}] '.format(epoch + 1, epochs_count)\n", 641 | " train_loss = do_epoch(model, criterion, train_iter, unk_idx, pad_idx, optimizer, name_prefix + 'Train:')\n", 642 | " \n", 643 | " if not val_iter is None:\n", 644 | " val_loss = do_epoch(model, criterion, val_iter, unk_idx, pad_idx, None, name_prefix + ' Val:')\n", 645 | "\n", 646 | " generate(model)" 647 | ], 648 | "execution_count": 0, 649 | "outputs": [] 650 | }, 651 | { 652 | "metadata": { 653 | "id": "LIj0Lcdh9UJy", 654 | "colab_type": "code", 655 | "colab": {} 656 | }, 657 | "cell_type": "code", 658 | "source": [ 659 | "model = ConvLM(vocab_size=len(train_iter.dataset.fields['text'].vocab)).to(DEVICE)\n", 660 | "\n", 661 | "pad_idx = train_iter.dataset.fields['text'].vocab.stoi['']\n", 662 | "unk_idx = train_iter.dataset.fields['text'].vocab.stoi['']\n", 663 | "criterion = nn.CrossEntropyLoss(reduction='none').to(DEVICE)\n", 664 | "\n", 665 | "optimizer = optim.Adam(model.parameters())\n", 666 | "\n", 667 | "fit(model, criterion, optimizer, train_iter, epochs_count=30, unk_idx=unk_idx, pad_idx=pad_idx, val_iter=test_iter)" 668 | ], 669 | "execution_count": 0, 670 | "outputs": [] 671 | }, 672 | { 673 | "metadata": { 674 | "id": "FycAd6MWMvYy", 675 | "colab_type": "text" 676 | }, 677 | "cell_type": "markdown", 678 | "source": [ 679 | "**Задание** Чтобы отучить модель сэмплировать `` можно явным образом запрещать это в сэплирующей функции - а можно просто не учить ее на них. Реализуйте маскинг по одновременно и паддингам, и неизвестным словам." 680 | ] 681 | }, 682 | { 683 | "metadata": { 684 | "id": "rQJKn1Uw94_0", 685 | "colab_type": "text" 686 | }, 687 | "cell_type": "markdown", 688 | "source": [ 689 | "## Рекуррентная языковая модель" 690 | ] 691 | }, 692 | { 693 | "metadata": { 694 | "id": "HeSojPwh_ZSS", 695 | "colab_type": "text" 696 | }, 697 | "cell_type": "markdown", 698 | "source": [ 699 | "Очевидно, хочется использовать не фиксированное окно истории, а всю информацию об уже сгенерированном. Как минимум, хочется знать, когда у нас лимит символов в твите подошел. \n", 700 | "Для этого используют рекуррентные языковые модели:\n", 701 | "\n", 702 | "![](https://hsto.org/web/dc1/7c2/c4e/dc17c2c4e9ac434eb5346ada2c412c9a.png =x250)\n", 703 | "\n", 704 | "Сети на вход передается предыдующий токен, а также предыдущее состояние RNN. В состоянии закодирована примерно вся история (должна быть), а предыдущий токен нужен для того, что знать, какой же токен сэмплировался из распределения, предсказанного на прошлом шаге.\n", 705 | "\n", 706 | "**Задание** Мы уже несколько раз так делали - реализуйте снова сеть, которая будет заниматься языковым моделированием." 707 | ] 708 | }, 709 | { 710 | "metadata": { 711 | "id": "x8ndCRZLl4ZZ", 712 | "colab_type": "code", 713 | "colab": {} 714 | }, 715 | "cell_type": "code", 716 | "source": [ 717 | "class RnnLM(nn.Module):\n", 718 | " def __init__(self, vocab_size, emb_dim=16, lstm_hidden_dim=128, num_layers=1):\n", 719 | " super().__init__()\n", 720 | "\n", 721 | " self._emb = nn.Embedding(vocab_size, emb_dim)\n", 722 | " self._rnn = nn.LSTM(input_size=emb_dim, hidden_size=lstm_hidden_dim)\n", 723 | " self._out_layer = nn.Linear(lstm_hidden_dim, vocab_size)\n", 724 | "\n", 725 | " def forward(self, inputs, hidden=None):\n", 726 | " \n", 727 | " return output, hidden" 728 | ], 729 | "execution_count": 0, 730 | "outputs": [] 731 | }, 732 | { 733 | "metadata": { 734 | "id": "H3MjLgDKBNsD", 735 | "colab_type": "text" 736 | }, 737 | "cell_type": "markdown", 738 | "source": [ 739 | "**Задание** Реализуйте функцию для сэмплирования предложений из модели." 740 | ] 741 | }, 742 | { 743 | "metadata": { 744 | "id": "ZJSXu_Pr_kYL", 745 | "colab_type": "code", 746 | "colab": {} 747 | }, 748 | "cell_type": "code", 749 | "source": [ 750 | "def generate(model, temp=0.8):\n", 751 | " model.eval()\n", 752 | " with torch.no_grad():\n", 753 | " prev_token = train_iter.dataset.fields['text'].vocab.stoi['']\n", 754 | " end_token = train_iter.dataset.fields['text'].vocab.stoi['']\n", 755 | " \n", 756 | " hidden = None\n", 757 | " for _ in range(150):\n", 758 | " \n", 759 | "\n", 760 | "generate(model)" 761 | ], 762 | "execution_count": 0, 763 | "outputs": [] 764 | }, 765 | { 766 | "metadata": { 767 | "id": "cibfrMxo_Gjg", 768 | "colab_type": "code", 769 | "colab": {} 770 | }, 771 | "cell_type": "code", 772 | "source": [ 773 | "model = RnnLM(vocab_size=len(train_iter.dataset.fields['text'].vocab)).to(DEVICE)\n", 774 | "\n", 775 | "pad_idx = train_iter.dataset.fields['text'].vocab.stoi['']\n", 776 | "unk_idx = train_iter.dataset.fields['text'].vocab.stoi['']\n", 777 | "criterion = nn.CrossEntropyLoss(reduction='none').to(DEVICE)\n", 778 | "\n", 779 | "optimizer = optim.Adam(model.parameters())\n", 780 | "\n", 781 | "fit(model, criterion, optimizer, train_iter, epochs_count=30, unk_idx=unk_idx, pad_idx=pad_idx, val_iter=test_iter)" 782 | ], 783 | "execution_count": 0, 784 | "outputs": [] 785 | }, 786 | { 787 | "metadata": { 788 | "id": "8cCcKrWjBzCp", 789 | "colab_type": "text" 790 | }, 791 | "cell_type": "markdown", 792 | "source": [ 793 | "## Улучшения модели\n", 794 | "\n", 795 | "### Оптимизатор\n", 796 | "\n", 797 | "Мы использовали только `Adam` до сих пор. Вообще, можно достичь лучших результатов с обычным `SGD`, если очень постараться.\n", 798 | " \n", 799 | "**Задание** Замените оптимизатор на `optim.SGD(model.parameters(), lr=20., weight_decay=1e-6)`. Например. Или другими параметрами на выбор.\n", 800 | "\n", 801 | "### Dropout\n", 802 | "\n", 803 | "Вспомним, что такое dropout.\n", 804 | "\n", 805 | "По сути это умножение случайно сгенерированной маски из нулей и единиц на входной вектор (+ нормировка).\n", 806 | "\n", 807 | "Например, для слоя Dropout(p):\n", 808 | "\n", 809 | "$$m = \\frac1{1-p} \\cdot \\text{Bernouli}(1 - p)$$\n", 810 | "$$\\tilde h = m \\odot h $$\n", 811 | "\n", 812 | "В рекуррентных сетях долго не могли прикрутить dropout. Делать это пытались, генерируя случайную маску: \n", 813 | "![A Theoretically Grounded Application of Dropout in Recurrent Neural Networks](https://cdn-images-1.medium.com/max/800/1*g4Q37g7mlizEty7J1b64uw.png =x300) \n", 814 | "from [A Theoretically Grounded Application of Dropout in Recurrent Neural Networks](https://arxiv.org/abs/1512.05287)\n", 815 | "\n", 816 | "Оказалось, правильнее делать маску фиксированную: для каждого шага должны зануляться одни и те же элементы.\n", 817 | "\n", 818 | "Для pytorch нет нормального встроенного variational dropout в LSTM. Зато есть [AWD-LSTM](https://github.com/salesforce/awd-lstm-lm).\n", 819 | "\n", 820 | "Советую посмотреть обзор разных способов применения dropout'а в рекуррентных сетях: [Dropout in Recurrent Networks — Part 1](https://becominghuman.ai/learning-note-dropout-in-recurrent-networks-part-1-57a9c19a2307) (в конце - ссылки на Part 2 и 3).\n", 821 | "\n", 822 | "**Задание** Реализуйте вариационный dropout. Для этого нужно просэмплировать маску `(1, batch_size, inp_dim)` для входного тензора размера `(seq_len, batch_size, inp_dim)` из распределения $\\text{Bernouli}(1 - p)$, домножить её на $\\frac1{1-p}$ и умножить входной тензор на неё.\n", 823 | "\n", 824 | "Благодаря broadcasting каждый timestamp из входного тензора домножится на одну и ту же маску - и должно быть счастье.\n", 825 | "\n", 826 | "Хотя лучше сравнить с обычным `nn.Dropout`, вдруг разница не будет заметна." 827 | ] 828 | }, 829 | { 830 | "metadata": { 831 | "id": "aDv4nutY-WOw", 832 | "colab_type": "code", 833 | "colab": {} 834 | }, 835 | "cell_type": "code", 836 | "source": [ 837 | "class LockedDropout(nn.Module):\n", 838 | " def __init__(self):\n", 839 | " super().__init__()\n", 840 | "\n", 841 | " def forward(self, inputs, dropout=0.5):\n", 842 | " if not self.training or not dropout:\n", 843 | " return inputs\n", 844 | " \n", 845 | " " 846 | ], 847 | "execution_count": 0, 848 | "outputs": [] 849 | }, 850 | { 851 | "metadata": { 852 | "id": "9m-InMeoIiCA", 853 | "colab_type": "text" 854 | }, 855 | "cell_type": "markdown", 856 | "source": [ 857 | "## Условная генерация" 858 | ] 859 | }, 860 | { 861 | "metadata": { 862 | "id": "J7aB2_YxIl-c", 863 | "colab_type": "text" 864 | }, 865 | "cell_type": "markdown", 866 | "source": [ 867 | "Мы уже классифицировали фамилии по языкам. Научимся теперь генерировать фамилию при заданном языке.\n", 868 | "\n", 869 | "Воспользуемся наследником `Dataset` - `TabularDataset`:" 870 | ] 871 | }, 872 | { 873 | "metadata": { 874 | "id": "Wa5benKoJMfc", 875 | "colab_type": "code", 876 | "colab": {} 877 | }, 878 | "cell_type": "code", 879 | "source": [ 880 | "from torchtext.data import TabularDataset\n", 881 | "\n", 882 | "name_field = Field(init_token='', eos_token='', lower=True, tokenize=lambda line: list(line))\n", 883 | "lang_field = Field(sequential=False)\n", 884 | "\n", 885 | "dataset = TabularDataset(\n", 886 | " path='surnames.txt', format='tsv', \n", 887 | " skip_header=True,\n", 888 | " fields=[\n", 889 | " ('name', name_field),\n", 890 | " ('lang', lang_field)\n", 891 | " ]\n", 892 | ")\n", 893 | "\n", 894 | "name_field.build_vocab(dataset)\n", 895 | "lang_field.build_vocab(dataset)\n", 896 | "\n", 897 | "print(name_field.vocab.itos)\n", 898 | "print(lang_field.vocab.itos)" 899 | ], 900 | "execution_count": 0, 901 | "outputs": [] 902 | }, 903 | { 904 | "metadata": { 905 | "id": "qp3SZHAsK85C", 906 | "colab_type": "text" 907 | }, 908 | "cell_type": "markdown", 909 | "source": [ 910 | "Разобьем датасет:" 911 | ] 912 | }, 913 | { 914 | "metadata": { 915 | "id": "kh-KKh08J5Oq", 916 | "colab_type": "code", 917 | "colab": {} 918 | }, 919 | "cell_type": "code", 920 | "source": [ 921 | "train_dataset, val_dataset = dataset.split(split_ratio=0.25, stratified=True, strata_field='lang')" 922 | ], 923 | "execution_count": 0, 924 | "outputs": [] 925 | }, 926 | { 927 | "metadata": { 928 | "id": "nIzaiUKDK_PG", 929 | "colab_type": "text" 930 | }, 931 | "cell_type": "markdown", 932 | "source": [ 933 | "**Задание** Сделать языковую модель, которая принимает как предыдующий сгенерированный символ, так и индекс языка, к которому это слово относится. Строить эмбеддинги для символа и для языка, конкатенировать их - а дальше всё то же самое.\n", 934 | "\n", 935 | "Нужно обучить эту модель и написать функцию-генератор фамилий при заданном языке." 936 | ] 937 | }, 938 | { 939 | "metadata": { 940 | "id": "s6LnEoU9LNlZ", 941 | "colab_type": "code", 942 | "colab": {} 943 | }, 944 | "cell_type": "code", 945 | "source": [ 946 | "" 947 | ], 948 | "execution_count": 0, 949 | "outputs": [] 950 | }, 951 | { 952 | "metadata": { 953 | "id": "9VfdL29AELhu", 954 | "colab_type": "text" 955 | }, 956 | "cell_type": "markdown", 957 | "source": [ 958 | "# In the wild" 959 | ] 960 | }, 961 | { 962 | "metadata": { 963 | "id": "GDqxGVo5EOfb", 964 | "colab_type": "text" 965 | }, 966 | "cell_type": "markdown", 967 | "source": [ 968 | "Применим свои знания к боевой задаче: [Kaggle Toxic Comment Classification Challenge](https://www.kaggle.com/c/jigsaw-toxic-comment-classification-challenge/).\n", 969 | "\n", 970 | "Она про классификацию сообщений по нескольким категориям. Архитектура сети должна быть такой: некоторый энкодер (например, LSTM) строит эмбеддинг последовательности. Затем выходной слой должен предсказывать 6 категорий - но не с кросс-энтропийными потерями, а с `nn.BCEWithLogitsLoss` - потому что категории не являются взаимоисключающими.\n", 971 | "\n", 972 | "Совет: разберитесь с токенизацией, которую умеет `Field`. Скачайте предобученные словные эмбеддинги, как мы делали. Постройте сеть и напишите цикл обучения для неё.\n", 973 | "\n", 974 | "**Задание** Скачать данные с kaggle, потренировать что-нибудь и сделать посылку." 975 | ] 976 | }, 977 | { 978 | "metadata": { 979 | "id": "8obdAs_E0zRb", 980 | "colab_type": "text" 981 | }, 982 | "cell_type": "markdown", 983 | "source": [ 984 | "# Дополнительные материалы\n", 985 | "\n", 986 | "## Блоги\n", 987 | "\n", 988 | "[A Friendly Introduction to Cross-Entropy Loss, Rob DiPietro](https://rdipietro.github.io/friendly-intro-to-cross-entropy-loss/)\n", 989 | "\n", 990 | "[A Tutorial on Torchtext, Allen Nie](http://anie.me/On-Torchtext/)\n", 991 | "\n", 992 | "[Dropout in Recurrent Networks, Ceshine Lee](https://becominghuman.ai/learning-note-dropout-in-recurrent-networks-part-1-57a9c19a2307)\n", 993 | "\n", 994 | "[The Unreasonable Effectiveness of Recurrent Neural Networks, Andrej Karpathy](http://karpathy.github.io/2015/05/21/rnn-effectiveness/)\n", 995 | "\n", 996 | "[The unreasonable effectiveness of Character-level Language Models, Yoav Goldberg](http://nbviewer.jupyter.org/gist/yoavg/d76121dfde2618422139)\n", 997 | "\n", 998 | "[Unsupervised Sentiment Neuron, OpenAI](https://blog.openai.com/unsupervised-sentiment-neuron/)\n", 999 | "\n", 1000 | "[Как научить свою нейросеть генерировать стихи](https://habr.com/post/334046/)\n", 1001 | "\n", 1002 | "## Видео\n", 1003 | "[cs224n, \"Lecture 8: Recurrent Neural Networks and Language Models\"](https://www.youtube.com/watch?v=Keqep_PKrY8)\n", 1004 | "\n", 1005 | "[Oxford Deep NLP, \"Language Modelling and RNNs\"](https://github.com/oxford-cs-deepnlp-2017/lectures#5-lecture-3---language-modelling-and-rnns-part-1-phil-blunsom)" 1006 | ] 1007 | }, 1008 | { 1009 | "metadata": { 1010 | "id": "WJVDoh5MLcdB", 1011 | "colab_type": "text" 1012 | }, 1013 | "cell_type": "markdown", 1014 | "source": [ 1015 | "# Сдача\n", 1016 | "\n", 1017 | "[Опрос для сдачи](https://goo.gl/forms/8bjGv7LLWUrwOUrt2)\n", 1018 | "\n", 1019 | "[Feedback](https://goo.gl/forms/PR76tYmvzMugIFID2)" 1020 | ] 1021 | } 1022 | ] 1023 | } -------------------------------------------------------------------------------- /Week 11/Week_11_Transformers.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "Week 11 - Transformers.ipynb", 7 | "version": "0.3.2", 8 | "provenance": [], 9 | "collapsed_sections": [] 10 | }, 11 | "kernelspec": { 12 | "name": "python3", 13 | "display_name": "Python 3" 14 | }, 15 | "accelerator": "GPU" 16 | }, 17 | "cells": [ 18 | { 19 | "metadata": { 20 | "id": "OE7fXh-OSJYF", 21 | "colab_type": "code", 22 | "colab": {} 23 | }, 24 | "cell_type": "code", 25 | "source": [ 26 | "!pip3 -qq install torch==0.4.1\n", 27 | "!pip -qq install torchtext==0.3.1\n", 28 | "!pip install sacremoses==0.0.5\n", 29 | "!wget -O news.zip -qq --no-check-certificate \"https://drive.google.com/uc?export=download&id=1hIVVpBqM6VU4n3ERkKq4tFaH4sKN0Hab\"\n", 30 | "!unzip news.zip" 31 | ], 32 | "execution_count": 0, 33 | "outputs": [] 34 | }, 35 | { 36 | "metadata": { 37 | "id": "uhvfH55PUJ8K", 38 | "colab_type": "code", 39 | "colab": {} 40 | }, 41 | "cell_type": "code", 42 | "source": [ 43 | "import numpy as np\n", 44 | "\n", 45 | "import torch\n", 46 | "import torch.nn as nn\n", 47 | "import torch.nn.functional as F\n", 48 | "import torch.optim as optim\n", 49 | "\n", 50 | "import matplotlib.pyplot as plt\n", 51 | "%matplotlib inline\n", 52 | "\n", 53 | "\n", 54 | "if torch.cuda.is_available():\n", 55 | " from torch.cuda import FloatTensor, LongTensor\n", 56 | " DEVICE = torch.device('cuda')\n", 57 | "else:\n", 58 | " from torch import FloatTensor, LongTensor\n", 59 | " DEVICE = torch.device('cpu')\n", 60 | "\n", 61 | "np.random.seed(42)" 62 | ], 63 | "execution_count": 0, 64 | "outputs": [] 65 | }, 66 | { 67 | "metadata": { 68 | "id": "txWqIO_74A4s", 69 | "colab_type": "text" 70 | }, 71 | "cell_type": "markdown", 72 | "source": [ 73 | "# Abstactive Summarization" 74 | ] 75 | }, 76 | { 77 | "metadata": { 78 | "id": "gJ7JQJMO2R7z", 79 | "colab_type": "text" 80 | }, 81 | "cell_type": "markdown", 82 | "source": [ 83 | "Задача - по тексту сгенерировать выдержку из него.\n", 84 | "\n", 85 | "Например, попробуем по новостям генерировать заголовки:" 86 | ] 87 | }, 88 | { 89 | "metadata": { 90 | "id": "NzNGUFOcXMUs", 91 | "colab_type": "code", 92 | "colab": {} 93 | }, 94 | "cell_type": "code", 95 | "source": [ 96 | "!shuf -n 10 news.csv" 97 | ], 98 | "execution_count": 0, 99 | "outputs": [] 100 | }, 101 | { 102 | "metadata": { 103 | "id": "QOVlO5_Qlg5y", 104 | "colab_type": "text" 105 | }, 106 | "cell_type": "markdown", 107 | "source": [ 108 | "Токенизируем их. Будем использовать единый словарь для текста и заголовков." 109 | ] 110 | }, 111 | { 112 | "metadata": { 113 | "id": "fsOvtO0fpCHa", 114 | "colab_type": "code", 115 | "colab": {} 116 | }, 117 | "cell_type": "code", 118 | "source": [ 119 | "from torchtext.data import Field, Example, Dataset, BucketIterator\n", 120 | "\n", 121 | "BOS_TOKEN = ''\n", 122 | "EOS_TOKEN = ''\n", 123 | "\n", 124 | "word_field = Field(tokenize='moses', init_token=BOS_TOKEN, eos_token=EOS_TOKEN, lower=True)\n", 125 | "fields = [('source', word_field), ('target', word_field)]" 126 | ], 127 | "execution_count": 0, 128 | "outputs": [] 129 | }, 130 | { 131 | "metadata": { 132 | "id": "VO-gix7yoBjg", 133 | "colab_type": "code", 134 | "colab": {} 135 | }, 136 | "cell_type": "code", 137 | "source": [ 138 | "import pandas as pd\n", 139 | "from tqdm import tqdm\n", 140 | "\n", 141 | "data = pd.read_csv('news.csv', delimiter=',')\n", 142 | "\n", 143 | "examples = []\n", 144 | "for _, row in tqdm(data.iterrows(), total=len(data)):\n", 145 | " source_text = word_field.preprocess(row.text)\n", 146 | " target_text = word_field.preprocess(row.title)\n", 147 | " examples.append(Example.fromlist([source_text, target_text], fields))" 148 | ], 149 | "execution_count": 0, 150 | "outputs": [] 151 | }, 152 | { 153 | "metadata": { 154 | "id": "A8uCsMEglm6V", 155 | "colab_type": "text" 156 | }, 157 | "cell_type": "markdown", 158 | "source": [ 159 | "Построим датасеты:" 160 | ] 161 | }, 162 | { 163 | "metadata": { 164 | "id": "ZOBgLAgVTrk1", 165 | "colab_type": "code", 166 | "colab": {} 167 | }, 168 | "cell_type": "code", 169 | "source": [ 170 | "dataset = Dataset(examples, fields)\n", 171 | "\n", 172 | "train_dataset, test_dataset = dataset.split(split_ratio=0.85)\n", 173 | "\n", 174 | "print('Train size =', len(train_dataset))\n", 175 | "print('Test size =', len(test_dataset))\n", 176 | "\n", 177 | "word_field.build_vocab(train_dataset, min_freq=7)\n", 178 | "print('Vocab size =', len(word_field.vocab))\n", 179 | "\n", 180 | "train_iter, test_iter = BucketIterator.splits(\n", 181 | " datasets=(train_dataset, test_dataset), batch_sizes=(16, 32), shuffle=True, device=DEVICE, sort=False\n", 182 | ")" 183 | ], 184 | "execution_count": 0, 185 | "outputs": [] 186 | }, 187 | { 188 | "metadata": { 189 | "id": "tHQGgoit2ljv", 190 | "colab_type": "text" 191 | }, 192 | "cell_type": "markdown", 193 | "source": [ 194 | "## Seq2seq for Abstractive Summarization\n", 195 | "\n", 196 | "Вообще задача не сильно отличается от машинного перевода:\n", 197 | "\n", 198 | "![](https://image.ibb.co/jAf3S0/2018-11-20-9-42-17.png)\n", 199 | "*From [Get To The Point: Summarization with Pointer-Generator Networks](https://arxiv.org/pdf/1704.04368.pdf)*\n", 200 | "\n", 201 | "Тут на каждом шаге декодер подглядывает на все токены - точнее, их эмбеддинги после BiRNN.\n", 202 | "\n", 203 | "Возникает вопрос - а зачем вообще RNN, если потом все равно будем смотреть на всё." 204 | ] 205 | }, 206 | { 207 | "metadata": { 208 | "id": "8FYJe2CA8GcY", 209 | "colab_type": "text" 210 | }, 211 | "cell_type": "markdown", 212 | "source": [ 213 | "# Transformer\n", 214 | "\n", 215 | "Из этой идеи - отказ от RNN - и получился Transformer.\n", 216 | "\n", 217 | "![](https://hsto.org/webt/59/f0/44/59f04410c0e56192990801.png =x600) \n", 218 | "*From Attention is all you need*\n", 219 | "\n", 220 | "Как в случае с RNN мы на каждом шаге применяем одну и ту же операцию (ячейку LSTM) к текущему входу, так и здесь - только теперь связей между timestamp'ами нет и можно обрабатывать их почти параллельно.\n", 221 | "\n", 222 | "*Код дальше очень сильно опирается на шикарную статью [The Annotated Transformer](http://nlp.seas.harvard.edu/2018/04/03/attention.html).*" 223 | ] 224 | }, 225 | { 226 | "metadata": { 227 | "id": "env8rDS_dM86", 228 | "colab_type": "text" 229 | }, 230 | "cell_type": "markdown", 231 | "source": [ 232 | "## Encoder\n", 233 | "\n", 234 | "Начнем с энкодера:\n", 235 | "\n", 236 | "![](http://jalammar.github.io/images/t/transformer_resideual_layer_norm.png =x400) \n", 237 | "*From [Illustrated Transformer](http://jalammar.github.io/illustrated-transformer/)*\n", 238 | "\n", 239 | "Он представляет из себя последовательность одинаковых блоков с self-attention + полносвязными слоями.\n", 240 | "\n", 241 | "Можно представить, что это - ячейка LSTM: она тоже применяется к каждому входу с одинаковыми весами. Разница основная в отсутствии рекуррентных связей: за счет этого энкодер может применяться одновременно ко всем входам батча." 242 | ] 243 | }, 244 | { 245 | "metadata": { 246 | "id": "rXjWcnpCJY92", 247 | "colab_type": "text" 248 | }, 249 | "cell_type": "markdown", 250 | "source": [ 251 | "### Positional Encoding\n", 252 | "\n", 253 | "Нужно как-то кодировать информацию о том, в каком месте в предложении стоит токен. Чуваки предложили делать так:\n", 254 | "$$PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\\text{model}}})$$\n", 255 | "$$PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\\text{model}}})$$\n", 256 | "\n", 257 | "где $(pos, i)$ - позиция в предложении и индекс в скрытом векторе размерности до $d_{model}$." 258 | ] 259 | }, 260 | { 261 | "metadata": { 262 | "id": "zI2rMiZhJcKX", 263 | "colab_type": "code", 264 | "colab": {} 265 | }, 266 | "cell_type": "code", 267 | "source": [ 268 | "import math \n", 269 | "\n", 270 | "class PositionalEncoding(nn.Module):\n", 271 | " def __init__(self, d_model, dropout, max_len=5000):\n", 272 | " super().__init__()\n", 273 | " self.dropout = nn.Dropout(p=dropout)\n", 274 | " \n", 275 | " pe = torch.zeros(max_len, d_model)\n", 276 | " position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)\n", 277 | " div_term = torch.exp(torch.arange(0, d_model, 2, dtype=torch.float) * (-math.log(10000.0) / d_model))\n", 278 | " pe[:, 0::2] = torch.sin(position * div_term)\n", 279 | " pe[:, 1::2] = torch.cos(position * div_term)\n", 280 | " pe = pe.unsqueeze(0)\n", 281 | " self.register_buffer('pe', pe)\n", 282 | " \n", 283 | " def forward(self, x):\n", 284 | " x = x + self.pe[:, :x.size(1)]\n", 285 | " return self.dropout(x)" 286 | ], 287 | "execution_count": 0, 288 | "outputs": [] 289 | }, 290 | { 291 | "metadata": { 292 | "id": "XEL9VppyKCBz", 293 | "colab_type": "code", 294 | "colab": {} 295 | }, 296 | "cell_type": "code", 297 | "source": [ 298 | "plt.figure(figsize=(15, 5))\n", 299 | "pe = PositionalEncoding(20, 0)\n", 300 | "y = pe(torch.zeros(1, 100, 20))\n", 301 | "plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())\n", 302 | "plt.legend([\"dim %d\"%p for p in [4,5,6,7]])" 303 | ], 304 | "execution_count": 0, 305 | "outputs": [] 306 | }, 307 | { 308 | "metadata": { 309 | "id": "a7OeqTcOdgud", 310 | "colab_type": "text" 311 | }, 312 | "cell_type": "markdown", 313 | "source": [ 314 | "В итоге эмбеддинги токена получается как сумма обычного эмбеддинга и эмбеддинга позиции: \n", 315 | "![](http://jalammar.github.io/images/t/transformer_positional_encoding_vectors.png =x500) \n", 316 | "*From [Illustrated Transformer](http://jalammar.github.io/illustrated-transformer/)*" 317 | ] 318 | }, 319 | { 320 | "metadata": { 321 | "id": "9rvG4ZQ_icKH", 322 | "colab_type": "text" 323 | }, 324 | "cell_type": "markdown", 325 | "source": [ 326 | "### Residual Connection\n", 327 | "\n", 328 | "Разберем блок энкодера - повторяющейся N раз комбинации операций на первом рисунке.\n", 329 | "\n", 330 | "Самое простое здесь - residual connection. Вместо к выходу произвольной функции $F$ прибавляется её вход\n", 331 | "$$y = F(x) \\quad \\to \\quad y = F(x) + x$$\n", 332 | "\n", 333 | "Идея в том, что обычные сети сложно делать слишком глубокими - градиенты затухают. А через этот residual вход $x$ градиентам течь ничего не стоит. В итоге в картинках благодаря таким блокам получилось настакать дофига слоев и улучшить качество (см. ResNet).\n", 334 | "\n", 335 | "Ничего не мешает нам поступить также." 336 | ] 337 | }, 338 | { 339 | "metadata": { 340 | "id": "7QLCqNeEjKpD", 341 | "colab_type": "code", 342 | "colab": {} 343 | }, 344 | "cell_type": "code", 345 | "source": [ 346 | "class ResidualBlock(nn.Module):\n", 347 | " def __init__(self, size, dropout_rate):\n", 348 | " super().__init__()\n", 349 | " self._norm = LayerNorm(size)\n", 350 | " self._dropout = nn.Dropout(dropout_rate)\n", 351 | "\n", 352 | " def forward(self, inputs, sublayer):\n", 353 | " return inputs + self._dropout(sublayer(self._norm(inputs)))" 354 | ], 355 | "execution_count": 0, 356 | "outputs": [] 357 | }, 358 | { 359 | "metadata": { 360 | "id": "EHUqCsifmd5e", 361 | "colab_type": "text" 362 | }, 363 | "cell_type": "markdown", 364 | "source": [ 365 | "### Layer Norm\n", 366 | "\n", 367 | "Дополнительно применяется нормализация LayerNorm. \n", 368 | "\n", 369 | "**Batch normalization** \n", 370 | "Мы вообще не разбирали, но BatchNorm работает так:\n", 371 | "$$\\mu_j = \\frac{1}{m}\\sum_{i=1}^{m}x_{ij} \\\\ \\sigma_j^2 = \\frac{1}{m}\\sum_{i=1}^{m}(x_{ij} - \\mu_j)^2 \\\\ \\hat{x}_{ij} = \\frac{x_{ij} - \\mu_j}{\\sqrt{\\sigma_j^2 + \\epsilon}}$$\n", 372 | "$$y_{ij} = \\gamma \\ \\hat{x}_{ij} + \\beta$$\n", 373 | "\n", 374 | "На каждом батче эти $\\mu$ и $\\sigma$ пересчитываются, обновляя статистики. На инференсе используются накопленные статистики.\n", 375 | "\n", 376 | "Основной его недостаток - он плохо работает с рекуррентными сетями. Чтобы побороть это придумали:\n", 377 | "\n", 378 | "**Layer normalization** \n", 379 | "А сейчас мы будем пользоваться немного другими формулами:\n", 380 | "$$\\mu_i = \\frac{1}{m}\\sum_{j=1}^{m}x_{ij} \\\\ \\sigma_i^2 = \\frac{1}{m}\\sum_{j=1}^{m}(x_{ij} - \\mu_i)^2 \\\\ \\hat{x}_{ij} = \\frac{x_{ij} - \\mu_i}{\\sqrt{\\sigma_i^2 + \\epsilon}}$$\n", 381 | "$$y_{ij} = \\gamma \\ \\hat{x}_{ij} + \\beta$$\n", 382 | "\n", 383 | "Разницу с ходу не видно, но она есть:\n", 384 | "![](https://image.ibb.co/hjtuX0/layernorm.png) \n", 385 | "*From [Weight Normalization and Layer Normalization Explained ](http://mlexplained.com/2018/01/13/weight-normalization-and-layer-normalization-explained-normalization-in-deep-learning-part-2/)*\n", 386 | "\n", 387 | "Если в BatchNorm статистики считаются для каждой фичи усреднением по батчу, то теперь - для каждого входа усредением по фичам." 388 | ] 389 | }, 390 | { 391 | "metadata": { 392 | "id": "Q5XLZZ3zrK24", 393 | "colab_type": "code", 394 | "colab": {} 395 | }, 396 | "cell_type": "code", 397 | "source": [ 398 | "class LayerNorm(nn.Module):\n", 399 | " def __init__(self, features, eps=1e-6):\n", 400 | " super().__init__()\n", 401 | " \n", 402 | " self._gamma = nn.Parameter(torch.ones(features))\n", 403 | " self._beta = nn.Parameter(torch.zeros(features))\n", 404 | " self._eps = eps\n", 405 | "\n", 406 | " def forward(self, inputs):\n", 407 | " " 408 | ], 409 | "execution_count": 0, 410 | "outputs": [] 411 | }, 412 | { 413 | "metadata": { 414 | "id": "WrWvtymp2G8o", 415 | "colab_type": "text" 416 | }, 417 | "cell_type": "markdown", 418 | "source": [ 419 | "### Attention\n", 420 | "\n", 421 | "Весь Transformer опирается на идею self-attention. Выглядит это так:\n", 422 | "\n", 423 | "![](http://jalammar.github.io/images/t/transformer_self-attention_visualization.png) \n", 424 | "*From [Tensor2Tensor Tutorial](https://colab.research.google.com/github/tensorflow/tensor2tensor/blob/master/tensor2tensor/notebooks/hello_t2t.ipynb)*\n", 425 | "\n", 426 | "Эмбеддинг слова *it* строится как комбинация всех эмбеддингов предложения.\n", 427 | "\n", 428 | "В статье придумали делать такой аттеншен:\n", 429 | "\n", 430 | "$$\\mathrm{Attention}(Q, K, V) = \\mathrm{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V$$\n", 431 | "\n", 432 | "Это примерно как dot-attention на прошлом занятии: запрос (**Q**uery) умножается на ключи (**K**ey) скалярно, затем берется софтмакс - получаются оценки того, насколько интересны разные таймстемпы из значений (**V**alue). \n", 433 | "\n", 434 | "Например, $\\mathrm{emb}(\\text{it}) = \\mathrm{Attention}(\\text{it}, \\ldots\\text{because it was too tired}, \\ldots\\text{because it was too tired})$.\n", 435 | "\n", 436 | "Только теперь ещё с параметром $\\frac{1}{\\sqrt{d_k}}$, где $d_k$ - это размерность ключа. Утверждается, это работает лучше при больших размерностях ключа $d_k$." 437 | ] 438 | }, 439 | { 440 | "metadata": { 441 | "id": "2ApuVJZn5i4R", 442 | "colab_type": "code", 443 | "colab": {} 444 | }, 445 | "cell_type": "code", 446 | "source": [ 447 | "class ScaledDotProductAttention(nn.Module):\n", 448 | " def __init__(self, dropout_rate):\n", 449 | " super().__init__()\n", 450 | " \n", 451 | " self._dropout = nn.Dropout(dropout_rate)\n", 452 | " \n", 453 | " def forward(self, query, key, value, mask):\n", 454 | " " 455 | ], 456 | "execution_count": 0, 457 | "outputs": [] 458 | }, 459 | { 460 | "metadata": { 461 | "id": "uZ-xQbgM6MNl", 462 | "colab_type": "text" 463 | }, 464 | "cell_type": "markdown", 465 | "source": [ 466 | "### Multi-Head Attention\n", 467 | "\n", 468 | "![](https://hsto.org/webt/59/f0/44/59f0440f1109b864893781.png)\n", 469 | "\n", 470 | "Важная идея, почему attention (и, главное, self-attention) заработал - использование нескольких голов (multi-head).\n", 471 | "\n", 472 | "Вообще, когда мы делаем attention - мы определяем похожесть ключа и запроса. Многоголовость помогает (должна) определять эту похожесть по разным критериям - синтаксически, семантически и т.д.\n", 473 | "\n", 474 | "Например, на картинке используется две головы и одна голова смотрит на *the animal* при генерации *it*, вторая - на *tired*:\n", 475 | "\n", 476 | "![](http://jalammar.github.io/images/t/transformer_self-attention_visualization_2.png) \n", 477 | "*From [Tensor2Tensor Tutorial](https://colab.research.google.com/github/tensorflow/tensor2tensor/blob/master/tensor2tensor/notebooks/hello_t2t.ipynb)*\n", 478 | "\n", 479 | "Применяется это таким образом:\n", 480 | "\n", 481 | "$$\\mathrm{MultiHead}(Q, K, V) = \\mathrm{Concat}(\\mathrm{head_1}, ...,\n", 482 | "\\mathrm{head_h})W^O \\\\\n", 483 | " \\mathrm{head_i} = \\mathrm{Attention}(QW^Q_i, KW^K_i, VW^V_i)$$\n", 484 | " \n", 485 | "где $W^Q_i \\in \\mathbb{R}^{d_{model} \\times d_k}, W_i^K \\in \\mathbb{R}^{d_{model} \\times d_k}, W^V_i \\in \\mathbb{R}^{d_{model} \\times d_v}, W^O \\in \\mathbb{R}^{hd_v \\times d_{model}}$.\n", 486 | "\n", 487 | "В оригинальной статье использовали $h=8$, $d_k=d_v=d_{\\text{model}}/h=64$.\n", 488 | "\n", 489 | "Процесс применения такой:\n", 490 | "![](http://jalammar.github.io/images/t/transformer_multi-headed_self-attention-recap.png) \n", 491 | "*From Illustrated Transformer*" 492 | ] 493 | }, 494 | { 495 | "metadata": { 496 | "id": "rg-CxvPDAJPP", 497 | "colab_type": "code", 498 | "colab": {} 499 | }, 500 | "cell_type": "code", 501 | "source": [ 502 | "class MultiHeadedAttention(nn.Module):\n", 503 | " def __init__(self, heads_count, d_model, dropout_rate=0.1):\n", 504 | " super().__init__()\n", 505 | " \n", 506 | " assert d_model % heads_count == 0\n", 507 | "\n", 508 | " self._d_k = d_model // heads_count\n", 509 | " self._heads_count = heads_count\n", 510 | " self._attention = ScaledDotProductAttention(dropout_rate)\n", 511 | " self._attn_probs = None\n", 512 | " \n", 513 | " self._w_q = nn.Linear(d_model, d_model)\n", 514 | " self._w_k = nn.Linear(d_model, d_model)\n", 515 | " self._w_v = nn.Linear(d_model, d_model)\n", 516 | " self._w_o = nn.Linear(d_model, d_model)\n", 517 | " \n", 518 | " def forward(self, query, key, value, mask=None):\n", 519 | " " 520 | ], 521 | "execution_count": 0, 522 | "outputs": [] 523 | }, 524 | { 525 | "metadata": { 526 | "id": "pOKwneaKGaJi", 527 | "colab_type": "text" 528 | }, 529 | "cell_type": "markdown", 530 | "source": [ 531 | "### Position-wise Feed-Forward Networks\n", 532 | "\n", 533 | "Линейный блок в энкодере выглядит так:\n", 534 | "$$\\mathrm{FFN}(x)=\\max(0, xW_1 + b_1) W_2 + b_2$$" 535 | ] 536 | }, 537 | { 538 | "metadata": { 539 | "id": "uh1UVkAUGiwh", 540 | "colab_type": "code", 541 | "colab": {} 542 | }, 543 | "cell_type": "code", 544 | "source": [ 545 | "class PositionwiseFeedForward(nn.Module):\n", 546 | " def __init__(self, d_model, d_ff, dropout=0.1):\n", 547 | " super().__init__()\n", 548 | " \n", 549 | " self.w_1 = nn.Linear(d_model, d_ff)\n", 550 | " self.w_2 = nn.Linear(d_ff, d_model)\n", 551 | " self.dropout = nn.Dropout(dropout)\n", 552 | "\n", 553 | " def forward(self, inputs):\n", 554 | " return self.w_2(self.dropout(F.relu(self.w_1(inputs))))" 555 | ], 556 | "execution_count": 0, 557 | "outputs": [] 558 | }, 559 | { 560 | "metadata": { 561 | "id": "_dZoU1JIt6QP", 562 | "colab_type": "text" 563 | }, 564 | "cell_type": "markdown", 565 | "source": [ 566 | "### Encoder block\n", 567 | "\n", 568 | "Соберем все в блок:" 569 | ] 570 | }, 571 | { 572 | "metadata": { 573 | "id": "Nh7wQL65sBmk", 574 | "colab_type": "code", 575 | "colab": {} 576 | }, 577 | "cell_type": "code", 578 | "source": [ 579 | "class EncoderBlock(nn.Module):\n", 580 | " def __init__(self, size, self_attn, feed_forward, dropout_rate):\n", 581 | " super().__init__()\n", 582 | " \n", 583 | " self._self_attn = self_attn\n", 584 | " self._feed_forward = feed_forward\n", 585 | " self._self_attention_block = ResidualBlock(size, dropout_rate)\n", 586 | " self._feed_forward_block = ResidualBlock(size, dropout_rate)\n", 587 | "\n", 588 | " def forward(self, inputs, mask):\n", 589 | " outputs = self._self_attention_block(inputs, lambda inputs: self._self_attn(inputs, inputs, inputs, mask))\n", 590 | " return self._feed_forward_block(outputs, self._feed_forward)" 591 | ], 592 | "execution_count": 0, 593 | "outputs": [] 594 | }, 595 | { 596 | "metadata": { 597 | "id": "x8ndCRZLl4ZZ", 598 | "colab_type": "code", 599 | "colab": {} 600 | }, 601 | "cell_type": "code", 602 | "source": [ 603 | "class Encoder(nn.Module):\n", 604 | " def __init__(self, vocab_size, d_model, d_ff, blocks_count, heads_count, dropout_rate):\n", 605 | " super().__init__()\n", 606 | " \n", 607 | " self._emb = nn.Sequential(\n", 608 | " nn.Embedding(vocab_size, d_model),\n", 609 | " PositionalEncoding(d_model, dropout_rate)\n", 610 | " )\n", 611 | " \n", 612 | " block = lambda: EncoderBlock(\n", 613 | " size=d_model, \n", 614 | " self_attn=MultiHeadedAttention(heads_count, d_model, dropout_rate), \n", 615 | " feed_forward=PositionwiseFeedForward(d_model, d_ff, dropout_rate),\n", 616 | " dropout_rate=dropout_rate\n", 617 | " )\n", 618 | " self._blocks = nn.ModuleList([block() for _ in range(blocks_count)])\n", 619 | " self._norm = LayerNorm(d_model)\n", 620 | " \n", 621 | " def forward(self, inputs, mask):\n", 622 | " inputs = self._emb(inputs)\n", 623 | " \n", 624 | " for block in self._blocks:\n", 625 | " inputs = block(inputs, mask)\n", 626 | " return self._norm(inputs)" 627 | ], 628 | "execution_count": 0, 629 | "outputs": [] 630 | }, 631 | { 632 | "metadata": { 633 | "id": "_pphRcbTvqnq", 634 | "colab_type": "text" 635 | }, 636 | "cell_type": "markdown", 637 | "source": [ 638 | "## Decoder\n", 639 | "\n", 640 | "![](https://hsto.org/webt/59/f0/44/59f0440f7d88f805415140.png =x550)\n", 641 | "\n", 642 | "Блок декодера (серая часть) состоит уже из трех частей:\n", 643 | "1. Сперва - тот же self-attention, что и в энкодере\n", 644 | "2. Затем - стандартный attention на выходы из энкодера + текущее состояние декодера (такой же был в seq2seq with attention)\n", 645 | "3. Наконец - feed-forward блок\n", 646 | "\n", 647 | "Всё это, конечно, с residual связями." 648 | ] 649 | }, 650 | { 651 | "metadata": { 652 | "id": "6LTWjKUXx2LP", 653 | "colab_type": "code", 654 | "colab": {} 655 | }, 656 | "cell_type": "code", 657 | "source": [ 658 | "class DecoderLayer(nn.Module):\n", 659 | " def __init__(self, size, self_attn, encoder_attn, feed_forward, dropout_rate):\n", 660 | " super().__init__()\n", 661 | " \n", 662 | " self._self_attn = self_attn\n", 663 | " self._encoder_attn = encoder_attn\n", 664 | " self._feed_forward = feed_forward\n", 665 | " self._self_attention_block = ResidualBlock(size, dropout_rate)\n", 666 | " self._attention_block = ResidualBlock(size, dropout_rate)\n", 667 | " self._feed_forward_block = ResidualBlock(size, dropout_rate)\n", 668 | " \n", 669 | " def forward(self, inputs, encoder_output, source_mask, target_mask):\n", 670 | " outputs = self._self_attention_block(\n", 671 | " inputs, lambda inputs: self._self_attn(inputs, inputs, inputs, target_mask)\n", 672 | " )\n", 673 | " outputs = self._attention_block(\n", 674 | " outputs, lambda inputs: self._encoder_attn(inputs, encoder_output, encoder_output, source_mask)\n", 675 | " )\n", 676 | " return self._feed_forward_block(outputs, self._feed_forward)" 677 | ], 678 | "execution_count": 0, 679 | "outputs": [] 680 | }, 681 | { 682 | "metadata": { 683 | "id": "Un0AOmdqLPp_", 684 | "colab_type": "code", 685 | "colab": {} 686 | }, 687 | "cell_type": "code", 688 | "source": [ 689 | "class Decoder(nn.Module):\n", 690 | " def __init__(self, vocab_size, d_model, d_ff, blocks_count, heads_count, dropout_rate):\n", 691 | " super().__init__()\n", 692 | " \n", 693 | " self._emb = nn.Sequential(\n", 694 | " nn.Embedding(vocab_size, d_model),\n", 695 | " PositionalEncoding(d_model, dropout_rate)\n", 696 | " )\n", 697 | " \n", 698 | " block = lambda: DecoderLayer(\n", 699 | " size=d_model, \n", 700 | " self_attn=MultiHeadedAttention(heads_count, d_model, dropout_rate),\n", 701 | " encoder_attn=MultiHeadedAttention(heads_count, d_model, dropout_rate),\n", 702 | " feed_forward=PositionwiseFeedForward(d_model, d_ff, dropout_rate),\n", 703 | " dropout_rate=dropout_rate\n", 704 | " )\n", 705 | " self._blocks = nn.ModuleList([block() for _ in range(blocks_count)])\n", 706 | " self._norm = LayerNorm(d_model)\n", 707 | " self._out_layer = nn.Linear(d_model, vocab_size)\n", 708 | " \n", 709 | " def forward(self, inputs, encoder_output, source_mask, target_mask):\n", 710 | " inputs = self._emb(inputs)\n", 711 | " for block in self._blocks:\n", 712 | " inputs = block(inputs, encoder_output, source_mask, target_mask)\n", 713 | " return self._out_layer(self._norm(inputs))" 714 | ], 715 | "execution_count": 0, 716 | "outputs": [] 717 | }, 718 | { 719 | "metadata": { 720 | "id": "tbEkHtwWz0Yh", 721 | "colab_type": "text" 722 | }, 723 | "cell_type": "markdown", 724 | "source": [ 725 | "В декодере нужно аттентиться только на предыдущие токены - сгенерируем маску для этого:" 726 | ] 727 | }, 728 | { 729 | "metadata": { 730 | "id": "EbVkZSRq0cD5", 731 | "colab_type": "code", 732 | "colab": {} 733 | }, 734 | "cell_type": "code", 735 | "source": [ 736 | "def subsequent_mask(size):\n", 737 | " mask = torch.ones(size, size, device=DEVICE).triu_()\n", 738 | " return mask.unsqueeze(0) == 0" 739 | ], 740 | "execution_count": 0, 741 | "outputs": [] 742 | }, 743 | { 744 | "metadata": { 745 | "id": "_8X_fqeL0jeW", 746 | "colab_type": "code", 747 | "colab": {} 748 | }, 749 | "cell_type": "code", 750 | "source": [ 751 | "plt.figure(figsize=(5,5))\n", 752 | "plt.imshow(subsequent_mask(20)[0])" 753 | ], 754 | "execution_count": 0, 755 | "outputs": [] 756 | }, 757 | { 758 | "metadata": { 759 | "id": "b7UEsntVj9lb", 760 | "colab_type": "text" 761 | }, 762 | "cell_type": "markdown", 763 | "source": [ 764 | "## Полная модель" 765 | ] 766 | }, 767 | { 768 | "metadata": { 769 | "id": "vLIGjPOiO7X9", 770 | "colab_type": "code", 771 | "colab": {} 772 | }, 773 | "cell_type": "code", 774 | "source": [ 775 | "class FullModel(nn.Module):\n", 776 | " def __init__(self, source_vocab_size, target_vocab_size, d_model=256, d_ff=1024, \n", 777 | " blocks_count=4, heads_count=8, dropout_rate=0.1):\n", 778 | " \n", 779 | " super().__init__()\n", 780 | " \n", 781 | " self.d_model = d_model\n", 782 | " self.encoder = Encoder(source_vocab_size, d_model, d_ff, blocks_count, heads_count, dropout_rate)\n", 783 | " self.decoder = Decoder(target_vocab_size, d_model, d_ff, blocks_count, heads_count, dropout_rate)\n", 784 | " \n", 785 | " for p in self.parameters():\n", 786 | " if p.dim() > 1:\n", 787 | " nn.init.xavier_uniform_(p)\n", 788 | " \n", 789 | " def forward(self, source_inputs, target_inputs, source_mask, target_mask):\n", 790 | " encoder_output = self.encoder(source_inputs, source_mask)\n", 791 | " return self.decoder(target_inputs, encoder_output, source_mask, target_mask)" 792 | ], 793 | "execution_count": 0, 794 | "outputs": [] 795 | }, 796 | { 797 | "metadata": { 798 | "id": "BmucSaUOjlmh", 799 | "colab_type": "code", 800 | "colab": {} 801 | }, 802 | "cell_type": "code", 803 | "source": [ 804 | "def make_mask(source_inputs, target_inputs, pad_idx):\n", 805 | " source_mask = (source_inputs != pad_idx).unsqueeze(-2)\n", 806 | " target_mask = (target_inputs != pad_idx).unsqueeze(-2)\n", 807 | " target_mask = target_mask & subsequent_mask(target_inputs.size(-1)).type_as(target_mask)\n", 808 | " return source_mask, target_mask\n", 809 | "\n", 810 | "\n", 811 | "def convert_batch(batch, pad_idx=1):\n", 812 | " source_inputs, target_inputs = batch.source.transpose(0, 1), batch.target.transpose(0, 1)\n", 813 | " source_mask, target_mask = make_mask(source_inputs, target_inputs, pad_idx)\n", 814 | " \n", 815 | " return source_inputs, target_inputs, source_mask, target_mask" 816 | ], 817 | "execution_count": 0, 818 | "outputs": [] 819 | }, 820 | { 821 | "metadata": { 822 | "id": "9iWSl6m6jfbl", 823 | "colab_type": "code", 824 | "colab": {} 825 | }, 826 | "cell_type": "code", 827 | "source": [ 828 | "batch = next(iter(train_iter))" 829 | ], 830 | "execution_count": 0, 831 | "outputs": [] 832 | }, 833 | { 834 | "metadata": { 835 | "id": "5_qVuSL8QJg4", 836 | "colab_type": "code", 837 | "colab": {} 838 | }, 839 | "cell_type": "code", 840 | "source": [ 841 | "model = FullModel(source_vocab_size=len(word_field.vocab), target_vocab_size=len(word_field.vocab)).to(DEVICE)\n", 842 | "\n", 843 | "model(*convert_batch(batch))" 844 | ], 845 | "execution_count": 0, 846 | "outputs": [] 847 | }, 848 | { 849 | "metadata": { 850 | "id": "vX6PrVksnaq7", 851 | "colab_type": "text" 852 | }, 853 | "cell_type": "markdown", 854 | "source": [ 855 | "## Оптимизатор\n", 856 | "\n", 857 | "Тоже очень важно в данной модели - использовать правильный оптимизатор" 858 | ] 859 | }, 860 | { 861 | "metadata": { 862 | "id": "KMhopCgTnh-w", 863 | "colab_type": "code", 864 | "colab": {} 865 | }, 866 | "cell_type": "code", 867 | "source": [ 868 | "class NoamOpt(object):\n", 869 | " def __init__(self, model_size, factor=2, warmup=4000, optimizer=None):\n", 870 | " if optimizer is not None:\n", 871 | " self.optimizer = optimizer\n", 872 | " else:\n", 873 | " self.optimizer = optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9)\n", 874 | " self._step = 0\n", 875 | " self.warmup = warmup\n", 876 | " self.factor = factor\n", 877 | " self.model_size = model_size\n", 878 | " self._rate = 0\n", 879 | " \n", 880 | " def step(self):\n", 881 | " self._step += 1\n", 882 | " rate = self.rate()\n", 883 | " for p in self.optimizer.param_groups:\n", 884 | " p['lr'] = rate\n", 885 | " self._rate = rate\n", 886 | " self.optimizer.step()\n", 887 | " \n", 888 | " def rate(self, step = None):\n", 889 | " if step is None:\n", 890 | " step = self._step\n", 891 | " return self.factor * (self.model_size ** (-0.5) * min(step ** (-0.5), step * self.warmup ** (-1.5)))" 892 | ], 893 | "execution_count": 0, 894 | "outputs": [] 895 | }, 896 | { 897 | "metadata": { 898 | "id": "OuYc21J5oIdb", 899 | "colab_type": "text" 900 | }, 901 | "cell_type": "markdown", 902 | "source": [ 903 | "Идея в том, чтобы повышать learning rate в течении первых warmup шагов линейно, а затем понижать его по сложной формуле:\n", 904 | "\n", 905 | "$$lrate = d_{\\text{model}}^{-0.5} \\cdot\n", 906 | " \\min({step\\_num}^{-0.5},\n", 907 | " {step\\_num} \\cdot {warmup\\_steps}^{-1.5})$$" 908 | ] 909 | }, 910 | { 911 | "metadata": { 912 | "id": "kMBL261hoA58", 913 | "colab_type": "code", 914 | "colab": {} 915 | }, 916 | "cell_type": "code", 917 | "source": [ 918 | "opts = [NoamOpt(512, 1, 4000, None), \n", 919 | " NoamOpt(512, 1, 8000, None),\n", 920 | " NoamOpt(256, 1, 4000, None)]\n", 921 | "plt.plot(np.arange(1, 20000), [[opt.rate(i) for opt in opts] for i in range(1, 20000)])\n", 922 | "plt.legend([\"512:4000\", \"512:8000\", \"256:4000\"])" 923 | ], 924 | "execution_count": 0, 925 | "outputs": [] 926 | }, 927 | { 928 | "metadata": { 929 | "id": "W71i85Q4pdOS", 930 | "colab_type": "text" 931 | }, 932 | "cell_type": "markdown", 933 | "source": [ 934 | "## Тренировка модели" 935 | ] 936 | }, 937 | { 938 | "metadata": { 939 | "id": "_E2JxfRuphch", 940 | "colab_type": "code", 941 | "colab": {} 942 | }, 943 | "cell_type": "code", 944 | "source": [ 945 | "import math\n", 946 | "from tqdm import tqdm\n", 947 | "tqdm.get_lock().locks = []\n", 948 | "\n", 949 | "\n", 950 | "def do_epoch(model, criterion, data_iter, optimizer=None, name=None):\n", 951 | " epoch_loss = 0\n", 952 | " \n", 953 | " is_train = not optimizer is None\n", 954 | " name = name or ''\n", 955 | " model.train(is_train)\n", 956 | " \n", 957 | " batches_count = len(data_iter)\n", 958 | " \n", 959 | " with torch.autograd.set_grad_enabled(is_train):\n", 960 | " with tqdm(total=batches_count) as progress_bar:\n", 961 | " for i, batch in enumerate(data_iter):\n", 962 | " source_inputs, target_inputs, source_mask, target_mask = convert_batch(batch) \n", 963 | " logits = model.forward(source_inputs, target_inputs[:, :-1], source_mask, target_mask[:, :-1, :-1])\n", 964 | " \n", 965 | " logits = logits.contiguous().view(-1, logits.shape[-1])\n", 966 | " target = target_inputs[:, 1:].contiguous().view(-1)\n", 967 | " loss = criterion(logits, target)\n", 968 | "\n", 969 | " epoch_loss += loss.item()\n", 970 | "\n", 971 | " if optimizer:\n", 972 | " optimizer.optimizer.zero_grad()\n", 973 | " loss.backward()\n", 974 | " optimizer.step()\n", 975 | "\n", 976 | " progress_bar.update()\n", 977 | " progress_bar.set_description('{:>5s} Loss = {:.5f}, PPX = {:.2f}'.format(name, loss.item(), \n", 978 | " math.exp(loss.item())))\n", 979 | " \n", 980 | " progress_bar.set_description('{:>5s} Loss = {:.5f}, PPX = {:.2f}'.format(\n", 981 | " name, epoch_loss / batches_count, math.exp(epoch_loss / batches_count))\n", 982 | " )\n", 983 | " progress_bar.refresh()\n", 984 | "\n", 985 | " return epoch_loss / batches_count\n", 986 | "\n", 987 | "\n", 988 | "def fit(model, criterion, optimizer, train_iter, epochs_count=1, val_iter=None):\n", 989 | " best_val_loss = None\n", 990 | " for epoch in range(epochs_count):\n", 991 | " name_prefix = '[{} / {}] '.format(epoch + 1, epochs_count)\n", 992 | " train_loss = do_epoch(model, criterion, train_iter, optimizer, name_prefix + 'Train:')\n", 993 | " \n", 994 | " if not val_iter is None:\n", 995 | " val_loss = do_epoch(model, criterion, val_iter, None, name_prefix + ' Val:')" 996 | ], 997 | "execution_count": 0, 998 | "outputs": [] 999 | }, 1000 | { 1001 | "metadata": { 1002 | "id": "5X2kYDU_rCjP", 1003 | "colab_type": "code", 1004 | "colab": {} 1005 | }, 1006 | "cell_type": "code", 1007 | "source": [ 1008 | "model = FullModel(source_vocab_size=len(word_field.vocab), target_vocab_size=len(word_field.vocab)).to(DEVICE)\n", 1009 | "\n", 1010 | "pad_idx = word_field.vocab.stoi['']\n", 1011 | "criterion = nn.CrossEntropyLoss(ignore_index=pad_idx).to(DEVICE)\n", 1012 | "\n", 1013 | "optimizer = NoamOpt(model.d_model)\n", 1014 | "\n", 1015 | "fit(model, criterion, optimizer, train_iter, epochs_count=30, val_iter=test_iter)" 1016 | ], 1017 | "execution_count": 0, 1018 | "outputs": [] 1019 | }, 1020 | { 1021 | "metadata": { 1022 | "id": "PRMFkzDV-wI9", 1023 | "colab_type": "text" 1024 | }, 1025 | "cell_type": "markdown", 1026 | "source": [ 1027 | "**Задание** Добавьте генератор для модели.\n", 1028 | "\n", 1029 | "**Задание** Добавьте оценку для модели с помощью ROUGE metric (например, из пакета https://pypi.org/project/pyrouge/0.1.3/)\n", 1030 | "\n", 1031 | "**Задание** Добавьте визуализацию (можно подсмотреть в коде по ссылкам)." 1032 | ] 1033 | }, 1034 | { 1035 | "metadata": { 1036 | "id": "Ij1GiNx54Ke5", 1037 | "colab_type": "text" 1038 | }, 1039 | "cell_type": "markdown", 1040 | "source": [ 1041 | "## Улучшения модели\n", 1042 | "\n", 1043 | "**Задание** Попробовать расшарить матрицы эмбеддингов - их тут три (входные в энкодер и декодер + выход декодера).\n", 1044 | "\n", 1045 | "**Задание** Замените лосс на LabelSmoothing." 1046 | ] 1047 | }, 1048 | { 1049 | "metadata": { 1050 | "id": "goFvQj18--iP", 1051 | "colab_type": "text" 1052 | }, 1053 | "cell_type": "markdown", 1054 | "source": [ 1055 | "# Pointer-Generator Networks\n", 1056 | "\n", 1057 | "Клёвая идея, специфичная для саммаризации:\n", 1058 | "\n", 1059 | "![](https://image.ibb.co/eijTc0/2018-11-20-10-18-52.png)\n", 1060 | "\n", 1061 | "**Задание** Попробуйте реализовать её." 1062 | ] 1063 | }, 1064 | { 1065 | "metadata": { 1066 | "colab_type": "text", 1067 | "id": "N4-3pYqVJIKA" 1068 | }, 1069 | "cell_type": "markdown", 1070 | "source": [ 1071 | "# Дополнительные материалы\n", 1072 | "\n", 1073 | "## Статьи\n", 1074 | "Attention Is All You Need, 2017 [[pdf]](https://arxiv.org/pdf/1706.03762.pdf) \n", 1075 | "Get To The Point: Summarization with Pointer-Generator Networks, 2017 [[pdf]](https://arxiv.org/pdf/1704.04368.pdf) \n", 1076 | "Universal Transformers, 2018 [[arxiv]](https://arxiv.org/abs/1807.03819)\n", 1077 | "\n", 1078 | "## Блоги\n", 1079 | "[Transformer — новая архитектура нейросетей для работы с последовательностями](https://habr.com/post/341240/) \n", 1080 | "[The Illustrated Transformer](http://jalammar.github.io/illustrated-transformer/) \n", 1081 | "[The Annotated Transformer](http://nlp.seas.harvard.edu/2018/04/03/attention.html) \n", 1082 | "[Weighted Tranformer](https://einstein.ai/research/blog/weighted-transformer) \n", 1083 | "[Your tldr by an ai: a deep reinforced model for abstractive summarization](https://einstein.ai/research/blog/your-tldr-by-an-ai-a-deep-reinforced-model-for-abstractive-summarization)" 1084 | ] 1085 | }, 1086 | { 1087 | "metadata": { 1088 | "id": "Vwb5e5hPQebd", 1089 | "colab_type": "text" 1090 | }, 1091 | "cell_type": "markdown", 1092 | "source": [ 1093 | "# Сдача\n", 1094 | "\n", 1095 | "[Форма для сдачи](https://goo.gl/forms/3npI3cPpru8JorXV2) \n", 1096 | "[Feedback](https://goo.gl/forms/9aizSzOUrx7EvGlG3)" 1097 | ] 1098 | } 1099 | ] 1100 | } --------------------------------------------------------------------------------