文章目录

  • 0. 前言
    • 语言模型
    • next-word prediction
  • 1. 数据集
  • 2. 生成单词表
  • 3. 实例化 Dataset 和 DataLoader
  • 4. 模型
    • 4.1 NNLM
    • 4.2 RNNLM
    • 4.3 其他模型(见github)
  • 5. 损失函数、优化器、调度器
  • 6. 训练
  • 7. 验证
  • 8. 完整训练+验证过程
  • 9. 测试
  • 10. 其他说明

0. 前言

完整项目的代码我已上传到 github:https://github.com/friedrichor/Language-Model-Next-Word-Prediction

本文主要讲解实现代码(可以搭配着 github 中的完整项目一起看),如何使用多种语言模型来实现 下一单词预测(next-word prediction)。

语言模型

  语言模型主要就是将 单词→\to数字。语言模型可以分为离散型表示 和 分布式表示。

  1. 离散表示有 one-hot 编码、BOW(Bag of Words,词袋模型)、N-gram;
  2. 分布式表示就是将单词转换为向量的形式,这样每个单词间就会有一定的关联,比如说 沈阳盛京,这两个词都表示的是同一地点,如果用离散型表示会将它们两个词判定为两个完全不同的词,而转换为向量就会将他们判定为意思相近的词,在向量空间中他们也会距离的比较近。主要有 神经网络语言模型(如NNLM、RNNLM)、Word2Vec、GloVe、Elmo、BERT等等。

next-word prediction

  最好的例子就是 智能手机键盘的下一个单词预测功能,这是一个数十亿人每天使用数百次的功能。下一个单词的预测是一项可以通过语言模型来完成的任务。语言模型可以获取一个单词列表(假设是两个单词),并尝试预测紧随其后的单词。比如说我输入了 I like,那么手机键盘就会有提示 cat, dog 等等来预测我接下将要打的单词。

1. 数据集

本文使用的是 PTB 数据集,PTB数据集是一个英语语料库,并被许多研究者用于语言建模实验。

  github 中提供了两种数据集,下图左边的是完整的 PTB 数据集(penn文件夹),右边的是小型的 PTB 数据集(data文件夹)。训练完整的数据集需要足够大的GPU显存,自己电脑GPU显存充足或者服务器GPU够大就能跑;为了防止显存不足,这里也提供了一个小型的数据集,这个一般自己电脑就能够跑了,不过这个小型的数据集由于数据量较小,比较容易过拟合。

penn/train.txt 部分数据如下所示:

2. 生成单词表

  由于数据集为文本,我们知道只有数字的形式才能使用深度学习来进行训练模型,所以首先就要把文本转化为数字。

  根据训练集来生成单词表,即 单词→\to索引索引→\to单词 的字典。

  除了单词外,NLP中还有四种常用的标识符,即 <PAD><UNK><SOS><EOS>

  • <PAD>: 补全字符
  • <UNK>: 低频词或未在词表中的词
  • <SOS>: 句子起始标识符
  • <EOS>: 句子结束标识符
# 在 utils.py
def generate_vocab(data_path):word_list = []f = open(data_path, 'r')lines = f.readlines()for sentence in lines:word_list += sentence.split()word_list = list(set(word_list))# 生成字典word2index_dict = {w: i + 2 for i, w in enumerate(word_list)}word2index_dict['<PAD>'] = 0word2index_dict['<UNK>'] = 1word2index_dict = dict(sorted(word2index_dict.items(), key=lambda x: x[1]))  # 排序index2word_dict = {index: word for word, index in word2index_dict.items()}# 将单词表写入jsonjson_str = json.dumps(word2index_dict, indent=4)with open(vocab_path, 'w') as json_file:json_file.write(json_str)return word2index_dict, index2word_dict

说明:

  • 这里并没有加标识符<SOS><EOS>,需要的话自己加上就可,同时 word2index_dict = {w: i + 2 for i, w in enumerate(word_list)}要改成 word2index_dict = {w: i + 4 for i, w in enumerate(word_list)}
  • 两个返回值 word2index_dict 用于编码(单词→\to索引),index2word_dict 用于解码(索引→\to单词)
  • 这里将单词表写入 json 文件,保证每次训练和测试时的单词表都是相同的,保证每个单词对应的索引要一致(如果更换数据集记得把先前生成的 vocab.json 删了,单词表和数据集要对应)。

train.py 中如下调用:

   # 生成单词表if not os.path.exists(vocab_path):word2index_dict, index2word_dict = generate_vocab(train_path)else:with open(vocab_path, "r") as f:word2index_dict = json.load(f)index2word_dict = {index: word for word, index in word2index_dict.items()}

vocab.json:

3. 实例化 Dataset 和 DataLoader

定义Dataset类:

# 在 my_dataset.py
from torch.utils.data import Dataset
class MyDataSet(Dataset):def __init__(self, inputs, targets):self.inputs = inputsself.targets = targetsdef __getitem__(self, item):input = self.inputs[item]target = self.targets[item]return input, targetdef __len__(self):return len(self.inputs)

  遍历数据集,首先 padding(如果句子长度不够的话),然后将句子中的每个单词进行编码(单词→\to索引),根据滑动窗口构造input、target列表,然后传入 MyDataset 类中实例化就完成了对数据集的实例化。

  对于滑动窗口构造input、target列表,可以通过以下例子理解:


  关于标识符<PAD><UNK><SOS><EOS>,举个例子,有句子 I like cats,我想要根据 I like 来预测下个单词可能是什么,假如我训练的语言模型是需要 根据前四个词才能预测下一个词单词like未在训练集中出现或出现频率低(即不在单词表中),那么这个句子已知的单词明显不足,首先需要补全字符,即 <PAD> <PAD> I <UNK>,然后把这个句子放入模型进行预测即可。(这里代码中并没加<SOS><EOS>,这个可以自行加上,在句首和句尾补充标识符即可,即 <SOS> I like cats <EOS>

# 在 utils.py
def generate_dataset(data_path, word2index_dict, n_step=5):""":param data_path: 数据集路径:param word2index_dict: word2index字典:param n_step: 窗口大小:return: 实例化后的数据集"""def word2index(word):try:return word2index_dict[word]except:return 1  # <UNK>input_list = []target_list = []f = open(data_path, 'r')lines = f.readlines()for sentence in lines:word_list = sentence.split()if len(word_list) < n_step + 1:  # 句子中单词不足,paddingword_list = ['<PAD>'] * (n_step + 1 - len(word_list)) + word_listindex_list = [word2index(word) for word in word_list]for i in range(len(word_list) - n_step):input = index_list[i: i + n_step]target = index_list[i + n_step]input_list.append(torch.tensor(input))target_list.append(torch.tensor(target))# 实例化数据集dataset = MyDataSet(input_list, target_list)return dataset

train.py 中如下调用:

   # 实例化数据集train_dataset = generate_dataset(train_path, word2index_dict, n_step)vaild_dataset = generate_dataset(valid_path, word2index_dict, n_step)

实例化 DataLoader:

    train_loader = DataLoader(train_dataset,batch_size=batch_size,shuffle=True,pin_memory=True,num_workers=nw)valid_loader = DataLoader(vaild_dataset,batch_size=batch_size,shuffle=False,pin_memory=True,num_workers=nw)

其中 nw 为线程数,我在 params.py 中有定义:

# 在 params.py
nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])

4. 模型

model.py 中提供了 NNLM、RNNLM 及 引入注意力机制的RNNLM

4.1 NNLM

论文:A Neural Probabilistic Language Model

class NNLM(nn.Module):def __init__(self, n_class):super(NNLM, self).__init__()self.n_step = n_stepself.emb_size = emb_sizeself.C = nn.Embedding(n_class, emb_size)self.w1 = nn.Linear(n_step * emb_size, n_hidden, bias=False)self.b1 = nn.Parameter(torch.ones(n_hidden))self.w2 = nn.Linear(n_hidden, n_class, bias=False)self.w3 = nn.Linear(n_step * emb_size, n_class, bias=False)def forward(self, X):X = self.C(X)X = X.view(-1, self.n_step * self.emb_size)Y1 = torch.tanh(self.b1 + self.w1(X))b2 = self.w3(X)Y2 = b2 + self.w2(Y1)  # 为什么不用加softmax?因为pytorch实现的交叉熵里面用了softmaxreturn Y2

4.2 RNNLM

论文:Recurrent neural network based language model

class TextRNN(nn.Module):def __init__(self, n_class):super(TextRNN, self).__init__()self.C = nn.Embedding(n_class, embedding_dim=emb_size)self.rnn = nn.RNN(input_size=emb_size, hidden_size=n_hidden)self.W = nn.Linear(n_hidden, n_class, bias=False)self.b = nn.Parameter(torch.ones([n_class]))def forward(self, X):X = self.C(X)X = X.transpose(0, 1) # X : [n_step, batch_size, embeding size]outputs, hidden = self.rnn(X)# outputs : [n_step, batch_size, num_directions(=1) * n_hidden]# hidden : [num_layers(=1) * num_directions(=1), batch_size, n_hidden]outputs = outputs[-1] # [batch_size, num_directions(=1) * n_hidden]model = self.W(outputs) + self.b # model : [batch_size, n_class]return model

引入注意力机制(仅供参考)(这部分与github有所差别,在于返回值,可以直接将下面这部分代码替换github中的代码,然后utils.py中的训练和验证代码均不需要动,注释可以删了,那部分注释是之前为了打印Attention矩阵才用的):

class TextRNN_attention(nn.Module):def __init__(self, n_class):super(TextRNN_attention, self).__init__()self.C = nn.Embedding(n_class, embedding_dim=emb_size)self.rnn = nn.RNN(input_size=emb_size, hidden_size=n_hidden)self.W = nn.Linear(2 * n_hidden, n_class, bias=False)self.b = nn.Parameter(torch.ones([n_class]))def forward(self, X):X = self.C(X)X = X.transpose(0, 1)  # X : [n_step, batch_size, embeding size]outputs, hidden = self.rnn(X)# outputs : [n_step, batch_size, num_directions(=1) * n_hidden]# hidden : [num_layers(=1) * num_directions(=1), batch_size, n_hidden]output = outputs[-1]attention = []for it in outputs[:-1]:attention.append(torch.mul(it, output).sum(dim=1).tolist())attention = torch.tensor(attention)attention = attention.transpose(0, 1)attention = nn.functional.softmax(attention, dim=1).transpose(0, 1)# get soft attentionattention_output = torch.zeros(outputs.size()[1], n_hidden)for i in range(outputs.size()[0] - 1):attention_output += torch.mul(attention[i], outputs[i].transpose(0, 1)).transpose(0, 1)output = torch.cat((attention_output, output), 1)# joint ouput output:[batch_size, 2*n_hidden]model = torch.mm(output, self.W.weight) + self.b  # model : [batch_size, n_class]return model

4.3 其他模型(见github)

可以参考 https://github.com/graykode/nlp-tutorial,里面包含多个语言模型。

直接在model.py 中添加即可,然后在 train.py 中更改 model = TextRNN(n_class).to(device)为自己的模型即可。

5. 损失函数、优化器、调度器

这部分可以自行更改,以下只是给了几个例子。

loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=lr)
# lr_scheduler = create_lr_scheduler(optimizer, len(train_loader), epochs,
#                                    warmup=True, warmup_epochs=5)
lr_scheduler = None
# lr_scheduler = optim.lr_scheduler.StepLR(optimizer, 50, gamma=0.1, last_epoch=-1)
# lr_scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)

说明:

  • 这里我定义 lr_scheduler = None 为不使用调度器(因为训练函数中需要传参进去,这里就需要实例化),即学习率一直不变
  • create_lr_scheduler 函数为Poly学习率调整策略,具体原理可以参考:网络训练时使用不同学习率策略(Poly)以及学习率是如何计算,实现如下:
# 在 utils.py
def create_lr_scheduler(optimizer,num_step: int,epochs: int,warmup=True,warmup_epochs=1,warmup_factor=1e-3,end_factor=1e-6):assert num_step > 0 and epochs > 0if warmup is False:warmup_epochs = 0def f(x):"""根据step数返回一个学习率倍率因子,注意在训练开始之前,pytorch会提前调用一次lr_scheduler.step()方法"""if warmup is True and x <= (warmup_epochs * num_step):alpha = float(x) / (warmup_epochs * num_step)# warmup过程中lr倍率因子从warmup_factor -> 1return warmup_factor * (1 - alpha) + alphaelse:current_step = (x - warmup_epochs * num_step)cosine_steps = (epochs - warmup_epochs) * num_step# warmup后lr倍率因子从1 -> end_factorreturn ((1 + math.cos(current_step * math.pi / cosine_steps)) / 2) * (1 - end_factor) + end_factorreturn torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=f)

按我代码中定义,得到的学习率变化图如下(训练 200 epoch,预热 5 epoch,优化器中定义的初始学习率为 1e-3):

6. 训练

以下为训练一轮的代码:

# 在 utils.py
def train_one_epoch(model, loss_function, optimizer, data_loader, device, epoch, lr_scheduler):model.train()accu_loss = torch.zeros(1).to(device)  # 累计损失optimizer.zero_grad()data_loader = tqdm(data_loader, file=sys.stdout)for step, data in enumerate(data_loader):input, target = datapred = model(input.to(device))loss = loss_function(pred, target.to(device))loss.backward()accu_loss += loss.detach()data_loader.desc = "[train epoch {}] loss: {:.3f}, ppl: {:.3f}, lr: {:.5f}".format(epoch,accu_loss.item() / (step + 1),math.exp(accu_loss.item() / (step + 1)),optimizer.param_groups[0]["lr"])if not torch.isfinite(loss):print('WARNING: non-finite loss, ending training ', loss)sys.exit(1)# gradient clip# clip_grad_norm_(parameters=model.parameters(), max_norm=0.1, norm_type=2)optimizer.step()optimizer.zero_grad()# update lrif lr_scheduler != None:lr_scheduler.step()return accu_loss.item() / (step + 1), math.exp(accu_loss.item() / (step + 1))

说明:

  • 参数:
    model:模型;
    loss_function:损失函数;
    optimizer:优化器;
    data_loader:上一步实例化后的 DataLoader;
    device:训练使用的设备,gpu / cpu;
    epoch:当前训练是第几轮,这个主要是用来实时显示当前训练到第几轮的;
    lr_scheduler:学习率调度器;
  • data_loader.desc 用于实时打印训练过程,其中 ppl 直接用 eCrossEntropyLosse^{CrossEntropyLoss}eCrossEntropyLoss 表示了,计算方法可能有所差别。
  • clip_grad_norm_(parameters=model.parameters(), max_norm=0.1, norm_type=2) 被注释掉了,这里表示是否使用梯度裁剪,如果需要的话可以自行启用,不过这里的参数我并没有进行调参,用或不用差别不大,可以自行探索。
  • 如果传入的参数 lr_scheduler 为None,那么就不使用调度器,即学习率一直保持不变。
  • 这里调度器是一个 step 更新一次学习率,用的是 Poly策略,如果改成StepLR、CosineAnnealingLR(余弦退火)等等的话一般是一个epoch更新一次学习率,这里就需要注意一下更改lr_scheduler.step()的位置。
  • 在 github 中的代码这部分还有更多的注释,那些注释其实是在 使用注意力机制的RNNLM 中我为了打印其中的 注意力Attention矩阵 才用的,正常使用NNLM、RNNLM等模型其实上面这部分代码就够了。

7. 验证

上面是对训练集进行模型训练的,这部分是验证当前模型效果的,大部分与训练时代码相似,这里不做过多说明。

def evaluate(model, loss_function, data_loader, device, epoch):model.eval()accu_loss = torch.zeros(1).to(device)  # 累计损失data_loader = tqdm(data_loader, file=sys.stdout)for step, data in enumerate(data_loader):input, target = datapred = model(input.to(device))loss = loss_function(pred, target.to(device))accu_loss += lossdata_loader.desc = "[valid epoch {}] loss: {:.3f}, ppl: {:.3f}".format(epoch,accu_loss.item() / (step + 1),math.exp(accu_loss.item() / (step + 1)),)return accu_loss.item() / (step + 1), math.exp(accu_loss.item() / (step + 1))

8. 完整训练+验证过程

   tb_writer = SummaryWriter()for epoch in range(epochs):# traintrain_loss, train_ppl = train_one_epoch(model=model,loss_function=loss_function,optimizer=optimizer,data_loader=train_loader,device=device,epoch=epoch,lr_scheduler=lr_scheduler)# validatevalid_loss, valid_ppl = evaluate(model=model,loss_function=loss_function,data_loader=valid_loader,device=device,epoch=epoch)tags = ["train_loss", "train_ppl", "valid_loss", "valid_ppl", "learning_rate"]tb_writer.add_scalar(tags[0], train_loss, epoch)tb_writer.add_scalar(tags[1], train_ppl, epoch)tb_writer.add_scalar(tags[2], valid_loss, epoch)tb_writer.add_scalar(tags[3], valid_ppl, epoch)tb_writer.add_scalar(tags[4], optimizer.param_groups[0]["lr"], epoch)if (epoch + 1) % save_epoch == 0:torch.save(model, os.path.join(models_path, f'weights-{epoch + 1}.ckpt'))

说明:

  • 这里使用了 tensorboard 中的 SummaryWriter 记录训练日志,存储每一轮训练时训练集、验证集的损失、ppl及学习率, 如下图就是我做对比实验时使用SummaryWriter保存的日志生成的图像,还是很好用的,不过不需要的话可以忽略。
  • 代码最后一部分设定了每训练多少轮就保存一次模型,这一部分也可以自行更改。

9. 测试

test.py,整个过程与验证相似,代码中仅仅输出了 loss。这部分可以根据 index2word_dict 解码回单词,实现真正的 next-word prediction 的功能。

10. 其他说明

项目中一些通用的全局参数在 params.py,包括 模型相关参数 n_step(滑动窗口大小)、n_hiddenemb_size,训练相关参数 epochsbatch_sizelrdevicenw,以及 训练集、验证集、测试集路径 等等。

如果想要进一步了解 Word2vec ,也可以查看我的一篇博文:一文带你通俗易懂地了解word2vec原理,是根据The Illustrated Word2vec翻译的(看原文更好一些,当时写的理解并不是很好),里面的讲解通俗易懂,整篇文章看完基本就明白了,还是很有帮助的。

语言模型 实现 下一单词预测(next-word prediction)相关推荐

  1. lstm预测单词_下一个单词预测完整指南

    lstm预测单词 As part of my summer internship with Linagora's R&D team, I was tasked with developing ...

  2. NLP之Bi-LSTM(在长句中预测下一个单词)

    Bi-LSTM 文章目录 Bi-LSTM 1.理论 1.1 基本模型 1.2 Bi-LSTM的特点 2.实验 2.1 实验步骤 2.2 实验模型 1.理论 1.1 基本模型 Bi-LSTM模型分为2个 ...

  3. Pytorch LSTM实现中文单词预测(附完整训练代码)

    Pytorch LSTM实现中文单词预测(附完整训练代码) 目录 Pytorch LSTM实现中文单词预测(词语预测 附完整训练代码) 1.项目介绍 2.中文单词预测方法(N-Gram 模型) 3.训 ...

  4. Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(完)...

    原文 http://www.cnblogs.com/mayswind/archive/2013/04/01/2991271.html [题外话] 这是这个系列的最后一篇文章了,为了不让自己觉得少点什么 ...

  5. Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(一)...

    [题外话] 这是2010年参加比赛时候做的研究,当时为了实现对Word.Excel.PowerPoint文件文字内容的抽取研究了很久,由于Java有POI库,可以轻松的抽取各种Office文档,而.N ...

  6. 海洋大数据关键技术及在灾害天气下船舶行为预测上的应用

    海洋大数据关键技术及在灾害天气下船舶行为预测上的应用 王冬海,卢峰,方晓蓉,郭刚 中电科海洋信息技术研究院有限公司,北京 100041 摘要:随着海洋数据量的爆炸式增长,海洋大数据受到越来越多的关注. ...

  7. 在linux环境下com.aspose.words将word文件转为pdf后乱码,window环境下不会

    在linux环境下com.aspose.words将word文件转为pdf后乱码,window环境下不会 乱码原因是因为在linux系统下没有中文字体,所以转换的时候乱码,需要我们手动把window系 ...

  8. python中排序英文单词怎么写_Python 排序最长英文单词链(列表中前一个单词末字母是下一个单词的首字母)...

    本文主要介绍排序最长的单词链的方法,列表中每个元素相当于一个单词,要实现列表中前一个单词末字母是下一个单词的首字母,并且这个链是最长的. 使用递归实现 words = ['giraffe', 'ele ...

  9. Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析

    转载http://www.cnblogs.com/mayswind/archive/2013/03/17/2962205.html [题外话] 这是2010年参加比赛时候做的研究,当时为了实现对Wor ...

最新文章

  1. Centos7.4 版本环境下安装Mysql5.7操作记录
  2. linux输入文件后怎么保存不了怎么办,关于linux:输入数据后为什么不能保存VI文件?...
  3. python文件处理,python文件处理
  4. 配置静态路由进阶实验
  5. c++ 1:非MFC工程使用MFC库时的问题及解决办法(如果要用CString或者提示windows头文件重复包含)...
  6. oracle数据库top用法,Oracle TOP SQLHIT
  7. 使用距离变换的分水岭分割
  8. Hibernate 双向一对多映射
  9. 如何用计算机寒假计划表,如何制定寒假学习计划表
  10. 【Gym - 101848B】Almost AP【等差数列改三个数】
  11. ci框架基础详解(入门学习)
  12. MT6573驱动开发日志之touchpanel
  13. c语言:鸡兔同笼问题
  14. python实战项目分析2—物流
  15. 云计算的特征:基本功能
  16. drupal mysql hash密码_Drupal7管理员密码重置
  17. Gradient Descent
  18. java 视频断点播放,实现无卡顿
  19. xcode 真机调试无法选择对应设备 “ineligible devices“
  20. Rstudio 更改工作路径和安装包的路径

热门文章

  1. 系统试运行报告是谁写的_深圳个人信用报告查询系统试运行,手机就能查!
  2. 分享一个自定义的popuwindow效果,高度适配
  3. Hadoop——使用secondary namenode数据恢复namenode
  4. 摄像头未能启动,不能创建预览
  5. Windows10 下安装 Nexus OSS 3.xx
  6. 声卡的故障分析与排除
  7. 第三方服务 “TOP10”Java 后端开发常用的
  8. 【报告分享】年轻代厨房小家电洞察报告-CBNDATA(附下载)
  9. Labelme左边的工具栏没有了?
  10. 【EDA】实验2:利用74161计数器芯片设计M=12的计数器