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