Pytorch的LSTM的理解
class torch.nn.LSTM(*args, **kwargs)
参数列表
- input_size:x的特征维度
- hidden_size:隐藏层的特征维度
- num_layers:lstm隐层的层数,默认为1
- bias:False则bih=0和bhh=0. 默认为True
- batch_first:True则输入输出的数据格式为 (batch, seq, feature)
- dropout:除最后一层,每一层的输出都进行dropout,默认为: 0
- bidirectional:True则为双向lstm默认为False
- 输入:input, (h0, c0)
- 输出:output, (hn,cn)
输入数据格式:
input(seq_len, batch, input_size)
h0(num_layers * num_directions, batch, hidden_size)
c0(num_layers * num_directions, batch, hidden_size)
输出数据格式:
output(seq_len, batch, hidden_size * num_directions)
hn(num_layers * num_directions, batch, hidden_size)
cn(num_layers * num_directions, batch, hidden_size)
Pytorch里的LSTM单元接受的输入都必须是3维的张量(Tensors).每一维代表的意思不能弄错。
第一维体现的是序列(sequence)结构,也就是序列的个数,用文章来说,就是每个句子的长度,因为是喂给网络模型,一般都设定为确定的长度,也就是我们喂给LSTM神经元的每个句子的长度,当然,如果是其他的带有带有序列形式的数据,则表示一个明确分割单位长度,
例如是如果是股票数据内,这表示特定时间单位内,有多少条数据。这个参数也就是明确这个层中有多少个确定的单元来处理输入的数据。
第二维度体现的是batch_size,也就是一次性喂给网络多少条句子,或者股票数据中的,一次性喂给模型多少是个时间单位的数据,具体到每个时刻,也就是一次性喂给特定时刻处理的单元的单词数或者该时刻应该喂给的股票数据的条数
第三位体现的是输入的元素(elements of input),也就是,每个具体的单词用多少维向量来表示,或者股票数据中 每一个具体的时刻的采集多少具体的值,比如最低价,最高价,均价,5日均价,10均价,等等
H0-Hn是什么意思呢?就是每个时刻中间神经元应该保存的这一时刻的根据输入和上一课的时候的中间状态值应该产生的本时刻的状态值,
这个数据单元是起的作用就是记录这一时刻之前考虑到所有之前输入的状态值,形状应该是和特定时刻的输出一致
c0-cn就是开关,决定每个神经元的隐藏状态值是否会影响的下一时刻的神经元的处理,形状应该和h0-hn一致。
当然如果是双向,和多隐藏层还应该考虑方向和隐藏层的层数。
注:上式中的 q后面跟一个单词,表示该单词的一定维度的向量表示,该维度即是LSTM接受的张量中的第三个维度。
记住这里存在一个尺寸为1的第二维度。此外,如果你希望一次在网络中走完整个序列,你可以将第一个维度的尺寸也设为1。
下面我简单看一下例子:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
torch.manual_seed(1)
lstm = nn.LSTM(3, 3) # 输入单词用一个维度为3的向量表示, 隐藏层的一个维度3,仅有一层的神经元,
#记住就是神经元,这个时候神经层的详细结构还没确定,仅仅是说这个网络可以接受[seq_len,batch_size,3]的数据输入
print(lstm.all_weights)
inputs = [torch.randn(1, 3) for _ in range(5)]
# 构造一个由5个单单词组成的句子 构造出来的形状是 [5,1,3]也就是明确告诉网络结构我一个句子由5个单词组成,
#每个单词由一个1X3的向量组成,就是这个样子[1,2,3]
#同时确定了网络结构,每个批次只输入一个句子,其中第二维的batch_size很容易迷惑人
#对整个这层来说,是一个批次输入多少个句子,具体但每个神经元,就是一次性喂给神经元多少个单词。
print('Inputs:',inputs)
# 初始化隐藏状态
hidden = (torch.randn(1, 1, 3),
torch.randn(1, 1, 3))
print('Hidden:',hidden)
for i in inputs:
# 将序列的元素逐个输入到LSTM,这里的View是把输入放到第三维,看起来有点古怪,
#回头看看上面的关于LSTM输入的描述,这是固定的格式,以后无论你什么形式的数据,
#都必须放到这个维度。就是在原Tensor的基础之上增加一个序列维和MiniBatch维,
#这里可能还会有迷惑,前面的1是什么意思啊,就是一次把这个输入处理完,
#在输入的过程中不会输出中间结果,这里注意输入的数据的形状一定要和LSTM定义的输入形状一致。
# 经过每步操作,hidden 的值包含了隐藏状态的信息
out, hidden = lstm(i.view(1, 1, -1), hidden)
print('out1:',out)
print('hidden2:',hidden)
# 另外, 我们还可以一次对整个序列进行训练. LSTM 返回的第一个值表示所有时刻的隐状态值,
# 第二个值表示最近的隐状态值 (因此下面的 "out"的最后一个值和 "hidden" 的值是一样的).
# 之所以这样设计, 是为了通过 "out" 的值来获取所有的隐状态值, 而用 "hidden" 的值来
# 进行序列的反向传播运算, 具体方式就是将它作为参数传入后面的 LSTM 网络.
# 增加额外的第二个维度
inputs = torch.cat(inputs).view(len(inputs), 1, -1)
hidden = (torch.randn(1, 1, 3), torch.randn(1, 1, 3)) # clean out hidden state
out, hidden = lstm(inputs, hidden)
print('out2',out)
print('hidden3',hidden)
运行输出:
out2 tensor([[[-0.0187, 0.1713, -0.2944]],
[[-0.3521, 0.1026, -0.2971]],
[[-0.3191, 0.0781, -0.1957]],
[[-0.1634, 0.0941, -0.1637]],
[[-0.3368, 0.0959, -0.0538]]])
hidden3 (tensor([[[-0.3368, 0.0959, -0.0538]]]), tensor([[[-0.9825, 0.4715, -0.0633]]]))
接下来我们要做一件事,
就是训练网络帮我我们标注词性,当然实际的自然语言处理我们有很多成功的算法,但是应对新词总会有点麻烦,我们想啊,既然神经网络可以帮我们做了很多神奇的事,那么我们可不可以训练一个网路模型来帮我我们自动的标注词性呢,显然这个思路靠谱,使用神经网络的套路:
- 准备训练数据,这一步最是头大的,最好的办法就是找各大机构提供的标准的标注库,实在找不到,自己处理,国内外很多的分词标准库和工具可以用,jieba分词标注是一个不错的选择,使用起来也简单。
- 读取数据文件
- 分词
- 把词语和标注分别放在两个数组里面
- 构建词汇表、构建标注表
- 把分词结果转换成对应词汇表和标签表中的序号。
- 构建网络模型,这里使用Word2Vec预处理一下输入文本
- 训练网络
- 分析结果
下面按照这个套路上源码:
import jieba.posseg
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# import sys
import gensim
torch.manual_seed(2)
# sys.stdout = open('1.log', 'a')
sent='明天是荣耀运营十周年纪念日。' \
'荣耀从两周年纪念日开始,' \
'在每年的纪念日这天凌晨零点会开放一个新区。' \
'第十版账号卡的销售从三个月前就已经开始。' \
'在老区玩的不顺心的老玩家、准备进入荣耀的新手,都已经准备好了新区账号对这个日子翘首以盼。' \
'陈果坐到了叶修旁边的机器,随手登录了她的逐烟霞。' \
'其他九大区的玩家人气并没有因为第十区的新开而降低多少,' \
'越老的区越是如此,实在是因为荣耀的一个账号想经营起来并不容易。' \
'陈果的逐烟霞用了五年时间才在普通玩家中算是翘楚,哪舍得轻易抛弃。' \
'更何况到最后大家都会冲着十大区的共同地图神之领域去。'
words=jieba.posseg.cut(sent,HMM=True) #分词
processword=[]
tagword=[]
for w in words:
processword.append(w.word)
tagword.append(w.flag)
#词语和对应的词性做一一对应
texts=[(processword,tagword)]
#使用gensim构建本例的词汇表
id2word=gensim.corpora.Dictionary([texts[0][0]])
#每个词分配一个独特的ID
word2id=id2word.token2id
#使用gensim构建本例的词性表
id2tag=gensim.corpora.Dictionary([texts[0][1]])
#为每个词性分配ID
tag2id=id2tag.token2id
def sen2id(inputs):
return [word2id[word] for word in inputs]
def tags2id(inputs):
return [tag2id[word] for word in inputs]
#根据词汇表把文本输入转换成对应的词汇表的序号张量
def formart_input(inputs):
return torch.tensor(sen2id(inputs),dtype=torch.long)
#根据词性表把文本标注输入转换成对应的词汇标注的张量
def formart_tag(inputs):
return torch.tensor(tags2id(inputs),dtype=torch.long)
#定义网络结构
class LSTMTagger(torch.nn.Module):
def __init__(self,embedding_dim,hidden_dim,voacb_size,target_size):
super(LSTMTagger,self).__init__()
self.embedding_dim=embedding_dim
self.hidden_dim=hidden_dim
self.voacb_size=voacb_size
self.target_size=target_size
# 使用Word2Vec预处理一下输入文本
self.embedding=nn.Embedding(self.voacb_size,self.embedding_dim)
# LSTM 以 word_embeddings 作为输入, 输出维度为 hidden_dim 的隐状态值
self.lstm=nn.LSTM(self.embedding_dim,self.hidden_dim)
## 线性层将隐状态空间映射到标注空间
self.out2tag=nn.Linear(self.hidden_dim,self.target_size)
self.hidden = self.init_hidden()
def init_hidden(self):
# 开始时刻, 没有隐状态
# 关于维度设置的详情,请参考 Pytorch 文档
# 各个维度的含义是 (Seguence, minibatch_size, hidden_dim)
return (torch.zeros(1, 1, self.hidden_dim),
torch.zeros(1, 1, self.hidden_dim))
def forward(self,inputs):
# 预处理文本转成稠密向量
embeds=self.embedding((inputs))
#根据文本的稠密向量训练网络
out,self.hidden=self.lstm(embeds.view(len(inputs),1,-1),self.hidden)
#做出预测
tag_space=self.out2tag(out.view(len(inputs),-1))
tags=F.log_softmax(tag_space,dim=1)
return tags
model=LSTMTagger(10,10,len(word2id),len(tag2id))
loss_function=nn.NLLLoss()
optimizer=optim.SGD(model.parameters(),lr=0.1)
#看看随机初始化网络的分析结果
with torch.no_grad():
input_s=formart_input(texts[0][0])
print(input_s)
print(processword)
tag_s=model(input_s)
for i in range(tag_s.shape[0]):
print(tag_s[i])
# print(tag_s)
for epoch in range(300):
# 再说明下, 实际情况下你不会训练300个周期, 此例中我们只是构造了一些假数据
for p ,t in texts:
# Step 1. 请记住 Pytorch 会累加梯度
# 每次训练前需要清空梯度值
model.zero_grad()
# 此外还需要清空 LSTM 的隐状态
# 将其从上个实例的历史中分离出来
# 重新初始化隐藏层数据,避免受之前运行代码的干扰,如果不重新初始化,会有报错。
model.hidden = model.init_hidden()
# Step 2. 准备网络输入, 将其变为词索引的Tensor 类型数据
sentence_in=formart_input(p)
tags_in=formart_tag(t)
# Step 3. 前向传播
tag_s=model(sentence_in)
# Step 4. 计算损失和梯度值, 通过调用 optimizer.step() 来更新梯度
loss=loss_function(tag_s,tags_in)
loss.backward()
print('Loss:',loss.item())
optimizer.step()
#看看训练后的结果
with torch.no_grad():
input_s=formart_input(texts[0][0])
tag_s=model(input_s)
for i in range(tag_s.shape[0]):
print(tag_s[i])