输入是什么(现在有什么) 输出是什么(想要什么) 要解决什么问题(要怎么做呢)
输入:m个向量,一个向量组A,由一组向量构成一个序列,m>1 输出:n个向量,另外一个向量组B,n>1 且输出的向量之间是有顺序的 B中的每个bi向量,都是 A中所有向量 + B中bi之前的向量 综合计算的结果 比如树枝的生长,每成长一步,就是建立在之前成长的基础上的 比如事物的运动,每状态改变,就是建立在之前状态的基础上的 当然了, 无尽的变化在不同的范围内有固定的规律, seq2seq的任务就是提取这些变化的共性, 不同的状态,相似的轮回... 通常情况下说的 seq2seq, 向量组A 之间的向量有依赖关系 向量组B 之间的向量有依赖关系 这个依赖关系,在代入程序中计算时, 通常转化为前后依赖关系,转换为一一映射, 实际上,依赖关系不止前后依赖这一种(关系通常是个网), 但只要 在时间上 有前后依赖关系,那么就属于序列问题 |
单纯使用RNN的seq2seq处理的序列长度大约在20 加上注意力后,能处理更长的序列 |
序列到序列是生成的,根据已有序列的规律,生成一个数据... 然后循环这个过程,直到结束 |
|
|
应用场景 降维:用少量特征来 代表 原来的多特征 翻译:一段文本转另外一段文本 语音转文本, 序列信号, 时序数据 |
前后依赖关系 序列 为 前后存在依赖关系的数据 当然了,人看数据都是线性的, 不管原来是个什么结构,往数组里一扔,就开始各种处理 有些数据虽然在数组中对应着索引的位置,但实际上没有前后依赖关系 是否有前后依赖关系,计算机并不知道,由业务含义决定 全连接没有前后依赖关系,它只是提取特征 这是由全连接的计算方式决定的, 全连接是 所有参数与数据相乘再相加, 不管原来特征什么顺序,最后一相加,结果都一样 |
卷积计算没有前后依赖关系 全连接计算没有依赖关系,那卷积呢? 对于单通道,一个kernel矩阵在特征shape上进行局部全连接计算, 多通道时,各个通道与各自的kernel参数矩阵分别计算后, 然后相加,即先将多个通道的信息融合到一起,再多维度分层 (卷积后通常紧跟一个批归一化,不然通道数太多,相加后就大于1了) 这个过程是有顺序的,即原特征图的左上角对应新特征shape的左上角 但这个顺序没有前后依赖关系,是可以并行计算的, 注意,这里说的是卷积计算没有前后依赖关系, 并不是说卷积计算的结果之间没有顺序 卷积不像RNN那样,当前单元依赖于上一个单元的输出 虽然一次卷积计算涉及了所有通道上的所有特征图, 但损失函数会逼迫大部分参数归于0 最后只留下特定区域的参数 卷积开始先将通道数提升,最后再抛弃或者说将大部分通道上的参数归于0 这是个暴力计算的过程,提前根本不知道,也不管 那些通道有用 换个方式再描述下: 一张白纸滴上一点墨水,大片白色中有个区域是黑色的, 卷积就可以把墨水和白色部分 分开,图像分割就是做这个的, 主体就是分层,一个特征图分散成N个特征图, 通过一层又一层计算以及损失函数的逼迫, 黑色到一张特征图,白色到另外一个特征图上了 总体趋势 - 特征图在收缩 - 通道维数在增加 - 即将特征从形状上转化到特征的维度上 ------------------------------------------------------------------------------------------ |
区域特征倒底是什么特征? 一张照片一朵花, 花在图像上左上,右下,中间都是花的图片, 它不会因为位置不同而变成动物图片, 它跟顺序关系不大, 就算花朵旁边还出现一个小草, 不会因为小草在花的左边还是右边而产生严重的业务差异, 这个图像只有因为某个区域是花,就叫花的图片 这就是区域特征 序列特征是什么特征? 就是调整一下顺序会得到不同的业务含义 我欠银行一百万,银行欠我一百万,这两句话的业务含义相差可太远了 区域特征中交换两个 主体 位置 业务含义几乎是不变的 有些问题可能界限并不明显, 比如情感识别,只要话语中出现负面语汇,不管是在开头,中间,还是结尾, 它都是负面情绪了,用卷积是可以做的 当然了,文本分类通常是序列问题... 这也说明,序列类算法未必就不能处理区域问题 后来出现的注意力机制,即可以提取区域特征,又可以提取序列特征,就说明了这一点 |
|
总体流程 先建立一组组/一对对映射关系,让计算机去学习 然后问上半对,计算机回答你 “曾经学习过的” 下半对 你无法描述你认知之外的事物,计算机也无法回答它没学过的知识 都到这了,那什么是知识?用数学怎么描述? 知识 就是 一对对映射关系 煤是黑的 墙是白的 水可以喝 米饭能吃 不与外界其他事物建立映射关系的事物,不可被描述,不可被认知 描述一个事物,总是用其他事物来描述,总是在找他们的共性/联系 序列到序列,就是知识的一种描述方式,是一对对映射关系... |
字到字 芝麻开花,节节高 start芝麻开花,节节高end start -->芝 芝-->麻 麻-->开 开-->花 花-->, ,-->节 节-->节 节-->高 高-->end 词到词 芝麻开花,节节高 start芝麻开花,节节高end start-->芝麻 芝麻-->开花 开花-->, ,-->节节高 节节高-->end 多个单词到词,同时抹去标点符号 芝麻开花,节节高 start芝麻开花,节节高end start-->芝麻开花 芝麻开花-->节节高 节节高-->end |
映射关系的叠加 文本向量化后,字或词 变成一个个向量, 词到词的映射转化为 向量到向量之间的映射 现阶段,AI中能用到的向量之间的关系,简单且粗糙, 就是个距离远近的关系, 两个的向量之间的远近用“一个数值”来表示 用 距离 表示 相似/相近程度 语言,描述的事物包罗万象, 结果组成它们之们的单词的关系,只是 “单个数字”就表示了 但实际上已经可以解决很多粗糙的问题了, 当然了,过于精细的还不行 话题收回来, 我们想要序列中相近的单词,转化为向量后,其距离也能近一些 怎么做? 简单粗糙的做法,就是前面的做法, 两个单词靠近,直接建立一个映射关系就行了 多层面/多角度收集信息 感觉这样不够,可以把周围的单词也考虑进去, 比如, 以开花为 中心词 芝麻-->节节高 也可以是一个映射关系 还不够? 把词性也用上,主谓宾丁壮补 就是序列中的隐藏序列 语法结构的信息,也融入 还不够?那就再细化 同义词,用上 成语/专有名词,单独训练一下 再不够?不仅佃化,还要深入 特殊场景 加权处理 融入规则 仍不够?精准化 规则匹配,建立特殊单词到特定单词的规则 哪怕这个单词从开始到结尾总共出现了一次,但你加了规则, 一经出现,必定触发特定操作 七分规则 ,三分概率,这系统即智能又稳定 --------------------------------------------------------------------------------- |
|
|
常规做法 预测, 就是将一个序列的向量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 解码器 根据前面的输出,推断下一个输出大概率是什么 这是一个续补的过程,能依据的只有前面的序列信息 然后要给出缺失的部分 |
|
以单词为例,文本向量化后,一个单词对应一个向量 学习的理想效果是一个固定的向量 至 另外一个固定的向量 但这只是理想罢了 实际上模型预测 出来 一个新的向量, 并且这个新向量不与任何已有的单词匹配相等 通过梯度下降法 不断调用模型的参数,可以让模型的输出不断地向已有的单词向量靠近 总体思路 回归最初,模型输出一个新的向量, 要解决的问题是 这个新向量 与 已有单词向量相比,离哪个最近 这个问题,直接逆向决定了,单词如何编码,损失函数如何设计 这里先说一个结果, 序列到序列的预测,是要模型输出一个单词的概率 |
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() |
|
|
模糊还是精准
闲聊,模糊匹配:人工智能 精准,不允许出错:规则匹配
规则
正则表达式 能够快速从大量字符串中匹配符合格式的子串 一对一或一对多的映射 固定的格式,固定的关系
卷积过程详细讲解