ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

使用LSTM训练语言模型(以《魔道祖师》为corpus)

2021-06-01 21:58:42  阅读:214  来源: 互联网

标签:vocab loss 祖师 corpus batch model LSTM hidden size


文章目录

import torchtext
from torchtext.vocab import Vectors
import torch 
from torch import nn
import numpy as np
import random
import jieba
random.seed(53113)
np.random.seed(53113)
torch.manual_seed(53113)
use_cuda = torch.cuda.is_available()
if use_cuda:
    torch.cuda.manual_seed(53113)
    device = torch.device('cuda')
else:
    device = torch.device('cpu')

1.读入原始文档和停用词txt文件

原始文档和停用词文档
在这里插入图片描述
在这里插入图片描述

with open('./mdzs.txt') as f:
    text = f.readlines()
f.close()
text = [i.strip() for i in text]

with open('./stop_words.txt',encoding='utf-8') as f:
    stop_words = f.readlines()
f.close()
stop_words = [i.strip() for i in stop_words]
stop_word = [' ','PS','1V1','HE','┃','O','∩','☆'] 
for word in stop_word:
    stop_words.append(word)
text[:10]
['',
 '《魔道祖师[重生]》作者:墨香铜臭',
 '',
 '文案:',
 '前世的魏无羡万人唾骂,声名狼藉。',
 '被护持一生的师弟带人端了老巢,',
 '纵横一世,死无全尸。',
 '',
 '曾掀起腥风血雨的一代魔道祖师,重生成了一个……',
 '脑残。']
stop_words[:10]
['', '为止', '纵然', 'all', '例如', '[④e]', 'when', '亦', '来讲', '谁料']

2.分词处理

text_token = []
for sentence in text:
    token = jieba.lcut(sentence)
    for word in token:
        if word not in stop_words:
            text_token.append(word)
a = ' '.join(i for i in text_token)
with open('cql.txt','w',encoding='utf-8') as f:
    f.write(a)
f.close()

3.建立字典和迭代器

field = torchtext.data.Field()
train = torchtext.datasets.LanguageModelingDataset.splits(path='./',train="cql.txt",text_field=field)[0]
field.build_vocab(train, max_size=20000)

train_iter,val_iter = torchtext.data.BPTTIterator.splits(
                                (train,train),batch_size=4,device=device,
                                 bptt_len=10,repeat=False,shuffle=True
                                )
print(field.vocab.itos[:10])
print(field.vocab.stoi['魏无羡'])
# itos => idx to string 是一个list,包含了20002个单词,其实就是我们上一讲中自己建的idx_to_word
# stoi => string to idx  是一个dictionary,keys是50002个单词,values是以上20002个单词在list中的索引
# 其实就是就是我们上一讲中自己建的word_to_idx
# PS:如果你输入的单词不在那20000个单词内,则idx就是0 即用<unk>来代表这个单词
['<unk>', '<pad>', '道', '魏无羡', '蓝忘机', '说', '魏无羡道', '金光', '江澄', '走']
3
batch = next(iter(train_iter))
print(batch.text)
print(batch.target)
# batch.text/target的size是bptt_len*batch_size 
#可以看成一个句子有10个单词(所以cell就是10个),一个batch里面有4个句子
# 里面的数字是该单词的idx
# target是text里面单词的后一个单词的idx
tensor([[  591,    15,     3,   500],
        [    2,  8269,  1305,   354],
        [ 6203,  1391,     2,  4898],
        [ 1739,   536, 12410,     0],
        [ 2102,  4534,  7433,    66],
        [16726,     0,  4723,   791],
        [    0,  3811, 14404,     0],
        [19624,   460, 13010, 11988],
        [ 1940,   380,  1276,    44],
        [    3,  8629,    78,  2251]], device='cuda:0')
tensor([[    2,  8269,  1305,   354],
        [ 6203,  1391,     2,  4898],
        [ 1739,   536, 12410,     0],
        [ 2102,  4534,  7433,    66],
        [16726,     0,  4723,   791],
        [    0,  3811, 14404,     0],
        [19624,   460, 13010, 11988],
        [ 1940,   380,  1276,    44],
        [    3,  8629,    78,  2251],
        [ 6624,     6,   362,   350]], device='cuda:0')
it = iter(train_iter)
for i in range(3):
    batch = next(it)
    print('*'*50)
    print(' '.join(field.vocab.itos[i] for i in batch.text[:,1].data.cpu()))
    print('  ')
    print(' '.join(field.vocab.itos[i] for i in batch.target[:,1].data.cpu()))

# 根据这个打印出来的结果可以看出,batch之间是连续的。什么意思呢?比如第一个batch的第一个句子的end-word是‘differing’
# 那么第二个batch的第一句话就是从‘differing’后一个单词开始(interpretations)
# 再比如:第3个batch的第二句话以‘reason’结尾,那么第4个batch的第二句话就是从‘reason’后面一个单词开始
# 因此,我们可以推测这个BPTTIterator是怎么工作的呢?以下面这个简单的例子来看看:
# 设batch_size是3,bptt_len(也就是一个句子里包含多少个单词)为2
# 整个text为  我心中有一簇向阳而生的花
# 一共有12个字,batch_size为3的话,那么一个iteration里面会有2个batch
#    batch_1  batch_2      
#       我心     中有      
#       向阳     而生
#       一簇     向阳
**************************************************
中 肆虐 贸然 靠近 吸入 <unk> 可比 进 嘴 难办
  
肆虐 贸然 靠近 吸入 <unk> 可比 进 嘴 难办 魏无羡道
**************************************************
魏无羡道 那片 地方 站 远点 蓝景 仪道 看不见 伸手不见五指 举步
  
那片 地方 站 远点 蓝景 仪道 看不见 伸手不见五指 举步 难
**************************************************
难 行 魏无羡 想起 避尘 每次 出鞘 剑光 穿透 白雾
  
行 魏无羡 想起 避尘 每次 出鞘 剑光 穿透 白雾 转头

4.定义模型及评估函数

class rnnmodel(nn.Module):
    def __init__(self,vocab_size,embed_size,hidden_size,num_layers):
        super(rnnmodel,self).__init__()
        self.embed = nn.Embedding(vocab_size,embed_size)
        self.lstm = nn.LSTM(input_size=embed_size,hidden_size=hidden_size,num_layers=num_layers)
        self.decoder = nn.Linear(hidden_size,vocab_size)
        self.hidden_size = hidden_size
        
        
        
    def forward(self,text,hidden) :
        embed = self.embed(text) #seq_length*batch_size*embed_size
        out,hidden = self.lstm(embed) 
        # out的size是seq_length*batch_size*hidden_size
        # h和c 的size都是num_layers*batch_size*hidden_size
        # view之后 out的size是(seq_length*batch_size)*hidden_size
        decoded = self.decoder(out.view(-1,out.shape[2]))
        # decoded的size是 (seq_length*batch_size)*vocab_size
        out_vocab = decoded.view(out.shape[0],out.shape[1],decoded.shape[-1])
        # out_vocab的size为seq_length*batch_size*vocab_size
        return out_vocab,hidden
    
    def init_hidden(self,num_layers,batch_size,requires_grad=True):
        weight = next(self.parameters())
#         print(requires_grad)
        h0 = weight.new_zeros(num_layers,batch_size,self.hidden_size,requires_grad=requires_grad)
        c0 = weight.new_zeros(num_layers,batch_size,self.hidden_size,requires_grad=requires_grad)

        return (h0,c0)
        
embed_size = 100
hidden_size = 100
num_layers = 1
model = rnnmodel(vocab_size=len(field.vocab),
                embed_size=embed_size,
                hidden_size=hidden_size,
                num_layers=num_layers)
if use_cuda:
    model = model.to(device)
# 根据我们前面的观察,batch之间是连续的。
# 所以前一个batch训练完之后的隐层(包含h和c两个部分)是可以拿来当做下一个batch的h0和c0的
# 但是呢,前一个batch最后输出的h和c是含有grad等很多之前的信息的,而之后拿来做h0和c0的时候,
# 其实我们只需要它们的数值就可以,所以把它们detach一下
def repackage_hidden(h):
    if isinstance(h,torch.Tensor):
        return h.detach()
    else:
        return tuple(repackage_hidden(v) for v in h)   
def evaluate(model,dataset):
    model.eval()
    total_loss = 0.
    total_count = 0.
    it = iter(dataset)
    with torch.no_grad():
        hidden = model.init_hidden(num_layers,batch_size,requires_grad=False)
        for i,batch in enumerate(it):
            data,target = batch.text,batch.target
            hidden = repackage_hidden(hidden)
            out,hidden = model(data,hidden)        
            loss = loss_fn(out.view(-1,vocab_size),target.view(-1))
            total_loss += loss.item()*np.multiply(*data.size())
            # total_loss增加的部分是该batch上的loss
            total_count += np.multiply(*data.size())
            # total_count增加的部分是该batch上的所有单词的数量
    valset_loss = total_loss/total_count
    model.train()
    return valset_loss

5.开始训练

epochs = 100
vocab_size = len(field.vocab)
bptt_len = 5
batch_size = 16

train_iter,val_iter = torchtext.data.BPTTIterator.splits(
                                (train,train),batch_size=batch_size,device=device,
                                 bptt_len=bptt_len,repeat=False,shuffle=True
                                )

optimizer = torch.optim.Adam(model.parameters(),lr=0.02)
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, 0.5)
loss_fn = nn.CrossEntropyLoss()
grad_clip = 5.0
min_valset_loss = 99999999999999
for epoch in range(epochs):
    model.train()
    it = iter(train_iter)
    hidden = model.init_hidden(num_layers,batch_size,True) 
    # 这里的hidden其实包含了h0和c0,在每个epoch开始时,将h0和c0全部初始化为0
    for batch_idx,batch in enumerate(it):
        data,target = batch.text,batch.target
        hidden = repackage_hidden(hidden)
        out,hidden = model(data,hidden)
        
        loss = loss_fn(out.view(-1,vocab_size),target.view(-1))
        # out view之后的size为 (seq_length*batch_size)*vocab_size,seq_length*batch_size是一个batch里面所有单词的总数
        # 因此out view过后就可以看成这样一个矩阵:每一行代表这个batch里面的一个单词,一共有vocab_size列,那么就代表了该单词是vocab中对应单词的概率
        # 而target.view(-1) 后的size为(seq_length*batch_size),变成了一个一维tensor,其中的每一个数是正确单词的索引,设为idx
        # 由crossentropy的步骤可以知道,会根据idx从out(经过view之后的out)对应行中取出idx对应的列,
        # 如果模型越好,取出来的数(我们之前说过,可以看成概率)就越大,
        # 而crossentropy是会取负数的,因此最后的crossentropyloss就越小
        # 简单来说,最后变成了一个多分类问题,一共有vocab_size个类别,(那么多分类问题,用crossentropy来算loss就很合理了噻)
        # 每个batch最后的样本数是seq_length*batch_size,每个样本对应一个vocab_size长度的向量
        optimizer.zero_grad()
        loss.backward()
        nn.utils.clip_grad_norm_(model.parameters(),grad_clip)
        # 梯度裁剪,如果经过loss.backward计算出来的梯度大于阈值的话,就强行给它变成阈值
        optimizer.step()
        if batch_idx%200==0:
            print('这是第{}个epoch的第{}个batch,在训练集上的loss为:{}'.format(epoch+1,batch_idx+1,loss.item()))
        if batch_idx%400==0:
            valset_loss = evaluate(model,val_iter)
            if valset_loss<min_valset_loss:
                print('验证集上的loss({})有所下降,(下降:{}),已将模型参数保存'.format(valset_loss,min_valset_loss-valset_loss))
                min_valset_loss = valset_loss
                torch.save(model.state_dict(),'cql_model.txt')
            else:
                print('验证集上的loss未下降,现将lr调整为原来的一半')
                scheduler.step()
        
这是第1个epoch的第1个batch,在训练集上的loss为:9.922673225402832
验证集上的loss(9.89220618335267)有所下降,(下降:99999999999989.11),已将模型参数保存
这是第1个epoch的第201个batch,在训练集上的loss为:8.417543411254883
这是第1个epoch的第401个batch,在训练集上的loss为:8.640040397644043
验证集上的loss(8.475546773959298)有所下降,(下降:1.4166594093933718),已将模型参数保存
这是第1个epoch的第601个batch,在训练集上的loss为:9.542922973632812
这是第1个epoch的第801个batch,在训练集上的loss为:7.937783241271973
验证集上的loss(8.34979754309314)有所下降,(下降:0.1257492308661572),已将模型参数保存
这是第1个epoch的第1001个batch,在训练集上的loss为:9.498292922973633
这是第1个epoch的第1201个batch,在训练集上的loss为:8.406221389770508
验证集上的loss(8.262422278677203)有所下降,(下降:0.08737526441593779),已将模型参数保存
这是第1个epoch的第1401个batch,在训练集上的loss为:8.839925765991211
这是第1个epoch的第1601个batch,在训练集上的loss为:8.732605934143066
验证集上的loss(8.163779331569236)有所下降,(下降:0.09864294710796706),已将模型参数保存
这是第2个epoch的第1个batch,在训练集上的loss为:8.276674270629883
验证集上的loss(8.135769210518502)有所下降,(下降:0.02801012105073397),已将模型参数保存
这是第2个epoch的第201个batch,在训练集上的loss为:7.824665069580078
这是第2个epoch的第401个batch,在训练集上的loss为:8.028740882873535
... ...
... ...

6.将训练好的模型load进来并进行评估

best_model = rnnmodel(vocab_size=len(field.vocab),
                embed_size=embed_size,
                hidden_size=hidden_size,
                num_layers=num_layers)
if use_cuda:
    best_model = best_model.to(device)
best_model.load_state_dict(torch.load('cql_model.txt'))
<All keys matched successfully>
valset_loss = evaluate(best_model,val_iter)
print('perplexity:',np.exp(valset_loss)) #刚开始的困惑度有1500多,现在降到97 
perplexity: 97.70309939699754
# 用训练好的语言模型生成100个词的句子
hidden = best_model.init_hidden(num_layers,batch_size,True)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
input_ = torch.randint(low=0,high=len(field.vocab), size=(1, 1), dtype=torch.long).to(device)
print(input_)

words = [field.vocab.itos[input_]] 

for i in range(100):
    output, hidden = best_model(input_,hidden)
    word_weights = output.squeeze().exp().cpu()
    word_idx = torch.multinomial(word_weights, 1)[0]
    input_.fill_(word_idx)
    word = field.vocab.itos[word_idx]
    words.append(word)
print(" ".join(words))
# 可以看到“姑苏”后面紧跟“蓝氏”,“泽芜”后面紧跟“君”,“金光”后面紧跟“瑶”,说明这个语言模型还是学到了点东西
tensor([[2509]], device='cuda:0')
锁 回去 姑苏 蓝氏 秘技 历代 作祟 受害者 死者 间 聪明 魏先生 惨死 敛芳尊 对泽芜 君 魏无羡 一把 我要 不多言 机会 五官 冷静 滚 <unk> 里 看书 退 害怕 家仆 此刻 <unk> 情况 埋 专注 无比 加固 抽出 奇怪 魏无羡 脚下 蹿 挨 谈论 神色 要害 数名 少年 声音 重复 箐 担心 水底 随便 收尸 魏无羡 道 欺负 <unk> 时 香 此刻 驻镇 蓝曦臣 思绪 身影 身上 动 愣愣 地道 发现 问 独子 一群 少年 十之八九 四下 <unk> 昏迷 真的 第一眼 温家 爆发 流传 刀锋 凶 魏无羡 心想 一群 魏无羡道 多年 转身 惩治 <unk> 一动 修仙 山里 走到 打坐 金光 瑶

在这里插入图片描述

标签:vocab,loss,祖师,corpus,batch,model,LSTM,hidden,size
来源: https://blog.csdn.net/weixin_41391619/article/details/117454106

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有