摘要
本周阅读了一篇题目为Regularizing and Optimizing LSTM Language Models的文献,文中主要提出了一种在LSTM的隐藏到隐藏层之间使用DropConnect进行正则化的方法,并引入了一种平均随机梯度下降法NT-ASGD的变体,与传统SGD进行了比较。其次,对Sequence to Sequence模型进行了学习,Seq2Seq模型利用Encoder-Decoder结构,将源语言序列编码为矢量表示,然后解码为目标语言,在机器翻译等领域发挥重要作用。
ABSTRACT
This week, I read a paper titled "Regularizing and Optimizing LSTM Language Models" . In the paper, a method of regularizing LSTM by using DropConnect between hidden layers was mainly proposed, and a variant of averaged stochastic gradient descent called NT-ASGD was introduced and compared with traditional SGD. Additionally, the Sequence to Sequence model was studied. The Seq2Seq model utilizes an Encoder-Decoder structure to encode the source language sequence into a vector representation and then decode it into the target language. It plays an important role in fields like machine translation.
一、文献阅读
1、题目
题目:Regularizing and Optimizing LSTM Language Models
链接:https://arxiv.org/abs/1708.02182
2、摘要
本篇文章研究了基于LSTM的模型的正则化和优化策略,提出了一种在隐藏到隐藏的权重上使用DropConnect的一种LSTM,并且引入了一种平均梯度下降的变体:NT—ASGD。然后在Penn Treebank和WikiTex-2上实现了最先进的词级困惑度。
This paper studied regularization and optimization strategies for LSTM-based models, proposing an LSTM with DropConnect on hidden-to-hidden weights and introducing a variant of averaged stochastic gradient descent: NT-ASGD. State-of-the-art word-level perplexities were then achieved on the Penn Treebank and WikiText-2 datasets.
3、文献解读
一、Introduction
由于泛化性能关键依赖于对模型进行充分正则化的能力,比如说dropout和归一化,但是将这些方法应用于RNN的时候缺不是很成功,比说如将dropout应用于RNN的隐藏状态就是无效的,因为它破坏了RNN保持长期依赖关系的能力。文章研究了一组正则化策略,这些策略不仅非常有效,而且可以在不修改现有LSTM实现的情况下使用。同时,优化器的选择也很重要,文章引入了平均SGD(ASGD)的变体,其中调优阈值T通过非单调性准则在训练中确定。
二、创新点
1、Weight-dropped LSTM
在循环隐藏到隐藏权重矩阵上使用DropConnect,不需要对RNN的公式进行任何修改,而且对训练速度的影响最小,在LSTM内,可以防止在LSTM的循环连接上发生过拟合,这种正则化技术也适用于防止对其他 RNN单元的循环权矩阵的过拟合。由于在多个时间步长上重复使用相同的权重,因此在整个向前和向后传递过程中,相同的单个丢弃的权重仍然会被丢弃。
2、优化
在文章中,引入了平均随机梯度下降的一种变体,称为NT-ASGD,它避免了对T进行调优的需要,此外,该算法在整个实验过程中使用恒定的学习率,因此不需要对衰减调度进行进一步调优。
3、变长反向传播序列
给定一个固定的序列长度,用来将数据集分解成固定长度的批,数据集没有得到有效的利用。为了防止这种低效的数据使用,我们分两步随机选择向前和向后传递的序列长度。在训练过程中,我们根据结果序列与原始指定序列长度相比的长度来重新调整学习率。重新缩放步骤是必要的,因为以固定的学习率对任意序列长度进行采样更有利于短序列而不是长序列。
4、Variational dropout
它的一种变体,变分dropout,在第一次调用时仅对二进制dropout 掩码进行一次采样,然后在向前和向后传递的所有重复连接中重复使用锁定的 dropout 掩码。我们对所有其他dropout操作使用变分 dropout,特别是在给定的前向和后向传递中对 LS TM 的所有输入和输出使用相同的dropout 掩码。迷你批处理中的每个示例都使用唯一的dropout 掩码,而不是在所有示例中使用单个dropout 掩码,从而确保掉出元素的多样性。
三、实验过程
1、数据集
PTB:Penn tree-bank数据集:该数据集经过了大量预处理,不包含大写字母、数字或标点符号。词汇表也被限制在 10,000 个唯一的单词,与大多数现代数据集相比,这是相当小的,这导致了大量的词汇表外(OoV)标记。
WT2:Wiktext-2:,大约是 PTB 数据集的两倍。文本使用 Moses 标记器进行标记和处理,该标记器经常用于机器翻译 ,并且具有超过30,000 个单词的词汇表。 在这个数据集中保留了大写、标点和数字。
2、评估指标
数据集分割的验证集和测试集上的单模型困惑度。
3、超参数的设定
使用三层LSTM模型,隐藏层有1150个单元,嵌入大小为400,嵌入权重在[-0.1,0.1]区间内均匀初始化,以最大范数0.25进行梯度裁剪,NT -ASGD 算法进行 750 个 epoch,其中 L 等于一个 epoch , n = 5。
4、实验结果
在表 1 和表 2 中给出了我们的模型(AWD-LS TM)和其他竞争模型的 PTB 和 WT2 的单模型困惑度结果。在这两个数据集上,我们都改进了最先进的技术,我们的模型在 PTB 和 WT2 上分别比最先进的技术高出大约 1个和 0. 1 个单位。
四、结论
文章提出了权重下降 LSTM,这是一种在隐藏到隐藏的权重矩阵上使用 DropConnect 掩码的策略,作为防止重复连接过拟合的一种手段。此外,研究了使用非单调触发器的平均 SGD 来训练语言模型,并表明它在很大程度上优于 SGD。并且研究其他正则化策略,包括使用可变 BPTT 长度,并在 PTB 和 Wi kiText - 2 数据集上实现新的最先进的困惑。我们的模型优于定制的 RNN 单元和复杂的正则化策略。最后,探索了将神经缓存与我们提出的模型结合使用,并表明这进一步提高了性能,从而实现了更低的最先进的困惑。
二、seq2seq
一、seq2seq模型结构
从图中可以看出,seq2seq结构是由Encoder-Decoder组成,Encoder与Decoder中间由一个context vector组成。Encoder与Decoder中皆是RNN单元,可以是LSTM也可以是GRU,然后输入经过Encoder,得到一个输出context vector,然后context vector作为Decoder的输入进行解码,得到目标语言的句子。
在Encoder中,我们将源文本的词序列先经过embedding层转化成向量,然后输入到一个RNN结构(可以是普通的RNN,LSTM,GRU等)中,而且,这里的RNN也可以是多层,双层的,经过了RNN的一系列计算,最终隐层的输入,作为源文本整体的一个表示向量,称为context vector。
Decoder中,首先,Decoder的输入在训练和预测时是不一样的!在训练时,我们使用真实的目标文本,即“标准答案”作为输入,每一步根据当前正确的输出词,上一步的隐状态来预测下一步的输出词。
在预测时,seq2seq的内部结构:
预测时,Encoder端没什么变化,在Decoder端,由于此时没有所谓的“真是输出”或“标准答案”,所以只能自产自销:每一步的预测结果,都送给下一步作为输入,直至输出end就结束。
二、为什么训练和预测时的Decoder不一样?
为什么在训练的时候,不能直接使用这种语言模型的模式,使用上一步的预测来作为下一步的输入呢?左图为训练时使用目标文本的“标准答案”作为Decoder的输入,右图为预测时使用Decoder上一步的预测结果作为当前步的输入。
根据标准答案来decode的方式为「teacher forcing」,而根据上一步的输出作为下一步输入的decode方式为「free running」。
想一下,free running真不能在训练时使用吗?当然不是,我们尝试在训练时使用free running,但是我们发现这样训练太难了,因为没有任何的知道,一开始完全是瞎预测,而且后边的预测都要以前边的预测作为输入,所以越错越离谱,这样会导致训练时的累积损失太大,这时,用标准答案来作为预测时的输入就显得尤为重要。
所以,更好的办法,更常用的办法,是老师只给适量的引导,学生也积极学习。即我们设置一个概率p,每一步,以概率p靠自己上一步的输入来预测,以概率1-p根据老师的提示来预测,这种方法称为「计划采样」(scheduled sampling):
三、seq2seq的损失函数
在上面的图中,我们看到decoder的每一步产生隐状态后,会通过一个projection层映射到对应的词。那怎么去计算每一步的损失呢?实际上,这个projection层,通常是一个softmax神经网络层,假设词汇量是V,则会输出一个V维度的向量,每一维代表是某个词的概率。映射的过程就是把最大概率的那个词找出来作为预测出的词。
在计算损失的时候,我们使用交叉熵作为损失函数,所以我们要找出这个V维向量中,正确预测对应的词的那一维的概率大小,则这一步的损失就是它的负导数
,将每一步的损失求和,即得到总体的损失函数:
其中T代表Decoder有多少步,[EOS]代表‘end of sentence’。
四、Attention注意力机制
在Seq2Seq结构中,encoder把所有的输入序列都编码成一个统一的语义向量Context,然后再由Decoder解码。由于context包含原始序列中的所有信息,它的长度就成了限制模型性能的瓶颈。如机器翻译问题,当要翻译的句子较长时,一个Context可能存不下那么多信息,就会造成精度的下降。除此之外,如果按照上述方式实现,只用到了编码器的最后一个隐藏层状态,信息利用率低下。
所以如果要改进Seq2Seq结构,最好的切入角度就是:利用Encoder所有隐藏层状态 解决Context长度限制问题。
attention注意力机制基本思路:
Attention可以在每个时间步关注到Encoder的不同部分,以收集产生Decoder输出词所需的语义细节。Attention权重模拟了人脑注意力的分配机制,为更重要的部分分配较多的注意力。
具体的计算过程是,Seq2seq每解码一个字,就用前一个时刻的Decoder隐层状态,和Encoder所有隐层状态做soft Attention,计算公式如下,得到当前输出和所有输入元素的关联程度,即权重。进而采用加权平均的方式,计算出对当前输出更友好的context向量表示。
加入Attention后,content向量在任意时刻是变化的,那么如何计算第i时刻content向量?
1、先计算Decoder的RNN隐层状态和Encoder中所有RNN隐层状态
的Attention得分;
2、Attention得分是第i时刻的输出单词与所有输入单词
的对齐可能性(此处可以联想机器翻译任务中源词与译词一一对应,如下图中,输出“student”时,输入序列中的最后一个词的Attention权重一定是最高的);
3、再以Attention得分为权重,对所有 加权平均,得到 i时刻的content向量。
加入Attention后,Decoder各个时刻的输入包括:
- 初始时刻:
<EOS>
标记的词向量,以及当前时刻的content向量; - 其他时刻:Decoder前一时刻的输出单词的词向量,以及Decoder前一时刻的隐层状态,以及当前时刻的content向量 。
三、seq2seq手推代码
1、导入依赖包
import torch
import torch.nn as nn
import pandas as pd
from torch.utils.data import Dataset,DataLoader
import pickle
2、数据处理
-get_datas函数从CSV文件中读取英文和中文句子,并可以限定读取数量
- MyDataset类继承Dataset,实现了__getitem__和__len__方法
- __getitem__根据索引获取单个英文和中文样本
- __len__返回样本总数
- batch_data_process对batch数据进行padding并转为tensor
get_datas函数- 从csv文件中逐行读取数据,每行包含英文句子和中文句子
- 使用codecs.open打开文件,并指定utf-8编码
- csv.reader逐行读取内容,存储在datas列表中
- 可以通过n_samples参数限定读取的样本数量
- 返回英文句子列表和中文句子列表2. MyDataset类- 继承torch.utils.data.Dataset
- 构造函数中接收英文和中文句子列表
- __getitem__方法根据索引i返回第i个英文和中文句子
- __len__方法返回样本总数len(self.en)3. batch_data_process函数- 将一个batch的英文和中文句子列表转成tensor
- 计算该batch中的最大英文句子长度en_seq_len
- 对英文句子进行padding,补充至最大长度
- 计算该batch中的最大中文句子长度cn_seq_len
- 对中文句子进行padding,补充至最大长度
- 将英文和中文tensor返回
class MyDataset(Dataset):
def __init__(self,en_data,ch_data,en_word_2_index,ch_word_2_index):
self.en_data = en_data
self.ch_data = ch_data
self.en_word_2_index = en_word_2_index
self.ch_word_2_index = ch_word_2_index
def __getitem__(self,index):
en = self.en_data[index]
ch = self.ch_data[index]
en_index = [self.en_word_2_index[i] for i in en]
ch_index = [self.ch_word_2_index[i] for i in ch]
return en_index,ch_index
def batch_data_process(self,batch_datas):
global device
en_index , ch_index = [],[]
en_len , ch_len = [],[]
for en,ch in batch_datas:
en_index.append(en)
ch_index.append(ch)
en_len.append(len(en))
ch_len.append(len(ch))
max_en_len = max(en_len)
max_ch_len = max(ch_len)
en_index = [ i + [self.en_word_2_index["<PAD>"]] * (max_en_len - len(i)) for i in en_index]
ch_index = [[self.ch_word_2_index["<BOS>"]]+ i + [self.ch_word_2_index["<EOS>"]] + [self.ch_word_2_index["<PAD>"]] * (max_ch_len - len(i)) for i in ch_index]
en_index = torch.tensor(en_index,device = device)
ch_index = torch.tensor(ch_index,device = device)
return en_index,ch_index
def __len__(self):
assert len(self.en_data) == len(self.ch_data)
return len(self.ch_data)
3、模型构建
- Encoder类包含embedding层和LSTM层,对英文进行编码得到隐状态
- Decoder类包含embedding层和LSTM层,对上一时刻输出和编码器隐状态进行解码,得到当前时刻输出
- Seq2Seq模型将Encoder和Decoder组合,计算交叉熵损失函数
Encoder类- 初始化时定义输入词典大小、embedding维度和隐状态大小
- embedding层将输入词编码成向量
- lstm层循环处理输入序列,输出最后一个时刻的隐状态
- forward函数接收输入词id序列,通过embedding和lstm输出编码器隐状态2. Decoder类- 初始化时定义输出词典大小、embedding维度和隐状态大小
- embedding层对目标语言词编码成向量
- lstm层基于上一时刻输出和编码器隐状态进行解码
- forward函数接收输入的上一时刻目标语言词和编码器隐状态
- 通过embedding和lstm得到当前时刻的解码输出和隐状态3. Seq2Seq模型- 初始化时构建Encoder和Decoder
- forward函数接收输入序列及目标序列
- 通过Encoder对输入序列编码得到上下文向量
- 通过Decoder基于上下文向量和目标序列解码
- 计算交叉熵损失函数并返回
class Encoder(nn.Module):
def __init__(self,encoder_embedding_num,encoder_hidden_num,en_corpus_len):
super().__init__()
self.embedding = nn.Embedding(en_corpus_len,encoder_embedding_num)
self.lstm = nn.LSTM(encoder_embedding_num,encoder_hidden_num,batch_first=True)
def forward(self,en_index):
en_embedding = self.embedding(en_index)
_,encoder_hidden =self.lstm(en_embedding)
return encoder_hidden
class Decoder(nn.Module):
def __init__(self,decoder_embedding_num,decoder_hidden_num,ch_corpus_len):
super().__init__()
self.embedding = nn.Embedding(ch_corpus_len,decoder_embedding_num)
self.lstm = nn.LSTM(decoder_embedding_num,decoder_hidden_num,batch_first=True)
def forward(self,decoder_input,hidden):
embedding = self.embedding(decoder_input)
decoder_output,decoder_hidden = self.lstm(embedding,hidden)
return decoder_output,decoder_hidden
class Seq2Seq(nn.Module):
def __init__(self,encoder_embedding_num,encoder_hidden_num,en_corpus_len,decoder_embedding_num,decoder_hidden_num,ch_corpus_len):
super().__init__()
self.encoder = Encoder(encoder_embedding_num,encoder_hidden_num,en_corpus_len)
self.decoder = Decoder(decoder_embedding_num,decoder_hidden_num,ch_corpus_len)
self.classifier = nn.Linear(decoder_hidden_num,ch_corpus_len)
self.cross_loss = nn.CrossEntropyLoss()
def forward(self,en_index,ch_index):
decoder_input = ch_index[:,:-1]
label = ch_index[:,1:]
encoder_hidden = self.encoder(en_index)
decoder_output,_ = self.decoder(decoder_input,encoder_hidden)
pre = self.classifier(decoder_output)
loss = self.cross_loss(pre.reshape(-1,pre.shape[-1]),label.reshape(-1))
return loss
4、训练
- 构建数据集DataLoader进行批量化
- 定义优化器,循环训练并计算损失
- 保存模型
构建Dataset和DataLoader- 创建MyDataset对象,传入英文和中文句子
- 构建DataLoader,设置batch_size等参数
- DataLoader将自动进行batching和shuffle2. 定义优化器和损失函数- 采用Adam优化器
- 损失函数为交叉熵损失函数3. 训练循环- 对每个batch,模型进行前向计算,得到预测和损失
- 清零梯度,进行反向传播计算gradients
- 优化器根据gradients更新模型参数
- 重复训练多个epochs4. 保存模型- 通过torch.save保存整个模型
- 可以在测试时重新加载使用5. 训练过程可视化- 绘制losses列表来观察loss曲线
- 可以看到loss逐步下降,模型逐步优化
def translate(sentence):
global en_word_2_index,model,device,ch_word_2_index,ch_index_2_word
en_index = torch.tensor([[en_word_2_index[i] for i in sentence]],device=device)
result = []
encoder_hidden = model.encoder(en_index)
decoder_input = torch.tensor([[ch_word_2_index["<BOS>"]]],device=device)
decoder_hidden = encoder_hidden
while True:
decoder_output,decoder_hidden = model.decoder(decoder_input,decoder_hidden)
pre = model.classifier(decoder_output)
w_index = int(torch.argmax(pre,dim=-1))
word = ch_index_2_word[w_index]
if word == "<EOS>" or len(result) > 50:
break
result.append(word)
decoder_input = torch.tensor([[w_index]],device=device)
print("译文: ","".join(result))
5、预测
接收输入英文句子- translate函数接收待翻译的英文句子words2. 英文句子转id- 调用en2id函数,将英文单词转化为词表中的id
- 得到输入的英文词id序列en_ids3. 模型编码- 调用模型的encoder对en_ids进行encode
- 得到输入句子的编码状态enc_output4. 初始化解码状态- 初始解码输入为BOS标记,表示句子开始
- 初始隐状态初始化为编码器输出5. 循环解码- 在最大解码长度内循环
- 将当前解码输入通过模型decoder解码一步
- 得到当前时刻的解码输出和隐状态
- 如果输出为EOS表示结束,则中断循环
- 否则将输出作为下一时刻输入继续解码6. id转中文- 将输出的id序列,通过id2cn函数转为中文词
- 拼接得到完整的中文翻译句子
if __name__ == "__main__":
device = "cuda:0" if torch.cuda.is_available() else "cpu"
with open("E:\\Yanjiushengstudy\\project\\ch.vec","rb") as f1:
_, ch_word_2_index,ch_index_2_word = pickle.load(f1)
with open("E:\\Yanjiushengstudy\\project\\en.vec","rb") as f2:
_, en_word_2_index, en_index_2_word = pickle.load(f2)
ch_corpus_len = len(ch_word_2_index)
en_corpus_len = len(en_word_2_index)
ch_word_2_index.update({"<PAD>":ch_corpus_len,"<BOS>":ch_corpus_len + 1 , "<EOS>":ch_corpus_len+2})
en_word_2_index.update({"<PAD>":en_corpus_len})
ch_index_2_word += ["<PAD>","<BOS>","<EOS>"]
en_index_2_word += ["<PAD>"]
ch_corpus_len += 3
en_corpus_len = len(en_word_2_index)
en_datas,ch_datas = get_datas(nums=200)
encoder_embedding_num = 50
encoder_hidden_num = 100
decoder_embedding_num = 107
decoder_hidden_num = 100
batch_size = 2
epoch = 40
lr = 0.001
dataset = MyDataset(en_datas,ch_datas,en_word_2_index,ch_word_2_index)
dataloader = DataLoader(dataset,batch_size,shuffle=False,collate_fn = dataset.batch_data_process)
model = Seq2Seq(encoder_embedding_num,encoder_hidden_num,en_corpus_len,decoder_embedding_num,decoder_hidden_num,ch_corpus_len)
model = model.to(device)
opt = torch.optim.Adam(model.parameters(),lr = lr)
for e in range(epoch):
for en_index,ch_index in dataloader:
loss = model(en_index,ch_index)
loss.backward()
opt.step()
opt.zero_grad()
print(f"loss:{loss:.3f}")
while True:
s = input("请输入英文: ")
translate(s)
6、结果展示
总结
下周,我将继续学习seq2seq,并将其与attention结合起来学习。