之前讲的CNN更多的是处理空间信息,而序列模型(RNN、LSTM这一类)主要是处理时间信息。现实生活中,很多数据是有时序结构的。比如豆瓣的电影评分,不光是跟电影好坏有关,还会随时间的变化而变化:
序列数据还包括:
自回归模型:给定t个数据预测下一个数据,标签和样本是一个东西。常见是输入法输入、GPT-2。
参考《学习笔记10:统计学习方法:——HMM和CRF》
文本是最常见序列之一。 例如,一篇文章可以被简单地看作是一串单词序列,甚至是一串字符序列。 本节中,我们将解析文本的常见预处理步骤。 这些步骤通常包括:
下面以H.G.Well的《时光机器》为例子进行介绍
首先,我们从H.G.Well的时光机器中加载文本。这是一个相当小的语料库,只有30000多个单词,下面的函数(将数据集读取到由多条文本行组成的列表中),其中每条文本行都是一个字符串。为简单起见,我们在这里忽略了标点符号和字母大写。
import collections
import re
from d2l import torch as d2l
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
'090b5e7e70c295757f55df93cb0a180b9691891a')
def read_time_machine(): #@save
"""将时间机器数据集加载到文本行的列表中"""
with open(d2l.download('time_machine'), 'r') as f:
lines = f.readlines()
return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]
lines = read_time_machine()
print(f'# 文本总行数: {len(lines)}')
print(lines[0])
print(lines[10])
# 文本总行数: 3221
the time machine by h g wells
twinkled and his usually pale face was flushed and animated the
下面的tokenize函数将文本行列表(lines)作为输入,返回一个由词元(token)列表组成的列表,每个词元都是一个字符串(string)。 文本行列表中的每个元素是一个文本序列(如一条文本行), 每个文本序列又被拆分成一个词元列表。
def tokenize(lines, token='word'): #@save
"""将文本行拆分为单词或字符词元"""
if token == 'word':
return [line.split() for line in lines]
elif token == 'char':
return [list(line) for line in lines]
else:
print('错误:未知词元类型:' + token)
tokens = tokenize(lines)
词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。 现在,让我们[构建一个字典,通常也叫做词表(vocabulary), 用来将字符串类型的词元映射到从 0 开始的数字索引中]。
Tips:对token的次数进行排序,常用词就会在词表的开头,这样计算机会经常访问这一块的内容,读取会比较快,做embedding也会较好。(性能会好一点点)。类与对象参考《python学习笔记——类与对象、常用函数》
class Vocab: #@save
"""文本词表,reserved_tokens表示句子开始结尾的单词"""
def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
if tokens is None:
tokens = []
if reserved_tokens is None:
reserved_tokens = []
# 按出现频率排序
counter = count_corpus(tokens)
self.token_freqs = sorted(counter.items(), key=lambda x: x[1],
reverse=True)
# 未知词元的索引为0,uniq_tokens就是包含所有词的序列
self.unk, uniq_tokens= 0,['' ] + reserved_tokens
uniq_tokens+=[token for token,freq in self.token_freqs
if freq>min_freq and token not in uniq_tokens]
#下面就开始构造词和索引的词典self.token_to_idx
self.idx_to_token,self.token_to_idx=[],dict()
for token in uniq_tokens:
self.idx_to_token.append(token)#只是一个列表
self.token_to_idx[token] = len(self.idx_to_token) - 1
def __len__(self):
return len(self.idx_to_token)
def __getitem__(self, tokens):#给定token返回下标索引
if not isinstance(tokens, (list, tuple)):
return self.token_to_idx.get(tokens, self.unk)#找到返回下标,没找到返回的下标
return [self.__getitem__(token) for token in tokens]
def to_tokens(self, indices):#给定索引返回对应的token
if not isinstance(indices, (list, tuple)):
return self.idx_to_token[indices]
return [self.idx_to_token[index] for index in indices]
def count_corpus(tokens): #@save
"""统计词元的频率"""
# 这里的tokens是1D列表或2D列表,函数isinstance()可以判断一个变量的类型
if len(tokens) == 0 or isinstance(tokens[0], list):
# 将词元列表展平成一个列表
tokens = [token for line in tokens for token in line]
return collections.Counter(tokens)
我们首先使用时光机器数据集作为语料库来[构建词表],然后打印前几个高频词元及其索引。
vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10],vocab.idx_to_token[:10])#类属性
print(vocab['the', 'i', 'and', 'of', 'a', 'to', 'was', 'in', 'that'])#vocab是字典,直接根据词得到索引
len(vocab),vocab.to_tokens([0,1,2,3,4,5,6,7,8,9,])#类方法
[('' , 0), ('the', 1), ('i', 2), ('and', 3), ('of', 4), ('a', 5), ('to', 6), ('was', 7), ('in', 8), ('that', 9)] ['' , 'the', 'i', 'and', 'of', 'a', 'to', 'was', 'in', 'that']
[1, 2, 3, 4, 5, 6, 7, 8, 9]
(4580, ['' , 'the', 'i', 'and', 'of', 'a', 'to', 'was', 'in', 'that'])
在使用上述函数时,我们[将所有功能打包到load_corpus_time_machine函数中], 该函数返回corpus(词元索引列表)和vocab(时光机器语料库的词表)。 我们在这里所做的改变是:
def load_corpus_time_machine(max_tokens=-1): #@save
"""返回时光机器数据集的词元索引列表和词表"""
lines = read_time_machine()
tokens = tokenize(lines, 'word')
vocab = Vocab(tokens)
# 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落,
# 所以将所有文本行展平到一个列表中,corpus是词的索引
corpus = [vocab[token] for line in tokens for token in line]
if max_tokens > 0:
corpus = corpus[:max_tokens]
return corpus, vocab
corpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)#char模式下len(vocab)=28,这是指26个字母和unk以及空格
参考李沐动手深度学习8.3《语言模型和数据集》
假设长度为 T T T的文本序列中的词元依次为 x 1 , x 2 , … , x T x_1, x_2, \ldots, x_T x1,x2,…,xT。 x t x_t xt( 1 ≤ t ≤ T 1 \leq t \leq T 1≤t≤T)可以被认为是文本序列在时间步 t t t处的观测或标签。在给定这样的文本序列时, 语言模型(language model)的目标是估计文本序列的联合概率
P ( x 1 , x 2 , … , x T ) . P(x_1, x_2, \ldots, x_T). P(x1,x2,…,xT).
例如,只需要一次抽取一个词元 x t ∼ P ( x t ∣ x t − 1 , … , x 1 ) x_t \sim P(x_t \mid x_{t-1}, \ldots, x_1) xt∼P(xt∣xt−1,…,x1),一个理想的语言模型就能够基于模型本身生成自然文本。
语言模型的应用包括:
参考《天池-新闻文本分类-task1机器学习算法》1.1 内容,这里就不写了
最大的优点是不论文本有多长,计算复杂度都是O(t),只是空间复杂度较高,要把所有n-gram存下来,n增大,存储量指数级增加。(但是实际中,去掉低频组合之后,n取较大也能用,比较长的序列有实际意义才会多次出现)
前馈神经网络:信息往一个方向流动。包括MLP和CNN
循环神经网络:信息循环流动,网络隐含层输出又作为自身输入,包括RNN、LSTM、GAN等。
RNN模型结构如下图所示:
其输出为:
h t = t a n h ( W x h x t + b x h + W h h h t − 1 + b h h ) \mathbf {h_{t}=tanh(W^{xh}x_{t}+b^{xh}+W^{hh}h_{t-1}+b^{hh})} ht=tanh(Wxhxt+bxh+Whhht−1+bhh)
y n = s o f t m a x ( W h y h n + b h y ) \mathbf {y_{n}=softmax(W^{hy}h_{n}+b^{hy})} yn=softmax(Whyhn+bhy)
在前向传播时:
h t = t a n h ( W x h x t + b x h + W h h h t − 1 + b h h ) \mathbf {h_{t}=tanh(W^{xh}x_{t}+b^{xh}+W^{hh}h_{t-1}+b^{hh})} ht=tanh(Wxhxt+bxh+Whhht−1+bhh)
假设最后时刻为t,反向传播求对i时刻的导数为:
∂ L o s s ∂ W i h h = ∂ L o s s ∂ y t ⋅ ∂ y t ∂ h i ⋅ ∂ h i ∂ W i h h \mathbf {\frac{\partial Loss}{\partial W_{i}^{hh}}=\frac{\partial Loss}{\partial y_{t}^{}}\cdot \frac{\partial y_{t}^{}}{\partial h_{i}}\cdot \frac{\partial h_{i}^{}}{\partial W_{i}^{hh}}} ∂Wihh∂Loss=∂yt∂Loss⋅∂hi∂yt⋅∂Wihh∂hi
∂ h i ∂ W i h h = ( h i − 1 ) T \mathbf {\frac{\partial h_{i}}{\partial W_{i}^{hh}}=(h_{i-1})^T} ∂Wihh∂hi=(hi−1)T
∂ y t ∂ h i = ∂ y t ∂ h t ⋅ ∂ h t ∂ h i = ∂ y t ∂ h t ⋅ t a n h ′ ⋅ ∂ h t ∂ ( h t − 1 ) T ⋅ tanh ′ ⋅ ∂ h t − 1 ∂ ( h t − 2 ) T . . . ⋅ tanh ′ ⋅ ∂ h i + 1 ∂ ( h i ) T = ∂ y t ∂ h t ⋅ ( t a n h ′ ) t − i ⋅ W t − i \mathbf {\frac{\partial y_{t}}{\partial h_{i}}=\frac{\partial y_{t}}{\partial h_{t}}\cdot\frac{\partial h_{t}}{\partial h_{i}}=\frac{\partial y_{t}}{\partial h_{t}}\cdot tanh'\cdot\frac{\partial h_{t}}{\partial (h_{t-1})^{T}}\cdot\tanh'\cdot\frac{\partial h_{t-1}}{\partial (h_{t-2})^{T}}...\cdot\tanh'\cdot\frac{\partial h_{i+1}}{\partial (h_{i})^{T}}=\frac{\partial y_{t}}{\partial h_{t}}\cdot (tanh')^{t-i}\cdot W^{t-i}} ∂hi∂yt=∂ht∂yt⋅∂hi∂ht=∂ht∂yt⋅tanh′⋅∂(ht−1)T∂ht⋅tanh′⋅∂(ht−2)T∂ht−1...⋅tanh′⋅∂(hi)T∂hi+1=∂ht∂yt⋅(tanh′)t−i⋅Wt−i
所以最终结果是: ∂ L o s s ∂ W i h h = ∂ L o s s ∂ y t ⋅ ∂ y t ∂ h t ⋅ ( t a n h ′ ) t − i ⋅ W t − i ⋅ ( h i − 1 ) T \mathbf {\frac{\partial Loss}{\partial W_{i}^{hh}}=\frac{\partial Loss}{\partial y_{t}}\cdot\frac{\partial y_{t}}{\partial h_{t}}\cdot (tanh')^{t-i}\cdot W^{t-i}\cdot(h_{i-1})^T} ∂Wihh∂Loss=∂yt∂Loss⋅∂ht∂yt⋅(tanh′)t−i⋅Wt−i⋅(hi−1)T
可以看到涉及到矩阵W的连乘。
线性代数中有: W = P − 1 Σ P W=P^{-1}\Sigma P W=P−1ΣP
其中, E = P − 1 P E=P^{-1} P E=P−1P为单位矩阵, Σ \Sigma Σ为对角线矩阵,对角线元素为W对应的特征值。即
Σ = [ λ 1 . . . 0 . . . . . . . . . . . . . . . λ m ] \Sigma =\begin{bmatrix} \lambda _{1} & ... & 0\\ ... &... &... \\ ... & ... &\lambda _{m} \end{bmatrix} Σ=⎣⎡λ1...............0...λm⎦⎤
所以有:
W = P − 1 Σ T P = Σ = [ λ 1 T . . . 0 . . . . . . . . . . . . . . . λ m T ] W=P^{-1}\Sigma^T P=\Sigma =\begin{bmatrix} \lambda _{1}^T & ... & 0\\ ... &... &... \\ ... & ... &\lambda _{m} ^T \end{bmatrix} W=P−1ΣTP=Σ=⎣⎡λ1T...............0...λmT⎦⎤
所以有:
RNN的缺点是信息经过多个隐含层传递到输出层,会导致信息损失。更本质地,会造成网络参数难以优化。LSTM加入全局信息context,可以解决这一问题。
1. 跨层连接
LSTM首先将隐含层更新方式改为:
u t = t a n h ( W x h x t + b x h + W h h h t − 1 + b h h ) \mathbf {u_{t}=tanh(W^{xh}x_{t}+b^{xh}+W^{hh}h_{t-1}+b^{hh})} ut=tanh(Wxhxt+bxh+Whhht−1+bhh)
h t = h t − 1 + u t \mathbf {h_{t}=h_{t-1}+u_{t}} ht=ht−1+ut
这样可以直接将 h k h_{k} hk与 h t h_{t} ht相连,实现跨层连接,减小网络层数,使得网络参数更容易被优化。证明如下:
h t = h t − 1 + u t = h t − 2 + u t − 1 + u t = . . . = h k + u k + 1 + u k + 2 + . . . + u t − 1 + u t \mathbf {h_{t}=h_{t-1}+u_{t}=h_{t-2}+u_{t-1}+u_{t}=...=h_{k}+u_{k+1}+u_{k+2}+...+u_{t-1}+u_{t}} ht=ht−1+ut=ht−2+ut−1+ut=...=hk+uk+1+uk+2+...+ut−1+ut
可以看出RNN的输入层隐含层和输出层三层都是共享参数,到了LSTM都变成参数不共享了。
bilstm=nn.LSTM(
input_size=1024,
hidden_size=512,
batch_first=True,
num_layers=2,#堆叠层数
dropout=0.5,
bidirectional=True#双向循环)
hidden, hn = self.rnn(inputs)
#hidden是各时刻的隐含层,hn为最后时刻隐含层
hidden, (hn, cn) = self.lstm(inputs)
#hidden是各时刻的隐含层,hn, cn为最后时刻隐含层和记忆细胞
encoder最后状态的输出输入decoder作为其第一个隐含状态 h 0 h_0 h0。decoder每时刻的输出都会加入下一个时刻的输入序列,一起预测下一时刻的输出,直到预测出End结束。