{"cells":[{"metadata":{"trusted":false},"cell_type":"code","source":"import torch\nfrom matplotlib import pyplot as plt\nimport seaborn as sns","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Небольшая вводная"},{"metadata":{"trusted":false},"cell_type":"code","source":"ENCODER_WORDS = 2\nDECODER_WORDS = 3\nEMBEDDING_DIM = 4","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"- 2 слова в энкодере\n- 3 слова в декодере\n- 4 - размер эмбеддинга"},{"metadata":{"trusted":false},"cell_type":"code","source":"encoder_hidden = torch.rand(ENCODER_WORDS, EMBEDDING_DIM)\ndecoder_hidden = torch.rand(DECODER_WORDS, EMBEDDING_DIM)\ndecoder_hidden.shape, encoder_hidden.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"encoder_hidden","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"decoder_hidden","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Скалярное произведение\n*dot product, matmul, matrix multiplication*  \nМожет выступать в качестве меры схожести векторов. Чем больше скалярное произведение двух вектор, тем больше они похожи\n![](https://wikimedia.org/api/rest_v1/media/math/render/svg/69f8ac1d2b7ffb9ef70bb6b151a4b931f20087a5)"},{"metadata":{"trusted":false},"cell_type":"code","source":"x1 = torch.Tensor([1, 2, 3])\nx2 = torch.Tensor([2, 2, 4])\nx3 = torch.Tensor([-2, 2, -4])\n\nprint('Скалярное произведение между первым и вторым векторами: {}'.format((x1 * x2).sum().item()))\nprint('Скалярное произведение между первым и третьим векторами: {}'.format((x1 * x3).sum().item()))\nprint('Скалярное произведение между вторым и третьим векторами: {}'.format((x2 * x3).sum().item()))\nprint()\nprint('Скалярное произведение первого вектора с самим собой: {}'.format((x1 * x1).sum().item()))\nprint('Скалярное произведение второго вектора с самим собой: {}'.format((x2 * x2).sum().item()))\nprint('Скалярное произведение третьего вектора с самим собой: {}'.format((x3 * x3).sum().item()))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Косинусное расстояние это просто скалирование (деление) на нормы (расстояние до начала координат) векторов"},{"metadata":{"trusted":false},"cell_type":"code","source":"print('Косинусная близость между первым и вторым векторами: {}'.format((x1 * x2).sum()/(x1.norm()*x2.norm()).item()))\nprint('Косинусная близость между первым и первым векторами: {}'.format((x1 * x1).sum()/(x1.norm()*x1.norm()).item()))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Рассчитаем насколько каждое слово в декодере похоже на каждое слово в энкодере с помощью скалярного умножения векторов (каждый с каждым)"},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_scores = torch.matmul(decoder_hidden, encoder_hidden.t())","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_scores","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Напомню, что у нас:\n- 2 слова в энкодере\n- 3 слова в декодере"},{"metadata":{"trusted":false},"cell_type":"code","source":"plt.figure(figsize=(12, 12))\nplt.title('Attention Scores')\nsns.heatmap(attention_scores.numpy(), annot=True, cmap=\"YlGnBu\")\nplt.xlabel('Слова энкодера')\nplt.ylabel('Слова декодера')","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_scores.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### То есть вектор ниже говорит о том, насколько нулевое слово в декодере похоже на нулевое и первое слово в энкодере"},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_scores[0]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Рассчитаем в долях насколько каждое слово в энкодере \"важно\" для отдельного слова в декодере, то есть по сути его долю, вклад, релеваность\n### Для этого нам поможет софтмакс по строкам"},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_distribution = torch.softmax(attention_scores, 1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_distribution_2 = torch.softmax(attention_scores / EMBEDDING_DIM ** 0.5, 1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_distribution","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# в сумме должны получить вектор из единиц размером количество слов в декодере\nattention_distribution.sum(1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"plt.figure(figsize=(12, 12))\nplt.title('Attention Distribution')\nsns.heatmap(attention_distribution.numpy(), annot=True, cmap=\"YlGnBu\")\nplt.xlabel('Слова энкодера')\nplt.ylabel('Слова декодера')","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"plt.figure(figsize=(12, 12))\nplt.title('Attention Distribution')\nsns.heatmap(attention_distribution_2.numpy(), annot=True, cmap=\"YlGnBu\")\nplt.xlabel('Слова энкодера')\nplt.ylabel('Слова декодера')","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_distribution","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_distribution_2","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"x = torch.rand(4, 128)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attn_scores = torch.matmul(x, x.t())","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attn_dist = torch.softmax(attn_scores, 1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"plt.figure(figsize=(12, 12))\nplt.title('Attention Distribution')\nsns.heatmap(attn_dist.numpy(), annot=True, cmap=\"YlGnBu\")\nplt.xlabel('Слова энкодера')\nplt.ylabel('Слова декодера')","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attn_dist = torch.softmax(torch.matmul(x, x.t()) / 128 ** 0.5 , 1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"attn_dist","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"plt.figure(figsize=(12, 12))\nplt.title('Attention Distribution')\nsns.heatmap(attn_dist.numpy(), annot=True, cmap=\"YlGnBu\")\nplt.xlabel('Слова энкодера')\nplt.ylabel('Слова декодера')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Что это нам говорит"},{"metadata":{"trusted":false},"cell_type":"code","source":"c00 = attention_distribution[0][0].item()\nc01 = attention_distribution[0][1].item()\n\nprint('Доля памяти нулевого слова энкодера для нулевого слова из декодера: {:.2f} %'.format(c00 * 100))\nprint('Доля памяти первого слова энкодера для нулевого слова из декодера: {:.2f} %'.format(c01 * 100))\n\nprint()\n\nc10 = attention_distribution[1][0].item()\nc11 = attention_distribution[1][1].item()\n\nprint('Доля памяти нулевого слова энкодера для первого слова из декодера: {:.2f} %'.format(c10 * 100))\nprint('Доля памяти первого слова энкодера для первого слова из декодера: {:.2f} %'.format(c11 * 100))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### По сути эти цифры говорят насколько сильно для каждого слова декодера нам нужно обратить внимание на каждое слово из энкодера\n### Или как вспомнить каждое слово из энкодера в долях, в зависимости насколько оно важно для слова из декодера, то есть"},{"metadata":{"trusted":false},"cell_type":"code","source":"print('Для первого слова декодера нужно вспомнить {:.2f} % нулевого слова энкодера и {:.2f} % первого слова энкодера'.format(\n    c10 * 100, c11 * 100))\nprint('Или')\nprint('Для первого слова декодера нулевое слова энкодера релевантно на {:.2f} %, а первое слово энкодера на {:.2f} %'.format(\n    c10 * 100, c11 * 100))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### По сути у нас получились веса слов энкодера относительно слов декодера\n### Давайте для каждого слова в декодере составим комбинацию векторов слов энкодера с этими весами\n### Это называется вектором внимания, то есть мы для каждого слова декодера собираем все предложение из энкодера с весами, которые говорят на какую часть предложения энкодера нужно обратить больше внимания"},{"metadata":{"trusted":false},"cell_type":"code","source":"# наша таблицу с весами\nattention_distribution","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# наши вектора слов энкодера\nencoder_hidden","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# наши вектора слов декодера\ndecoder_hidden","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Составим вектор внимания для нулевого слова декодера"},{"metadata":{"trusted":false},"cell_type":"code","source":"# веса слов энкодера для нулевого слова декодера\nattention_distribution[0]","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# два веса на каждое из слов в энкодере\nattention_distribution[0][0], attention_distribution[0][1]","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# домножим вектор нулевого слова энкодера на его вес (какую долю информации мы хотим взять от этого слова)\nattention_distribution[0][0] * encoder_hidden[0]","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# тоже самое для первого слова в энкодере\nattention_distribution[0][1] * encoder_hidden[1]","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"print('Для нулевого слова декодера нужно вспомнить {:.2f} % нулевого слова энкодера и {:.2f} % первого слова энкодера'.format(\n    c00 * 100, c01 * 100))","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# теперь давайте эту информацию сложим\n# то есть возьмем часть от нулевого слова и часть от первого\n# эти части зависят от того насколько для текущего слова декодера нам важные слова энкодера\nattn_vec_0 = attention_distribution[0][0] * encoder_hidden[0] + attention_distribution[0][1] * encoder_hidden[1]","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# получился вектор внимания нулевого слова декодера относительно слов энкодера\nattn_vec_0","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# сделаем тоже самое для остальных слов декодера\n# их у нас все еще 3\nattn_vec_1 = attention_distribution[1][0] * encoder_hidden[0] + attention_distribution[1][1] * encoder_hidden[1]\nattn_vec_2 = attention_distribution[2][0] * encoder_hidden[0] + attention_distribution[2][1] * encoder_hidden[1]","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# сделаем массив для векторов внимания, в нем должно быть количество векторов равное количеству слов в декодере\n# потому что мы считаем вектора внимания для каждого слова в декодере, а \"обращать внимание\" будем на слова в энкодере\nattention_vectors = [attn_vec_0, attn_vec_1, attn_vec_2]","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# теперь давайте переведем массив в матрицу\n# добавим в каждый вектор нулевую размерность\n# и по этой размерности сконкатенируем\nattention_vectors = torch.cat([vec.unsqueeze(0) for vec in attention_vectors], dim=0)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# вот что у нас получилось\n# это просто несколько комбинаций векторов энкодера\nattention_vectors","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# посмотрим размерность\nattention_vectors.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# убедимся, что она такая же, что и у декодера\ndecoder_hidden.shape == attention_vectors.shape\n# этого мы и хотели добиться","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Теперь для каждого слова декодера у нас есть вектора внимания, в которых находится наиболее релевантная информация от конкретного слова в декодере к каждому слову в энкодере\n### То есть в каждом векторе внимания у нас есть информация обо всем предложение энкодера\n### То есть каждый вектор внимания вбирает в себя наиболее полезную(взвешенную) комбинацию слов в предложении\n### Теперь мы можем скомбинировать вектора декодера и вектора внимания! И таким образом у нас будет больше полезной информации и меньше шанс, что мы забудем что-то из энкодера"},{"metadata":{"trusted":false},"cell_type":"code","source":"# можно либо конкатенировать\noutput_vectors = torch.cat([decoder_hidden, attention_vectors], dim=1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# теперь размерность эмбеддинга каждого слова декодера стала в два раза больше\noutput_vectors.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# либо складывать\noutput_vectors = decoder_hidden + attention_vectors","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"output_vectors.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Теперь по этим векторам мы можем лучше предсказать слово для задачи seq2seq\n### Например, слово на языке декодера"},{"metadata":{},"cell_type":"markdown","source":"### Матрицу attention_vectors мы, на самом деле, можем получить значительно проще\n### Так как нам нужные линейные комбинации векторов, то это опять-таки скалярное произведение матриц"},{"metadata":{"trusted":false},"cell_type":"code","source":"attention_vectors_new = torch.matmul(attention_distribution, encoder_hidden)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"# мы получили ту же самую матрицу\nattention_vectors_new, (attention_vectors_new - attention_vectors).sum().item()\n\n# вы можете выписать на листок перемножение матриц attention_distribution и encoder_hidden\n# и проверить","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Еще раз в кратце"},{"metadata":{"trusted":false},"cell_type":"code","source":"# есть представления слов энкодера\nencoder_hidden\n\n# есть представления слов декодера\ndecoder_hidden\n\n# мы хотим понять насколько каждое слово в декодере похоже на каждое слово в энкодере\n# сделаем это через скалярное произведение матриц\nattention_scores = torch.matmul(decoder_hidden, encoder_hidden.t())\n\n# далее для каждого слова из декодера (каждой строки матрицы attention_scores)\n# посчитаем \"вклад\"/\"долю\"/\"важность\"/\"вес\" с помощью софтмакса по строкам\n# то есть для каждой строки (для каждого слова декодера) у нас будет распределение важности слов энкодера\nattention_distribution = torch.softmax(attention_scores, 1)\n\n# возьмем взвешенную сумму вектором энкодера\n# веса мы посчитали в attention_distribution\n# если вы не понимаете почему у нас это получилось\n# вернитесь выше или распишите на листочке\nattention_vectors = torch.matmul(attention_distribution, encoder_hidden)\n\n# скомбинируем вектора декодера и вектора внимания слов энкодера к этим словам декодера\ndecoder_with_attention = torch.cat([decoder_hidden, attention_vectors], dim=1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"decoder_with_attention.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Но у нас же батчи!"},{"metadata":{"trusted":false},"cell_type":"code","source":"# для батчей нужно использовать bmm вместо matmul\n# Performs a batch matrix-matrix product of matrices stored in :attr:`input` and :attr:`mat2`\n\nBATCH_SIZE = 32\n\nencoder_hidden = torch.rand(BATCH_SIZE, ENCODER_WORDS, EMBEDDING_DIM)\ndecoder_hidden = torch.rand(BATCH_SIZE, DECODER_WORDS, EMBEDDING_DIM)\n\n# .transpose(1, 2) тоже транспонирование, только явно указываем \nattention_scores = torch.bmm(decoder_hidden, encoder_hidden.transpose(1, 2))\n\n# заметим, что у нас добавилась одна размерность и поэтому чуть меняем софтмакс\nattention_distribution = torch.softmax(attention_scores, 2)\nattention_vectors = torch.bmm(attention_distribution, encoder_hidden)\n\ndecoder_with_attention = torch.cat([decoder_hidden, attention_vectors], dim=-1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"decoder_with_attention.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.7.3"}},"nbformat":4,"nbformat_minor":1}