最近在学习一个课程,课程里一个项目就是看图说话,需要提起使用CNN提取图像特征,然后使用LSTM进行文本特征提取,然后将图像特征和文本特征进行合并,扔到网络里面进行训练,完成看图说话的训练,CNN很熟悉,LSTM第一次接触,记录下新手使用 LSTM 的过程。
一、RNN
下图是RNN模型的结构图
上图的RNN 的工作模式是这样的:
假如更定"the students opened their"四个单词,预测第5个单词出现的概率,先把"the"这个单词的one-hot即独热编码的向量,通过Embedding将稀疏的独热向量变成一个稠密的向量
e
e
e,通过下面的公式获得中间隐藏层的值:
h
(
t
)
=
σ
(
W
h
h
t
−
1
+
W
e
e
(
t
)
+
b
1
h^{(t)} = \sigma(W_hh^{t-1} + W_ee{(t)} + b_1
h(t)=σ(Whht−1+Wee(t)+b1
那么
h
(
1
)
h^{(1)}
h(1)的计算方式是:
h
(
1
)
=
σ
(
W
h
h
0
+
W
e
e
(
1
)
+
b
1
)
h^{(1)} = \sigma(W_hh^{0} + W_ee^{(1)} + b_1)
h(1)=σ(Whh0+Wee(1)+b1)
h
(
0
)
h^{(0)}
h(0)可以随机初始化,即
h
(
0
)
h^{(0)}
h(0)与
W
h
W_h
Wh相乘之后加上
e
(
1
)
e^{(1)}
e(1)与
W
e
W_e
We相乘的结果,得到了中间变量
h
(
1
)
h^{(1)}
h(1),。
第二个单词"students"进来之后,重复和第一个单词的过程,只不过这里使用的中间变量是
h
(
1
)
h^{(1)}
h(1),里面包含有第一个单词"the"的一些信息,这样可以生成中间变量
h
(
2
)
h^{(2)}
h(2),
h
(
2
)
h^{(2)}
h(2)里面有第一个单词"the"和第二个单词"students"的信息,依次计算下去,一直到计算出来
h
(
4
)
h^{(4)}
h(4),
h
(
4
)
h^{(4)}
h(4)是我们看过了"the students opened their"四个单词之后的结果,然后
h
(
4
)
h^{(4)}
h(4)经过下面的公式获得概率分布;
y
(
t
)
=
s
o
f
t
m
a
x
(
U
h
(
t
)
+
b
2
)
y^{(t)} = softmax(Uh^{(t)} + b_2)
y(t)=softmax(Uh(t)+b2)
如果 e ( t ) e^{(t)} e(t)转置的尺寸是(4, 1), h ( t − 1 ) h^{(t-1)} h(t−1)转置的尺寸是(13, 11),那么 W e W_e We的尺寸是(13, 4), W h W_h Wh的尺寸是(13, 13),那么 h ( t ) h^{(t)} h(t)转置的尺寸是13x13x13x1+13x4x4x1=(13, 1).
下面图是RNN 的训练模式:
例如我们要训练一句话"the students opened their exams",首先我们拿出"the",进行one-hot编码,然后进行embedding,成为
e
(
1
)
e^{(1)}
e(1),
e
(
1
)
e^{(1)}
e(1)经过下面的公式得到的隐藏层变量
h
(
1
)
h^{(1)}
h(1):
h
(
t
)
=
σ
(
W
h
h
t
−
1
+
W
e
e
(
t
)
+
b
1
h^{(t)} = \sigma(W_hh^{t-1} + W_ee{(t)} + b_1
h(t)=σ(Whht−1+Wee(t)+b1
然后经过下面的公式获得概率分布
y
(
1
)
y^{(1)}
y(1)
y
(
t
)
=
s
o
f
t
m
a
x
(
U
h
(
t
)
+
b
2
)
y^{(t)} = softmax(Uh^{(t)} + b_2)
y(t)=softmax(Uh(t)+b2)
y
(
1
)
y^{(1)}
y(1)是预测下一个单词的概率分布,而下一个单词的真实值是"students“,这样根据预测值和真实值可以计算Loss函数
J
(
1
)
(
θ
)
J^{(1)}(\theta)
J(1)(θ),那么按照同意的方法可以计算,给定"the sutdent”预测"opened"的损失值
J
(
2
)
(
θ
)
J^{(2)}(\theta)
J(2)(θ), 给定"the sutdent opened”预测"their"的损失值
J
(
3
)
(
θ
)
J^{(3)}(\theta)
J(3)(θ), 给定"the sutdent opened their”预测"exams"的损失值
J
(
4
)
(
θ
)
J^{(4)}(\theta)
J(4)(θ), 然后
L
o
s
s
=
J
(
1
)
(
θ
)
+
J
(
2
)
(
θ
)
+
J
(
3
)
(
θ
)
+
J
(
4
)
(
θ
)
Loss = J^{(1)}(\theta) + J^{(2)}(\theta) +J^{(3)}(\theta) +J^{(4)}(\theta)
Loss=J(1)(θ)+J(2)(θ)+J(3)(θ)+J(4)(θ)
RNN的优势是:1、能够处理任何长度的输入; 2、t时刻时能够使用t-1, t-2, t-3,…等时刻的信息;3、增加输入的长度,模型的大小不会变;4、每个时刻共享权重。
RNN的劣势:1、RNN很慢(上一步相乘的结果作为下一步相乘的输入);2、实际上,RNN使用更多的信息是离自己越近的时刻。
二、LSTM
1、什么是LSTM
传统的RNN一直在擦除和改写中间隐藏层变量
h
(
t
)
h^{(t)}
h(t),可以给RNN不同的存储的地方来保持一个特别的状态,这种思路就是LSTM(long short-term memory),来解决梯度消失的问题。
下图是LSTM各种门的意义:
f
(
t
)
f^{(t)}
f(t)是遗忘门,之前的状态中哪部分是被保留,哪部分是被遗忘的;
i
(
t
)
i^{(t)}
i(t)是输入门,新的cell中哪部分被写入;
o
(
t
)
o^{(t)}
o(t)是输出门,控制哪一部分输出成为下个隐藏层;
c
~
(
t
)
\tilde{c}^{(t)}
c~(t)是新的单元格内容
c
(
t
)
{c}^{(t)}
c(t)是单元门,从上一个单元内容中擦除一些内容,把新的内容写入;
h
(
t
)
h^{(t)}
h(t)是隐藏层
X
t
X_t
Xt进入之后和
h
t
−
1
h_{t-1}
ht−1一起根据公式计算出
f
t
f_{t}
ft,
i
t
i_{t}
it,
o
t
o_{t}
ot,
c
~
t
\tilde{c}^t
c~t,然后根据这四个门计算出
c
t
{c}^t
ct和
h
t
h^t
ht
lstm结构让RNN 能够保存更长久的信息,这种可以比较大程度上减弱梯度消失的问题,但是不能避免这个问题,但是LSTM在实际中依旧取得了很好的成绩。
三、实操训练LSTM
下面是一个基础款的lstm的训练流程
1、 准备训练数据文本库:
例如 Flickr8K数据集,8000 图像, 每幅图5个标题, 描述图像里面的事物和事件
2、 tokenizer (分词,文本向量化)
例如,lines是Flickr8K数据集中训练集的语料库,下面是keras中的tokenizer在这个语料库上训练的结果
tokenizer = Tokenizer() #初始化
tokenizer.fit_on_texts(lines) # 训练
test_line = " in street racer armor be examine the tire"
print (tokenizer.texts_to_sequences(line)[0])
[4, 73, 711, 4558, 497, 2782, 5, 465]
通过上面的方式," in street racer armor be examine the tire"就向量化为[4, 73, 711, 4558, 497, 2782, 5, 465],我们可以用一个小的例子,来看下是怎么向量化的
tokenizer = Tokenizer()
tokenizer.fit_on_texts(["I am am am the a student",
"being a student student student feeling good, "])
print (tokenizer.index_word)
print (tokenizer.word_index)
print (tokenizer.word_counts)
{1: 'student', 2: 'am', 3: 'a', 4: 'i', 5: 'the', 6: 'being', 7: 'feeling', 8: 'good'}
{'student': 1, 'am': 2, 'a': 3, 'i': 4, 'the': 5, 'being': 6, 'feeling': 7, 'good': 8}
OrderedDict([('i', 1), ('am', 3), ('the', 1), ('a', 2), ('student', 4), ('being', 1), ('feeling', 1), ('good', 1)])
word_counts: 字典,将单词(字符串)映射为它们在训练期间出现的次数。仅在调用fit_on_texts之后设置。
word_docs: 字典,将单词(字符串)映射为它们在训练期间所出现的文档或文本的数量。仅在调用fit_on_texts之后设置。
word_index: 字典,将单词(字符串)映射为它们的排名或者索引。仅在调用fit_on_texts之后设置。
3、 pad_sequences 填充序列使文本集中所有文本长度相同。
上面的步骤之后,将字符串进行量化,但是由于每句的长度不一样,因此需要做填充
# in_seq是已经向量化过的
in_seq = [2]
pad_sequences([in_seq], maxlen=10)[0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 2]
4、 embedding 进行嵌入化编码
one-hot编码表示太浪费内存了,需要讲稀疏的独热编码,变成稠密的向量,边稠密的原理是,独热编码的一个特点是向量与向量之间是独立同分布的,看不到向量与向量之间的关系,但是在自然语言中,词语与词语之间是有关系的,例如爸爸的爸爸是我的爷爷,那么爷爷和我之间的关联是很紧密的,但是在one-hot编码中,从编码的结果看,爷爷和我之间没有任何关系。
torch.nn.Embedding(num_embeddings, embedding_dim, padding_idx=None,
max_norm=None, norm_type=2.0, scale_grad_by_freq=False,
sparse=False, _weight=None)
- num_embeddings (python:int) – 词典的大小尺寸,比如总共出现5000个词,那就输入5000。此时index为(0-4999)
- embedding_dim (python:int) – 嵌入向量的维度,即用多少维来表示一个符号。
- padding_idx (python:int, optional) – 填充id,比如,输入长度为100,但是每次的句子长度并不一样,后面就需要用统一的数字填充,而这里就是指定这个数字,这样,网络在遇到填充id时,就不会计算其与其它符号的相关性。(初始化为0)
- max_norm (python:float, optional) – 最大范数,如果嵌入向量的范数超过了这个界限,就要进行再归一化。
- norm_type (python:float, optional) – 指定利用什么范数计算,并用于对比max_norm,默认为2范数。
- scale_grad_by_freq (boolean, optional) – 根据单词在mini-batch中出现的频率,对梯度进行放缩。默认为False.
- sparse (bool, optional) – 若为True,则与权重矩阵相关的梯度转变为稀疏张量。
a = torch.LongTensor([0])
embedding = torch.nn.Embedding(2, 5)
print(embedding(a))
Out[29]: tensor([[1.7931, 0.5004, 0.3444, 0.7140, 0.3001]]
5、 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)