import torch
from torch import Tensor

from config import Config
import torch.nn as nn
from transformers import BertConfig

config = Config()
bert_config = BertConfig.from_pretrained(config.bert_path)

class BiLSTMStudentModel(nn.Module):
    def __init__(self):
        # 1- 初始化父类
        super().__init__()

        # 2- 设置属性值
        self.vocab_size = bert_config.vocab_size
        self.embedding_dim = 128
        self.hidden_size = 256
        self.num_layers = 3

        # 3- 搭建网络结构
        # 3.1- 词嵌入层
        # embedding_dim：由我们自己设置，与教师模型没有任何关系
        self.embedding = nn.Embedding(num_embeddings=self.vocab_size,embedding_dim=self.embedding_dim)

        # 3.2- 循环神经网络层：BiLSTM
        """
            参数解释：
                input_size：输入的词向量维度。必须和上面的embedding_dim相同
                hidden_size：隐藏层向量维度。大小自定义
                num_layers：隐藏层的层数。大小自定义，与是否是双向无关
                batch_first：输入和输出的数据中，batch_size是否在张量的第一个位置上
                bidirectional：是否是双向LSTM
        """
        self.lstm = nn.LSTM(
            input_size=self.embedding_dim,
            hidden_size=self.hidden_size,
            num_layers=self.num_layers,
            batch_first=True,
            bidirectional=True
        )

        # 3.3- 随机失活层
        self.dropout = nn.Dropout(p=0.3)

        # 3.4- 线性层
        """
            为什么是self.hidden_size*2？
            答：因为是双向的LSTM。双向的LSTM执行完以后，会将两个方向的隐藏状态信息进行concat拼接，形状变成两倍了
        """
        self.linear = nn.Linear(in_features=self.hidden_size*2,out_features=config.classname_len)

    def forward(self,input_ids, attentition_mask):
        """
        :param input_ids: 句子中词索引
        :param attentition_mask: 输入句子的掩码
        :return:
        """

        # 1- 调用词嵌入层，得到词向量
        # 张量形状：[batch_size, seq_len, embedding_dim]
        ebd = self.embedding(input_ids)

        # 2- 对词向量进行掩码操作：最终目的是只对正常词进行处理，句子开头、句子结尾、padding填充的0这三者不关心
        cls_token_index = 101   # 句子开头
        sep_token_index = 102   # 句子结尾
        ebd_mask = (input_ids!=cls_token_index) & (input_ids!=sep_token_index)
        ebd_mask:Tensor = ebd_mask & attentition_mask

        # 对词向量进行掩码操作
        # [batch_size, seq_len] 升维至 [batch_size, seq_len, 1]
        ebd_mask = ebd_mask.unsqueeze(-1)
        ebd = ebd * ebd_mask

        # 3- 调用循环神经网络BiLSTM
        # 为什么调用lstm的时候，没有手动传递初始的细胞状态和隐藏状态：LSTM内部会自动的进行全0初始化。源代码在1056行
        output,(hidden, c) = self.lstm(ebd)

        # 4- 计算平均池化值
        """
            下面两个地方都是sum(dim=1)的原因：
                1- output的张量形状[batch_size,seq_len,hidden_size]
                2- 我们的业务需求是对句子进行分类，因此主体是句子
                3- sum(dim=1)得到的结果形状[batch_size,hidden_size]。形状的含义是把一条句子中所有词的词向量加起来，
                    得到句子级别的向量总和
                    
        """
        # 分子：所有有效的词的向量之和
        output_sum = output.sum(dim=1)
        # 分母：所有有效的词总数。1e-6为了防止分母为0
        token_count = ebd_mask.sum(dim=1) + 1e-6
        new_output = output_sum/token_count

        # 5- 调用线性层，得到预测结果，并返回
        return self.linear(self.dropout(new_output))

if __name__ == '__main__':
    # 编写测试数据
    input_ids = torch.LongTensor([[101, 2, 3, 4, 5], [4, 102, 6, 7, 8]])
    attention_mask = torch.LongTensor([[1, 1, 1, 1, 0], [1, 1, 0, 0, 0]])
    model = BiLSTMStudentModel()
    print(model(input_ids, attention_mask))
