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

文章目录

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文件

原始文档和停用词文档
使用LSTM训练语言模型(以《魔道祖师》为corpus)
使用LSTM训练语言模型(以《魔道祖师》为corpus)

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> 一动 修仙 山里 走到 打坐 金光 瑶

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

上一篇:韩顺平细说Servlet视频系列意外收获之用命令行编译带有包的java类解决方案


下一篇:pytorch中LSTM各参数理解