"""
    Transformer的输入部分：包含如下内容
        词嵌入层：输入词索引，得到词向量
        位置编码
"""
import math
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import numpy as np

class Embedding(nn.Module):
    def __init__(self,vocab_size,d_model):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.vocab_size = vocab_size    # 词汇表大小
        self.d_model = d_model  # 词向量的维度。例如：512

        # 3- 搭建网络结构：只有一个词嵌入层
        self.embed = nn.Embedding(num_embeddings=self.vocab_size,embedding_dim=self.d_model)

    def forward(self,input):
        """
        前向传播。输入一条句子，得到词向量
        :param input: 一条句子，里面的元素是词索引。张量形状[batch_size每个批次中句子的条数,seq_len每条句子中词的个数]
        :return: 词向量
        """

        """
            为什么要乘以math.sqrt(self.d_model)，也就是根号dk？
            答：为了对数据进行缩放，避免与位置编码的数据值之间的大小差异过大。为了让模型训练稳定，也就是缓解梯度消失或梯度爆炸
                词向量维度越大，越容易出现极小值
        """
        return self.embed(input) * math.sqrt(self.d_model)

def use_embedding():
    # 1- 创建词嵌入层类的实例对象
    my_embed = Embedding(vocab_size=1000,d_model=5)
    # my_embed = Embedding(vocab_size=1000,d_model=10240)

    # 2- 准备数据
    # 注意：目前情况下，词索引的取值区间[0,999]
    x = torch.tensor([
        # 单词索引
        [100, 2, 666],
        [500, 888, 421]
    ])

    # 3- 输入句子，得到词向量
    word_vector = my_embed(x)
    print(f"词向量的形状{word_vector.shape}") # 2条句子，3句子中3个词，5词向量维度
    print(f"词向量的数据{word_vector}")
    print(f"词向量的数据{word_vector.abs().min()}")   # 获得乘以根号dk以后数据中绝对值的最小值

# 位置编码
class PositionalEncoding(nn.Module):
    def __init__(self,d_model,dropout,max_len=60):
        """
        位置编码初始化方法
        :param d_model: 词向量维度。例如：512
        :param dropout: 神经元随机失活概率
        :param max_len: 能够处理的句子最大长度。也就是词的个数
        """

        # 1- 初始化父类
        super().__init__()

        # 2- 创建随机失活层
        self.dropout = nn.Dropout(p=dropout)

        # 3- 定义pe(也就是位置编码张量)
        pe = torch.zeros(size=(max_len,d_model))    # 目前的形状[60,d_model]

        # 4- 定义列向量，用来存储输入句子中词索引信息
        position = torch.arange(0,max_len).unsqueeze(1) # 形状[60,1]

        # 5- 得到pos/分母公式中的，1/分母
        div_term = 1/(10000**(torch.arange(0,d_model,2).float()/d_model))

        # 6- 计算pos/分母的结果
        position_value = position * div_term

        # 7- 调用sin、cos分别计算位置编码值
        pe[:,0::2] = torch.sin(position_value)  # 偶数维度
        pe[:,1::2] = torch.cos(position_value)  # 奇数维度

        # 8- 调整pe的形状变成3维，也就是[60,d_model]->[1,60,d_model]每个批次1条句子，每个句子最多60个词，词向量是d_model
        pe = pe.unsqueeze(0)

        # 9- 将pe的值注册到缓存中，通过它来不断的更新位置编码信息。后面可以直接通过self.pe调用
        self.register_buffer("pe",pe)

    def forward(self,embed):
        """
        位置编码的前向传播：多条句子中所有词的词向量，和位置编码张量进行求和操作，返回结果
        :param embed: 词向量
        :return:
        """

        """
            embed.shape[1]：embed的形状[batch_size,seq_len,d_model]，得到多少个词
            为什么self.pe[:, :embed.shape[1]]？
            如果这条句子中词的个数超过了max_len，那么最多只对前max_len个词的位置编码进行相加
        """
        result = embed + self.pe[:, :embed.shape[1]]
        # print(f"对应的位置编码值：{self.pe[:, :embed.shape[1]]}")
        return self.dropout(result)

def use_positional_encoding():
    d_model = 512

    # 实例化词嵌入层类的对象
    my_embed = Embedding(vocab_size=1000,d_model=d_model)

    # 准备数据
    x = torch.tensor([
        # 单词索引
        [100, 2, 421, 600],
        [500, 888, 421, 615]
    ])

    # 输入数据，得到对应的词向量
    word_embed = my_embed(x)
    # print(f"词向量的形状：{word_embed.shape}")
    # print(f"词向量的值：{word_embed}")

    # 创建位置编码
    my_pe = PositionalEncoding(d_model=d_model,dropout=0.1,max_len=60)

    # 调用位置编码，最终效果是往词向量中加上了位置编码的值
    result = my_pe(word_embed)
    # print(f"最终的形状：{result.shape}")
    # print(f"最终的值：{result}")

    return result

# 可视化位置编码
def plot_position():
    # 1. 实例化位置编码器.
    my_position = PositionalEncoding(20, dropout=0.1, max_len=100)

    # 2. 生成全0的输入, 观察位置编码的模式.
    # (1, 100, 20) -> 批次大小, 句子长度, 词嵌入维度
    embed = torch.zeros(1, 100, 20)
    y = my_position(embed)

    # 3. 设置图表大小.
    plt.figure(figsize=(20, 15))
    # 绘制位置编码第4到第7列, 100个词的  [4, 5, 6, 7]
    """
        图形的信息解释：
            x轴：词的索引，目前总共有100个词
            y轴：位置编码值
    """
    plt.plot(np.arange(100), y[0, :, 4:8].detach().numpy())
    plt.legend([f'dim {p}' for p in [4, 5, 6, 7]])
    plt.show()

if __name__ == '__main__':
    # use_embedding()
    use_positional_encoding()

    # plot_position()