最近开始系统的学习Pytorch,准备写一个系列的blog(大概5篇左右),记录一下自己学习的进程。这第一篇笔记,写的是PyTorch官方教程中序列模型和长短句记忆(LSTM)模型篇的课后练习题实现,网络很简单,但确实是我用Pytorch实现的第一个模型,值得写一篇blog来mark一下。
关于LSTM的基本概念和结构这里都不再过多介绍(想了解的来这里——一个非常有名的blog),主要说一下使用pytorch自带的lstm模型时一些需要注意的点:
tensor.view()
方法显式的修改embedding层的输出结构——embeds.view(len(sentence), 1, -1)
output, (h_n, c_n)
,其中 output
保存了最后一层,每个time step的输出h,如果是双向LSTM,每个time step的输出h = [h正向, h逆向] (同一个time step的正向和逆向的h连接起来);而h_n
实际上是每个layer最后一个状态(纵向)输出的拼接,c_n
则为每个layer最后一个状态记忆单元中的值的拼接。Note:这个地方先简单讲到这里,想更深入了解的推荐去看这篇,里面的图示很清晰。pytorch中LSTM的输入输出比想象中的复杂很多,其中还涉及到了输入序列的packed,后期会再开一篇来说一说这个。
教程中给出了 LSTM 网络来进行词性标注的代码,代码包括了三部分——数据准备,创建模型与训练模型。
def prepare_sequence(seq, to_ix):
idxs = [to_ix[w] for w in seq]
return torch.tensor(idxs, dtype=torch.long)
training_data = [
("The dog ate the apple".split(), ["DET", "NN", "V", "DET", "NN"]),
("Everybody read that book".split(), ["NN", "V", "DET", "NN"])
]
word_to_ix = {}
for sent, tags in training_data:
for word in sent:
if word not in word_to_ix:
word_to_ix[word] = len(word_to_ix)
print(word_to_ix)
tag_to_ix = {"DET": 0, "NN": 1, "V": 2}
# 实际中通常使用更大的维度如32维, 64维.
# 这里我们使用小的维度, 为了方便查看训练过程中权重的变化.
EMBEDDING_DIM = 6
HIDDEN_DIM = 6
class LSTMTagger(nn.Module):
def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size):
super(LSTMTagger, self).__init__()
self.hidden_dim = hidden_dim
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)
# LSTM以word_embeddings作为输入, 输出维度为 hidden_dim 的隐藏状态值
self.lstm = nn.LSTM(embedding_dim, hidden_dim)
# 线性层将隐藏状态空间映射到标注空间
self.hidden2tag = nn.Linear(hidden_dim, tagset_size)
self.hidden = self.init_hidden()
def init_hidden(self):
# 一开始并没有隐藏状态所以我们要先初始化一个
# 关于维度为什么这么设计请参考Pytoch相关文档
# 各个维度的含义是 (num_layers, minibatch_size, hidden_dim)
return (torch.zeros(1, 1, self.hidden_dim),
torch.zeros(1, 1, self.hidden_dim))
def forward(self, sentence):
embeds = self.word_embeddings(sentence)
lstm_out, self.hidden = self.lstm(
embeds.view(len(sentence), 1, -1), self.hidden)
tag_space = self.hidden2tag(lstm_out.view(len(sentence), -1))
tag_scores = F.log_softmax(tag_space, dim=1)
return tag_scores
model = LSTMTagger(EMBEDDING_DIM, HIDDEN_DIM, len(word_to_ix), len(tag_to_ix))
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 查看训练前的分数
# 注意: 输出的 i,j 元素的值表示单词 i 的 j 标签的得分
# 这里我们不需要训练不需要求导,所以使用torch.no_grad()
with torch.no_grad():
inputs = prepare_sequence(training_data[0][0], word_to_ix)
tag_scores = model(inputs)
print(tag_scores)
for epoch in range(300): # 实际情况下你不会训练300个周期, 此例中我们只是随便设了一个值
for sentence, tags in training_data:
# 第一步: 请记住Pytorch会累加梯度.
# 我们需要在训练每个实例前清空梯度
model.zero_grad()
# 此外还需要清空 LSTM 的隐状态,
# 将其从上个实例的历史中分离出来.
model.hidden = model.init_hidden()
# 准备网络输入, 将其变为词索引的 Tensor 类型数据
sentence_in = prepare_sequence(sentence, word_to_ix)
targets = prepare_sequence(tags, tag_to_ix)
# 第三步: 前向传播.
tag_scores = model(sentence_in)
# 第四步: 计算损失和梯度值, 通过调用 optimizer.step() 来更新梯度
loss = loss_function(tag_scores, targets)
loss.backward()
optimizer.step()
# 查看训练后的得分
with torch.no_grad():
inputs = prepare_sequence(training_data[0][0], word_to_ix)
tag_scores = model(inputs)
# 句子是 "the dog ate the apple", i,j 表示对于单词 i, 标签 j 的得分.
# 我们采用得分最高的标签作为预测的标签. 从下面的输出我们可以看到, 预测得
# 到的结果是0 1 2 0 1. 因为 索引是从0开始的, 因此第一个值0表示第一行的
# 最大值, 第二个值1表示第二行的最大值, 以此类推. 所以最后的结果是 DET
# NOUN VERB DET NOUN, 整个序列都是正确的!
print(tag_scores)
教程给出的模型只使用词向量作为序列模型的输入,相当于只考虑词级别的特征,而像词缀这样的字符级信息对于词性有很大的影响。比如说, 像包含词缀 -ly 的单词基本上都是被标注为副词。因此,接下来我们会在刚刚代码的基础上考虑加入每个的单词的字符级别的特征来增强词嵌入。
教程中给出的思路为:
因此,我们会在模型设置两个embedding层——character_embeddings
与word_embeddings
:
# 词嵌入
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)
# 字符嵌入
self.character_embeddings = nn.Embedding(character_size, character_embedding_dim)
两个LSTM层—— character-ltsm
与 tag-lstm
:
# lstm_character以每个字符的character_embeddings作为输入, 输出即为该单词对应字符级别的特征,输出维度为 character_hidden_dim 的隐藏状态值
self.lstm_character = nn.LSTM(character_embedding_dim, character_hidden_dim)
# tag_lstm以word_embeddings和该词字符级别特征的拼接向量作为输入, 输出维度为 hidden_dim 的隐藏状态值
self.tag_lstm = nn.LSTM(embedding_dim + character_hidden_dim, hidden_dim)
其中,character_embeddings
用来表示每个单词字符级别的embedding,这个embedding会输入到character-ltsm
中获取每个单词字符级别的特征;随后,character-ltsm
层的输出会和word_embeddings
层输出的词向量进行拼接,拼接后的结果作为新的词向量输入到tag-lstm
中进行序列的标注。
# 词嵌入
word_embed = self.word_embeddings(sentence_word)
# 获取单词字符级别的特征
word_character = words_characters[sentence_word.item()]
word_character_in = prepare_sequence(word_character, character_to_ix)
character_embeds = self.character_embeddings(word_character_in)
character_lstm_out, self.hidden_character = self.lstm_character(
character_embeds.view(len(word_character_in), 1, -1), self.hidden_character)
# 拼接词向量与字符级别的特征
embed = torch.cat((word_embed, self.hidden_character[0].view(-1)))
# 拼接句子中每个词的词向量,拼接后的结果作为tag_lstm的输入
embeds = torch.cat(embeds).view(len(sentence), 1, -1)
lstm_out, self.hidden = self.tag_lstm(embeds, self.hidden)
模型训练时,需要清空清空 LSTM 的隐状态,将其从上个实例的历史中分离出来
# 此外还需要清空 LSTM 的隐状态,
# 将其从上个实例的历史中分离出来.
model.hidden_tag = model.init_hidden(HIDDEN_DIM)
model.hidden_character = model.init_hidden(CHARACTER_HIDDEN_DIM)
大概就是这些了,萌新一枚,欢迎交流指正。
完整代码链接 -.-