AI建模三问

 
输入是什么(现在有什么)
输出是什么(想要什么)
要解决什么问题(要怎么做呢)

什么是seq2seq

 
输入:m个向量,一个向量组A,由一组向量构成一个序列,m>1
输出:n个向量,另外一个向量组B,n>1 且输出的向量之间是有顺序的 

B中的每个bi向量,都是 A中所有向量 + B中bi之前的向量 综合计算的结果  
比如树枝的生长,每成长一步,就是建立在之前成长的基础上的
比如事物的运动,每状态改变,就是建立在之前状态的基础上的

当然了,
无尽的变化在不同的范围内有固定的规律,
seq2seq的任务就是提取这些变化的共性,
不同的状态,相似的轮回... 

通常情况下说的 seq2seq,
向量组A 之间的向量有依赖关系
向量组B 之间的向量有依赖关系

这个依赖关系,在代入程序中计算时,
通常转化为前后依赖关系,转换为一一映射,
实际上,依赖关系不止前后依赖这一种(关系通常是个网),
但只要 在时间上 有前后依赖关系,那么就属于序列问题

 

    

 
单纯使用RNN的seq2seq处理的序列长度大约在20

加上注意力后,能处理更长的序列 
    

 

    

 

    

 
序列到序列是生成的,根据已有序列的规律,生成一个数据...
然后循环这个过程,直到结束  

 


 

  

 


NLP序列类问题

应用场景

 
降维:用少量特征来 代表 原来的多特征

翻译:一段文本转另外一段文本

语音转文本,

序列信号,

时序数据

 

    

前后依赖关系

 
序列 为 前后存在依赖关系的数据
当然了,人看数据都是线性的,
不管原来是个什么结构,往数组里一扔,就开始各种处理

有些数据虽然在数组中对应着索引的位置,但实际上没有前后依赖关系
是否有前后依赖关系,计算机并不知道,由业务含义决定

全连接没有前后依赖关系,它只是提取特征
这是由全连接的计算方式决定的,
全连接是 所有参数与数据相乘再相加,
不管原来特征什么顺序,最后一相加,结果都一样

 

    

卷积计算没有前后依赖关系

 
全连接计算没有依赖关系,那卷积呢?

对于单通道,一个kernel矩阵在特征shape上进行局部全连接计算,
多通道时,各个通道与各自的kernel参数矩阵分别计算后,
然后相加,即先将多个通道的信息融合到一起,再多维度分层 
(卷积后通常紧跟一个批归一化,不然通道数太多,相加后就大于1了)

这个过程是有顺序的,即原特征图的左上角对应新特征shape的左上角
但这个顺序没有前后依赖关系,是可以并行计算的,

注意,这里说的是卷积计算没有前后依赖关系,
并不是说卷积计算的结果之间没有顺序
卷积不像RNN那样,当前单元依赖于上一个单元的输出

虽然一次卷积计算涉及了所有通道上的所有特征图,
但损失函数会逼迫大部分参数归于0 
最后只留下特定区域的参数


 
卷积开始先将通道数提升,最后再抛弃或者说将大部分通道上的参数归于0 
这是个暴力计算的过程,提前根本不知道,也不管 那些通道有用

换个方式再描述下:
一张白纸滴上一点墨水,大片白色中有个区域是黑色的,
卷积就可以把墨水和白色部分 分开,图像分割就是做这个的,
主体就是分层,一个特征图分散成N个特征图,
通过一层又一层计算以及损失函数的逼迫,
黑色到一张特征图,白色到另外一个特征图上了

 
总体趋势 
- 特征图在收缩 
- 通道维数在增加
- 即将特征从形状上转化到特征的维度上
    

 
------------------------------------------------------------------------------------------

区域特征倒底是什么特征?

 
一张照片一朵花,
花在图像上左上,右下,中间都是花的图片,
它不会因为位置不同而变成动物图片,
它跟顺序关系不大,
就算花朵旁边还出现一个小草,
不会因为小草在花的左边还是右边而产生严重的业务差异,

这个图像只有因为某个区域是花,就叫花的图片 
这就是区域特征

序列特征是什么特征?

 
就是调整一下顺序会得到不同的业务含义
我欠银行一百万,银行欠我一百万,这两句话的业务含义相差可太远了 
区域特征中交换两个 主体 位置 业务含义几乎是不变的

有些问题可能界限并不明显,
比如情感识别,只要话语中出现负面语汇,不管是在开头,中间,还是结尾,
它都是负面情绪了,用卷积是可以做的

当然了,文本分类通常是序列问题... 这也说明,序列类算法未必就不能处理区域问题
后来出现的注意力机制,即可以提取区域特征,又可以提取序列特征,就说明了这一点 

 

  

 


seq2seq学习过程

总体流程

 
先建立一组组/一对对映射关系,让计算机去学习
然后问上半对,计算机回答你 “曾经学习过的” 下半对

你无法描述你认知之外的事物,计算机也无法回答它没学过的知识
都到这了,那什么是知识?用数学怎么描述?

知识 就是 一对对映射关系

煤是黑的
墙是白的
水可以喝
米饭能吃

不与外界其他事物建立映射关系的事物,不可被描述,不可被认知 
描述一个事物,总是用其他事物来描述,总是在找他们的共性/联系 

序列到序列,就是知识的一种描述方式,是一对对映射关系... 

 

    

字到字

 
芝麻开花,节节高

start芝麻开花,节节高end

start -->芝
芝-->麻
麻-->开
开-->花
花-->,
,-->节
节-->节
节-->高
高-->end

词到词

 
芝麻开花,节节高
start芝麻开花,节节高end

start-->芝麻
芝麻-->开花
开花-->,
,-->节节高
节节高-->end

多个单词到词,同时抹去标点符号

 
芝麻开花,节节高
start芝麻开花,节节高end

start-->芝麻开花 
芝麻开花-->节节高
节节高-->end

 

    

映射关系的叠加

 
文本向量化后,字或词 变成一个个向量,
词到词的映射转化为 向量到向量之间的映射 

现阶段,AI中能用到的向量之间的关系,简单且粗糙,
就是个距离远近的关系,
两个的向量之间的远近用“一个数值”来表示 

用 距离 表示 相似/相近程度

 
语言,描述的事物包罗万象,
结果组成它们之们的单词的关系,只是 “单个数字”就表示了
但实际上已经可以解决很多粗糙的问题了,
当然了,过于精细的还不行

话题收回来,
我们想要序列中相近的单词,转化为向量后,其距离也能近一些

怎么做?
简单粗糙的做法,就是前面的做法,
两个单词靠近,直接建立一个映射关系就行了

多层面/多角度收集信息

 

感觉这样不够,可以把周围的单词也考虑进去,
比如, 以开花为 中心词 
芝麻-->节节高 
也可以是一个映射关系 

还不够?
把词性也用上,主谓宾丁壮补 就是序列中的隐藏序列 
语法结构的信息,也融入

还不够?那就再细化 
同义词,用上 
成语/专有名词,单独训练一下 

再不够?不仅佃化,还要深入
特殊场景 加权处理 

融入规则

 
仍不够?精准化
规则匹配,建立特殊单词到特定单词的规则 
哪怕这个单词从开始到结尾总共出现了一次,但你加了规则,
一经出现,必定触发特定操作

七分规则 ,三分概率,这系统即智能又稳定 


 
---------------------------------------------------------------------------------

 

  

 


seq2seq预测过程

常规做法

 
预测,
就是将一个序列的向量A输入模型,模型输出一个新向量B 
新向量B,不会与任何已有单词向量 完全相等 

这里还需要计算 新向量B 与 所有已有向量之间的距离,
与之最近的那个向量 定为 模型的输出 

没错,
模型直接的输出结果并不是我们期望的标签,这里需要一步转化操作
不仅如此,
按神经网络的常规操作,
不用计算就知道 模型输出 离哪个单词向量最近

 
单词索引编码后,化为一个个从0开始的整数,
设定不重复的单词个数为n,
单词的索引编号,
恰恰是 长度为n的one hot向量V 中唯一的1所在的索引编号 
将单词进行one hot编码,模型的输出直接就是单词集合的长度
这样就免去计算就知道是哪个单词了

但实际上没人这么做,因为单词个数太多了,计算机算不动
    

 

    

 

    

 


 

  

 


编码器与解码器

输入是一个序列,输出是另外一个序列

 
NLP有关序列的问题:主要有 编码器,解码器 两大模块 

编码器是一个序列,解码器是一个类别,典型的文本分类问题
编码器是一个序列,解码器是一个序列,典型的seq to seq问题

这二类问题,解码器中的每个部分,受 编码器所有部分影响
即编码器计算后得到一个整体的特征,供解码器使用

这就是一个序列到另外一个序列 

编码器,解码器是从 编程/代码实现 的角度看 序列到序列  
编码器对应前一个序列 
解码器对应后一个序列

 

    

编码器-模板类参数

 
数据X[sql_len,batch_size,hidden_size]
一个序列中所有的单词一次性全输入进去了
因此,可以使用双向这个参数,正一条RNN链,反再来一条 
层数为2,可以更好地提取特征 

编码器-对象参数:
数据X,mask标记(记录哪个地方是PAD)


解码器-模板类参数

 

数据X[1,batch_size,hidden_size]

一次只输入一个单词,这个单词最终要映射到字典的维度,就是标签的维度

不管中间增加多少技巧,解码器的终极目的,就是 输入单词 映射到 字典的某个单词上 

输入单词 -- 字典中某个单词
因此,输出的维度必不可少,且要清楚要与标签维度对应 
    

 
是否双向,
由于解码器是根据当前单词预测下一个单词,
与编码器一次可逐步计算一个序列中所有单词是不同的,
因此,解码器只有单向,一个方向

层数,
层数为2比1能更好地提取特征,但编码器可100%确定一个序列中所有的单词都是正确的,
而解码器的单词是预测出来的,某个单词预测错了,再深度提取特征也没用,
因此解码器的层数为1  
    

 
解码器-对象参数:
单个单词,上个单元的输出(隐藏层)
该单词相对整个编码序列的上下文对象 
    

 

    

 
为了批次计算,深度学习都是批次,对数据进行补0对齐 

如果是短句,那么大部分是0,好处是可以批次计算了,缺点如下 
- 计算量大了,因为多了很多不属于原来的数据 
- 增加了噪声,原来的数据中根本就没有“补充”的数据,AI学了之后,反之形成不好的影响 

为什么要批次计算,单个样本不行吗?
- 行,但损失会剧烈波动,效率也没有批次计算高 
- 现在的损失都是批次中所有样本损失的均值,平均一下,看起来平滑多了
- 两者本质上没啥区别,单样本也是行的,但看着舒服一些,也不影响效果也是可以的吧?!
- 当然可以 

于是就有了下面的方法,即能批次运算,模型学习时也无噪声

编码器模型

 
class EncoderRNN(nn.Module):
  """编码模型
    - 从批次数据中pack_pad出真实数据
    - 将真实数据输入模型
    - 将模型输出进行pad_pack得到批次数据
    
  输入数据
    - 要求输入的数据格式为[seq_len,batch_size]
    - 通常为单词索引列表
  """
  def __init__(self, hidden_size, embedding, n_layers=1, dropout=0):
      super(EncoderRNN, self).__init__()
      self.n_layers = n_layers
      self.hidden_size = hidden_size
      self.embedding = embedding

      # Initialize GRU; the input_size and hidden_size params are both set to 'hidden_size'
      # because our input size is a word embedding with number of features == hidden_size
      self.gru = nn.GRU(hidden_size, hidden_size, n_layers,
                        dropout=(0 if n_layers == 1 else dropout), bidirectional=True)

  def forward(self, input_seq, input_lengths, hidden=None):
      # Convert word indexes to embeddings
      embedded = self.embedding(input_seq)

      # Pack padded batch of sequences for RNN module
      packed = nn.utils.rnn.pack_padded_sequence(embedded, input_lengths)

      # Forward pass through GRU
      outputs, hidden = self.gru(packed, hidden)

      # Unpack padding
      outputs, _ = nn.utils.rnn.pad_packed_sequence(outputs)

      # Sum bidirectional GRU outputs
      outputs = outputs[:, :, :self.hidden_size] + outputs[:, :, self.hidden_size:]

      # Return output and final hidden state
      return outputs, hidden

 
总体上来说,就是一个GRU,很简洁 
- GRU接受一个句子,输出out,hn 
- out包含序列上每一步的输出,有整个句子的信息,是个序列 
- 即一个GRU就是一个编码器 

 
同时增加了两个处理技巧 
- GRU模型计算的数据是无补的,
- 之前数据加工时,记录了每个句子的长度,
- 在入模之前根据这个长度,取出了真实的数据,没有补的数据 
- 处理之后,又恢复补0对齐,恢复了原来的形状 

处理之前,有单词的位置有信息,无单词的位置补的0
处理之后,有单词的位置有信息,无单词的位置还是0 
seq单词的位置是一对一的

 
双向GRU,官方说法是在起点也包含了未来的信息
GRU算法如果是双链,处理方法是拼接,前半部分为正向,后半部分为反向
在seq2seq中为了保持形状的一致,将二者进行了相加 

 


注意力机制来由

 
seq2seq中编码器传给解码器的hn,隐藏状态,状态不是很好 

解决办法是,求解码器中每个单词,就是每个单词+该单词相对编码器重要的信息
- 对于解码器中不同的单词,它们相对于编码器的重要程度是不一样的 
- 反过来说也是如此,编码器中的信息相对于解码器中不同的单词有不同的侧重点 
    
怎么做?

注意力权重的使用

 
解码器的一个单词D[1,hidden_size]
编码器信息E[seq_len,batch_size,hidden_size]
    
D[1,hidden_size] -- D[seq_len,hidden_size] 复制seq_len份,是编码器的单词个数 

E[seq_len,batch_size,hidden_size] -- E[batch_size,seq_len,hidden_size]
D[seq_len,hidden_size] -- D[1,seq_len,hidden_size]


torch.sum(D * E, dim=2)
- D[1,seq_len,hidden_size] * E[batch_size,seq_len,hidden_size]
- 注意,这是按位相乘再相加 
- 得到 A[batch_size,seq_len],这一步就是注意力,也叫注意力权重 

它是的shape是[batch_size,seq_len],即批次中每个单词相对另外一个句子都有一个概率值 
- 另外一个句子中的每个单词与该单词都有一个重要性数值,即概率值 
- 其和为1 
- 第2维的个数为另外一个句子的单词个数 
- 第1维是批次数 

 
注意力权重并不是最终的目的,目的是找到解码器单词相对编码器信息的关键信息 
所以还要使用注意力权重乘到编码器信息上,这次使用的是矩阵相乘  
A[batch_size,seq_len] -- A[batch_size,1,seq_len]

A[batch_size,1,seq_len]@E[batch_size,seq_len,hidden_size] = C[batch_size,1,hidden_size]
    
C[batch_size,1,hidden_size] -- C[batch_size,hidden_size]
最终上下文向量的维度就是hidden_size

代码

 
# Luong attention layer
class Attn(nn.Module):
    def __init__(self, method, hidden_size):
        """ 
        描述
          - 计算一句话中各个单词对输入单词的重要程度;
        
        输入参数
          - hidden:输出单词
          - hidden的shape皆为[1, batch_size, hidden_size]
          - encoder_outputs.shape=[seq_len, batch_size, hidden_size]
          
        返回结果
          - 一个单词hidden与encoder_outputs seq_len个单词的得分(做了softmax总和为1)
          - 返回结果shape=[batch_size,1,seq_len]
          - sum(dim=2)=1,编码一句话中各个单词对输入单词的重要程度
        """
        super(Attn, self).__init__()
        self.method = method
        if self.method not in ['dot', 'general', 'concat']:
            raise ValueError(self.method, "is not an appropriate attention method.")
        
        self.hidden_size = hidden_size
        
        if self.method == 'general':
            # 矩阵相乘就是一系列向量内积
            self.attn = nn.Linear(self.hidden_size, hidden_size)
        elif self.method == 'concat':
            self.attn = nn.Linear(self.hidden_size * 2, hidden_size)
            self.v = nn.Parameter(torch.FloatTensor(hidden_size))

    def dot_score(self, hidden, encoder_output):
        # hidden.shape=[1,64,500],encoder_output.shape=[10,64,500]
        # 1个单词,64个批次,500为hidden的大小,这是按批次 对一个单词 转换后的矩阵
        # MAX_LENGTH = 10,句子最大长度为10,最后一个字符为EOS
        # 单词维度 位乘(按位置相乘),再相加,sum之后单词这个维度消失
        # [sel_len,batch_size]
        # [1,64,500][10,64,500]=> [10,64,500][10,64,500]=>[10,64]=[sel_len,batch_size]
        # 位乘再相加,实际就是向量点乘,通常的点乘指两个向量之间的位乘再相加
        # 一个单词长度为hidden_size的向量,这样的单词有seq_len*batch_size个
        # 将它们按[sel_len,batch_size,hidden_size]的方式存放
        # 真实计算的时候,仍然是两个单词(hidden_size维度)之间的点乘
        # 每两个单词之间向量点乘之后,得到一个数字,这个数字近似代表了两个单词之间的相似程度
        # 这个单词与[sel_len,batch_size]个单词进行了点乘,就得到了[sel_len,batch_size]个结果
        # 这就是序列到序列,注意力提取特征的关键
        # 矩阵乘法是向量内积按一定格式/规律计算的过程,前一个矩阵的首与后一个矩阵的尾维度相等,首尾同
        # 而向量内积的使用,除了矩阵乘法的计算方法外,
        # 还可以按矩阵shape一致的方式计算,首与首同,尾与尾同,即同shape计算
        # 这里不影响,这里计算的是一个单词相对一个句子的注意力 
        return torch.sum(hidden * encoder_output, dim=2)

    def general_score(self, hidden, encoder_output):
        # 相当于用ht向量与编码层的每个向量进行向量内积运算
        # 得到编码层每个单词输出的得分,或叫做百分比
        energy = self.attn(encoder_output)

        # 编码层的输出与得分点乘再相加
        return torch.sum(hidden * energy, dim=2)

    def concat_score(self, hidden, encoder_output):
        energy = self.attn(torch.cat((hidden.expand(encoder_output.size(0), -1, -1), encoder_output), 2)).tanh()
        return torch.sum(self.v * energy, dim=2)

    def forward(self, hidden, encoder_outputs):
        """
        描述
          - 计算一句话中各个单词对输入单词的重要程度;
        
        输入参数
          - hidden:输出单词
          - hidden的shape皆为[1, batch_size, hidden_size]
          - encoder_outputs.shape=[seq_len, batch_size, hidden_size]
          
        返回结果
          - 一个单词hidden与encoder_outputs seq_len个单词的得分(做了softmax总和为1)
          - 返回结果shape=[batch_size,1,seq_len]
          - sum(dim=2)=1,编码一句话中各个单词对输入单词的重要程度
        """
        # Calculate the attention weights (energies) based on the given method
        if self.method == 'general':
            attn_energies = self.general_score(hidden, encoder_outputs)
        elif self.method == 'concat':
            attn_energies = self.concat_score(hidden, encoder_outputs)
        elif self.method == 'dot':
            attn_energies = self.dot_score(hidden, encoder_outputs)

        # Transpose max_length and batch_size dimensions
        # [sel_len,batch_size] -- [batch_size,sel_len]
        attn_energies = attn_energies.t()

        # 按seq_len维度转为概率,[batch_size,seq_len]
        # [batch_size,seq_len] -- [batch_size, 1, seq_len] 添加这个1是为了后面进行bmm
        # 因为计算的是一个单词相对一个句子的注意力,这个1也可以理解为1个单词,即这个维度的业务含义是seq_len
        # [batch_size, 1, seq_len]中的seq_len的业务含义则是重要百分比,一个单词相对句子的重要百分比
        return F.softmax(attn_energies, dim=1).unsqueeze(1)


 


 


单个单词gru映射 拼接 注意力

 
decoder是一个单词一个单词计算的,对于每个单词来说 
- 使用gru将单词的维度映射到hidden_size,记为out,hn,gru不限制序列长度,一个单词也能算
- 使用out计算编码序列整个句子的注意力,即一个单词相对一个句子的注意力,
- 得到的上下文与out拼接成一个2倍hidden_size的向量,tanh激活函数处理将值域转换到[-1,1]之间 
- 再使用线性变换将维度从2*hidden_size变换到hidden_size 
- 最后一层全连接是分类,将数据的维度从hidden_size转换到字典的维度
  

 
class LuongAttnDecoderRNN(nn.Module):
  def __init__(self, attn_model, embedding, hidden_size, output_size, n_layers=1, dropout=0.1):
      """
      输入参数
        - 一个批次某个位置上的单词,单词维度是索引,格式为[1,batch_size]
        - 隐藏层,编码器最后一次的输出,或者上一个解码器的输出
        - 解码层
        
      输出参数
        - 单词向量
          - 被预测的单词,经过了注意力计算,shape为[batch_size,dict_len]
          - 这里用[dict_len]长度的向量表示一个单词,并且使用softmax转概率,其和为1
          - 如此,就可以使用交叉熵逼迫其与one-hot意义的单词标签接近
        - gru输出的隐藏层

      批次计算每个单词的概率,最终将单词维度映射到字典,确定它是哪一个单词,然后解码输出一个单词
      """
      super(LuongAttnDecoderRNN, self).__init__()

      # Keep for reference
      self.attn_model = attn_model  # dot
      self.hidden_size = hidden_size # 500
      self.output_size = output_size # voc.num_words 7826
      self.n_layers = n_layers       # 2
      self.dropout = dropout

      # Define layers
      self.embedding = embedding
      self.embedding_dropout = nn.Dropout(dropout)
      self.gru = nn.GRU(hidden_size, hidden_size, n_layers, dropout=(0 if n_layers == 1 else dropout))
      self.concat = nn.Linear(hidden_size * 2, hidden_size)
      self.out = nn.Linear(hidden_size, output_size)  # 500 --> voc.num_words

      self.attn = Attn(attn_model, hidden_size)

  def forward(self, input_step, last_hidden, encoder_outputs):
      """根据前一个单词预测后一个单词

      Args:
          input_step (_type_): 某个位置一个批次的单词索引,shape=[1, 64]
          last_hidden (_type_): 隐藏层,shape=[2, 64, 500]
          encoder_outputs (_type_): [10,64,500]

      Returns:
          output: 单词向量,[64, 7826],转了概率,方便后续与标签求损失
          hidden: 隐藏层,中间结果,[2, 64, 500]
      """
      # Note: we run this one step (word) at a time
      # Get embedding of current input word
      embedded = self.embedding(input_step)      # [1, 64, 500]
      embedded = self.embedding_dropout(embedded)

      # Forward through unidirectional GRU
      # 每次只输入一个单词,
      # 因此rnn_output, hidden的shape皆为[1, batch_size, hidden_size]
      rnn_output, hidden = self.gru(embedded, last_hidden) 

      # Calculate attention weights from the current GRU output
      # [batch_size,seq_len] -- [batch_size, 1, seq_len]
      # 1个单词与一个句子求注意力
      attn_weights = self.attn(rnn_output, encoder_outputs)
      
      # Multiply attention weights to encoder outputs to get new "weighted sum" context vector
      # encoder_outputs.shape=[seq_len,batch_size,hidden_size]
      # encoder_outputs.transpose(0, 1).shape = [batch_size,seq_len,hidden_size]
      # attn_weights.shape = [batch_size, 1, seq_len]
      # bmm [1, seq_len]@[seq_len,hidden_size] = [batch_size,1,hidden_size]
      # context.shape = [batch_size,1,hidden_size]
      # decoder中每个单词对应一个encoder的上下文向量
      # 加权平均,[1, seq_len] sum(dim=1)=1,注意力的得分之和为1,将这个得分乘到原来解码器上
      context = attn_weights.bmm(encoder_outputs.transpose(0, 1))

      # Concatenate weighted context vector and GRU output using Luong eq. 5
      # rnn_output.shape = [1, batch_size, hidden_size]
      # rnn_output.squeeze(0).shape = [batch_size, hidden_size]
      # decoder中每个单词的输出
      rnn_output = rnn_output.squeeze(0)

      # context.shape = [batch_size,hidden_size]
      # decoder中每个单词对应encoder中的上下文向量
      context = context.squeeze(1)

      # 将从encoder中得到的上下文向量拼接到decoder中每个单词的输出维度上
      # concat_input.shape=[batch_size,hidden_size*2]
      concat_input = torch.cat((rnn_output, context), dim=1)

      # 全连接,线性变换,再tanh,[batch_size,hidden_size*2]-- [batch_size,hidden_size]
      concat_output = torch.tanh(self.concat(concat_input))

      # Predict next word using Luong eq. 6
      # [batch_size,hidden_size] -- [batch_size,output_size]
      # output_size为标签的个数
      output = self.out(concat_output)
      
      # 这里转了概率,单词维度之和为1
      output = F.softmax(output, dim=1) # 转概率,不改变维度,[64, 7826]
      # Return output and final hidden state
      # hidden:普通的gru,一个单词通过gru得到的输出,[2, 64, 500]
      # output:经过gru+attention,然后转标签概率
      return output, hidden

  

 

  

 

  

 

  

编码器与解码器的训练

 
序列是成对出现的(编码器序列1,解码器序列2)

编码器对序列1提取特征得到一个状态0
解码器:
  输入序列2的START标记+状态0
  输出为 序列的第1个单词+状态1

  输入序列2的第1个单词+状态1
  输出为序列的第2个单词+状态2
  
  每次输入一个单词+前面的状态,
  输出一个单词+一个状态 

  输入序列2的第n个单词+状态n
  输出END标记+状态n+1 

 


编码器与解码器的预测

 
输入序列1,预期的是,得到序列2

编码器对序列1提取特征得到一个状态0
解码器:
    输入序列2的START标记+状态1
    输出 单词1+状态1

    输入单词1+状态1
    输出一个单词2+状态2

    每次输入上个单元输出的单词+前面的状态,
    输出一个单词+一个状态 

    输入序列2的第n个单词+状态n
    输出END标记+状态n+1 

解码器 根据前面的输出,推断下一个输出大概率是什么
这是一个续补的过程,能依据的只有前面的序列信息
然后要给出缺失的部分

 


 


 


 


seq2seq损失函数

 
以单词为例,文本向量化后,一个单词对应一个向量

学习的理想效果是一个固定的向量 至 另外一个固定的向量

但这只是理想罢了

实际上模型预测 出来  一个新的向量,
并且这个新向量不与任何已有的单词匹配相等

通过梯度下降法 不断调用模型的参数,可以让模型的输出不断地向已有的单词向量靠近
  

总体思路

 
回归最初,模型输出一个新的向量,

要解决的问题是
这个新向量 与 已有单词向量相比,离哪个最近

这个问题,直接逆向决定了,单词如何编码,损失函数如何设计

这里先说一个结果,
序列到序列的预测,是要模型输出一个单词的概率 

 

    

 
def maskNLLLoss(inp, target, mask):
  """
  功能描述
    - 计算模型预测单词与target标签单词之间距离
    
  输入参数
    - inp: 某个位置一个批次的单词,inp.shape=[64, 7826] = [batch_size,dict_len]
    - target: 对应答句相应位置单词的索引
    - mask: 该位置是单词还是pad 
  输出参数
    - 批次平均损失
    - 批次中单词个数
  
  --------------
  按位置计算,一次计算某个位置上一个批次的单词;
  不同的句子长度不一致,所有句子在批次处理时长度皆为MAX_LENGTH
  mask记录对应位置是否为单词,是单词为True,PAD为False
  比如某句子长度为8,那么这句话9这个位置上就是PAD,
  9这个位置对应的mask为False
  """
  # 某个位置一个批次有多少个单词
  nTotal = mask.sum() # 表示一个批次的某个位置上有多少个单词,1表示有单词,0表示无单词

  print(f"target.shape={target.shape}")  # target.shape=torch.Size([64])
  index = target.view(-1, 1)  # [64, 1]


  # 取与 标签对应索引位置 上的模型输出的 数据,其他的都不要了
  # 比如某个单词在原dict_len=7826这个字典的索引为100,那么其他位置上的值都不是该单词
  # output的最后一维与dict_len=7826这个字典索引对应
  # 所以,若标签这个位置应该被预测为索引100对应的单词时,
  # 那么output索引100对应的数值应该概率最大
  # 针对每个单词,先获取标签单词在字典中的索引下标,从one-hot的角度看,只有该位置为1,其他位置皆为0
  # 同样取出模型输出的单词维度对应索引下标上的数据,该数据将与1通过损失函数进行校正
  # 让模型输出的单词维度相同索引下标的数据不断接近1,让损失慢慢减少
  # inp[batch_size,dict_len]单词维度dict_len做了softmax,其和为1
  # 当某个索引位置上的数据接近1时,其他位置将趋于0
  y = torch.gather(inp, 1, index)  # [64,1]
  y = y.squeeze(1)  # [64] 

  # 交叉熵计算,计算一个批次的两个分布之间的距离,模型预测的单词与标签单词的距离
  # -(lable*log(y) + (1-lable)*log(1-y))
  # lable = 1 , -log(y)
  # crossEntropy = -torch.log(torch.gather(inp, 1, index).squeeze(1))
  # 分布的维度就是批次的维度,因此可以做交叉熵 
  crossEntropy = -torch.log(y)  # [64]

  # 所有单词位置上的损失的均值 
  # 取有单词的位置上的数据,不计算PAD
  loss = crossEntropy.masked_select(mask).mean()
  loss = loss.to(device)
  return loss, nTotal.item()


    

 

    

 

    

 
# Initialize variables
loss = 0
print_losses = []
n_totals = 0

for t in range(max_target_len): # 解码序列有多少个单词,就循环多少次
  # decoder_input每次从target_variable取出一个单词
  # decoder_input.shape=[1, 64]
  # decoder_hidden.shape=[2, 64, 500]
  # decoder_output=[64,dict_len]
  decoder_output, decoder_hidden = decoder(
      decoder_input, decoder_hidden, encoder_outputs
  )
  
  # Teacher forcing: next input is current target
  # 取当前序列需要输入的单词
  # decoder_input.shape=[1,64]
  # target_variable.shape=[10,64]
  # target_variable是答句的word2index列表,还没有embedding
  # 一次输入某个序列位置上一个批次的单词 
  # [64] -- [1,64]
  # 从第二次循环开始,输入的是单词[seq_len,batch_size],其中seq_len=1
  decoder_input = target_variable[t].view(1, -1)

  # Calculate and accumulate loss
  # mask[t]表示t位置是单词还是PAD标记,为单词时为True
  # target_variable[t]是t位置一个批次单词的索引
  mask_loss, nTotal = maskNLLLoss(decoder_output, target_variable[t], mask[t])
  loss += mask_loss

# Perform backpropatation
loss.backward()

 


 


 

  

 


搜索与匹配:规则与AI的选择

模糊还是精准

 
闲聊,模糊匹配:人工智能 

精准,不允许出错:规则匹配

规则

 
正则表达式 
能够快速从大量字符串中匹配符合格式的子串 

一对一或一对多的映射 
固定的格式,固定的关系

参考
    卷积过程详细讲解