序列模型
序列
序列通常被描述为被排成一列的元素对象,是一种最常见的数据结构形式之一。序列模型 (Sequence Model )是一类专门用于处理和预测序列数据的模型。这类模型在自然语言处理、音频处理、时间序列分析等领域有着广泛的应用,举例如下
人类语言的语音识别
股市的每日行情预测
大语言模型的文本生成
视频、游戏的画面帧生成
我们以离散时间序列为例,来讨论如何处理和预测序列数据。令时间 t = 1 , 2 , ⋯ t=1,2,\cdots t = 1 , 2 , ⋯ 观察到的随机变量为 x t x_t x t ,那么观察 T T T 次后,我们就得到了 T T T 个不一定独立 的随机变量。这些随机变量的联合分布律可以表示为
( x 1 , x 2 , ⋯ , x T ) ∼ P ( x ) (x_1,x_2,\cdots,x_T)\sim P(\boldsymbol{x})
( x 1 , x 2 , ⋯ , x T ) ∼ P ( x )
根据条件概率的乘法公式
P ( x i x j ) = P ( x i ) P ( x j ∣ x i ) = P ( x j ) P ( x i ∣ x j ) P(x_ix_j)=P(x_i)P(x_j|x_i)=P(x_j)P(x_i|x_j)
P ( x i x j ) = P ( x i ) P ( x j ∣ x i ) = P ( x j ) P ( x i ∣ x j )
因此,可以对 P ( x ) P(\boldsymbol{x}) P ( x ) 进行概率展开
P ( x ) = P ( x 1 ) P ( x 2 ∣ x 1 ) P ( x 3 ∣ x 1 x 2 ) ⋯ P ( x T ∣ x 1 x 2 ⋯ x T − 1 ) = ∏ t = 1 T P ( x t ∣ x 1 ⋯ x t − 1 ) \begin{aligned}P(\boldsymbol{x})&=P(x_1)P(x_2|x_1)P(x_3|x_1x_2)\cdots P(x_T|x_1x_2\cdots x_{T-1})\\&=\prod_{t=1}^T P(x_t|x_{1}\cdots x_{t-1})\end{aligned}
P ( x ) = P ( x 1 ) P ( x 2 ∣ x 1 ) P ( x 3 ∣ x 1 x 2 ) ⋯ P ( x T ∣ x 1 x 2 ⋯ x T − 1 ) = t = 1 ∏ T P ( x t ∣ x 1 ⋯ x t − 1 )
因此,我们需要讨论如何求这 T T T 个条件概率。这实际上可以看做一个建模问题,我们需要基于不同的假设给出不同的求法。下面是两个常用的模型假设方案。
马尔科夫假设
对于一个长序列数据,我们是不一定要考虑每一个前置变量的。举例而言,我们要预测每日的天气情况,目前已经预测了325天。如果我们把前面所有325天的天气情况都考虑进来,去预测第326天的天气,这显然是不必要的,因为很久之前的天气和现在的天气已经关系不大了。
一般的,假设当前数据只跟过去 τ \tau τ 个数据点相关,那么有
P ( x T ∣ x 1 ⋯ x T − 1 ) = P ( x T ∣ x T − τ ⋯ x T − 1 ) P(x_T|x_1\cdots x_{T-1})=P(x_T|x_{T-\tau}\cdots x_{T-1})
P ( x T ∣ x 1 ⋯ x T − 1 ) = P ( x T ∣ x T − τ ⋯ x T − 1 )
该假设称为马尔科夫假设 ,此时的预测结果变为
P ( x ) = ∏ t = 1 T P ( x t ∣ x t − τ ⋯ x t − 1 ) P(\boldsymbol{x})=\prod_{t=1}^T P(x_t|x_{t-\tau}\cdots x_{t-1})
P ( x ) = t = 1 ∏ T P ( x t ∣ x t − τ ⋯ x t − 1 )
若假设数据点满足线性关系
x t = c + ϕ 1 x t − 1 + ϕ 2 x t − 2 + ⋯ + ϕ τ x t − τ + ε t x_t=c+\phi_1x_{t-1}+\phi_2x_{t-2}+\cdots+\phi_{\tau}x_{t-\tau}+\varepsilon_t
x t = c + ϕ 1 x t − 1 + ϕ 2 x t − 2 + ⋯ + ϕ τ x t − τ + ε t
称之为 τ \tau τ 阶自回归模型 (Autoregressive, AR ),其中 c c c 为偏置项,ε t ∼ N ( 0 , σ 2 ) \varepsilon_t\sim N(0,\sigma^2) ε t ∼ N ( 0 , σ 2 ) 为白噪声项,ϕ i \phi_i ϕ i 为参数。
当 τ = 1 \tau=1 τ = 1 时,称之为一阶马尔科夫模型。
潜变量假设
另一种假设方案是潜变量假设 ,其核心思想可以抽象如下:事物的发展是由一个不可见的内部隐藏状态 h h h 驱动的。我们观测到的数据,只是这个隐藏状态的外在表现 x i x_i x i 。
假设当前的数据 x \boldsymbol{x} x 对应潜变量 h h h ,现在想要预测下一步的 x ∗ \boldsymbol{x}^* x ∗ ,我们可以这样做
考虑建模问题:通过 x \boldsymbol{x} x 和其潜变量 h h h 推导出下一时刻的潜变量 h ∗ h^* h ∗ 。因为我们在假设中提到,潜变量 h h h 才是整个序列发展的关键因素
基于建模结果,通过预测的潜变量 h ∗ h^* h ∗ 和已有 x \boldsymbol{x} x ,预测外在表现 x ∗ \boldsymbol{x}^* x ∗
这个过程中的潜变量 h h h 一般需要通过人为建模找出。比如在股市行情预测中,我们观察到的是股价的时间序列 x t x_t x t 。由于股市行和市场心理是紧密相关的,这是一个抽象的、不可预测的潜变量 h t h_t h t 。因此,我们可以通过数学建模的方法整理出 h t = g ( x t − 1 , h t − 1 ) h_t=g(\boldsymbol{x}_{t-1},h_{t-1}) h t = g ( x t − 1 , h t − 1 ) ,再根据 h t h_t h t 预测下一步的 x t \boldsymbol{x}_t x t 。
这种假设称为潜变量序列模型 。
代码实现
我们尝试把马尔科夫假设模型运用到MLP上,我们使用正弦函数和白噪声生成长度为1000的序列。
1 2 3 4 5 6 7 8 %matplotlib inline import torchfrom torch import nnfrom d2l import torch as d2lT = 1000 time = torch.arange(1 , T + 1 , dtype=torch.float32) x = torch.sin(0.01 * time) + torch.normal(0 , 0.2 , (T,))
想要使用MLP模型,主要难点是把一维的时间序列,变成传统机器学习/深度学习模型所需要的二维矩阵数据(特征矩阵 X \boldsymbol{X} X 和标签向量 y \boldsymbol{y} y )。根据马尔科夫假设,令 τ = 4 \tau=4 τ = 4 ,那么就把 x t = ( x t − τ , ⋯ , x t − 1 ) \boldsymbol{x}_t=(x_{t-\tau},\cdots,x_{t-1}) x t = ( x t − τ , ⋯ , x t − 1 ) 作为特征 features,y t = x t y_t=x_t y t = x t 作为标签 labels。我们采用前600个特征-标签对 作为训练数据
1 2 3 4 5 6 7 8 9 10 11 12 13 tau = 4 features = torch.zeros((T - tau, tau)) for i in range (tau): features[:, i] = x[i: T - tau + i] labels = x[tau:].reshape((-1 , 1 )) batch_size, n_train = 16 , 600 train_iter = d2l.load_array((features[:n_train], labels[:n_train]), batch_size, is_train=True )
基于MLP部分,搭建一个简单的多层感知机并执行训练
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def init_weights (m ): if type (m) == nn.Linear: nn.init.xavier_uniform_(m.weight) def get_net (): net = nn.Sequential(nn.Linear(4 , 10 ), nn.ReLU(), nn.Linear(10 , 1 )) net.apply(init_weights) return net loss = nn.MSELoss(reduction='none' ) def train (net, train_iter, loss, epochs, lr ): trainer = torch.optim.Adam(net.parameters(), lr) for epoch in range (epochs): for X, y in train_iter: trainer.zero_grad() l = loss(net(X), y) l.sum ().backward() trainer.step() print (f'epoch {epoch + 1 } , ' f'loss: {d2l.evaluate_loss(net, train_iter, loss):f} ' ) net = get_net() train(net, train_iter, loss, 5 , 0.01 )
我们根据前 600 对训练数据,考察模型的序列预测能力。对于直到 x t x_t x t 的观测序列,其在时间 t + k t+k t + k 处的预测输出 x ^ t + k \hat{x}_{t+k} x ^ t + k 称为 k k k 步预测 。对于单步预测(k = 1 k=1 k = 1 )而言,我们的预测都是基于已有的真实数据 X 做出的下一步预测,而对于 k > 1 k>1 k > 1 的情况,我们需要借助自己模型的预测值去预测。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 max_steps = 64 features = torch.zeros((T - tau - max_steps + 1 , tau + max_steps)) for i in range (tau): features[:, i] = x[i: i + T - tau - max_steps + 1 ] for i in range (tau, tau + max_steps): features[:, i] = net(features[:, i - tau:i]).reshape(-1 ) steps = (1 , 4 , 16 , 64 ) d2l.plot([time[tau + i - 1 : T - max_steps + i] for i in steps], [features[:, (tau + i - 1 )].detach().numpy() for i in steps], 'time' , 'x' , legend=[f'{i} -step preds' for i in steps], xlim=[5 , 1000 ], figsize=(6 , 3 ))
下图给出了不同步长预测的结果,可以发现 k k k 增大的同时,预测能力变差。
我们提到的 600 对训练数据只是为了训练模型参数,不要和 k k k 步预测的使用参数搞混。
文本预处理
文本序列
对于序列数据处理问题,文本处理是最常见例子之一。一篇文章可以被简单地看作一串单词序列 ,甚至是一串字符序列。本节中,我们将解析文本的常见预处理步骤,这些步骤通常包括以下四点
将文本作为字符串加载到内存中
将字符串拆分为词元 (Token )
建立一个词表,将拆分的词元映射到数字索引
将文本转换为数字索引序列,方便模型操作
我们从 H. G. Well 的科幻文章 The Time Machine 为例,尝试进行文本预处理。这个文章一共包括约三万个单词,我们忽略其中的标点和字母的大写,把文章读取为若干个文本行 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import collectionsimport refrom d2l import torch as d2ld2l.DATA_HUB['time_machine' ] = (d2l.DATA_URL + 'timemachine.txt' , '090b5e7e70c295757f55df93cb0a180b9691891a' ) def read_time_machine (): 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()
Token 化
为了实现 Token 化,我们将文本行列表 lines 作为输入,其每个元素是一个文本序列(如一条文本行),而每个文本序列又被处理为一个词元列表。词元 (Token ) 是人工智能模型处理文本信息的最小单位,类型为字符串,我们可以粗略理解为一个单词或语言单位。
例如,英文文本序列 I am a boy. 的 Token 列表可以表示为 ['I', 'am', 'a', 'boy', '.'] ;中文文本序列 姬你太美 的 Token 列表可以表示为 ['姬', '你', '太', '美', '!']。这里只是举个例子,真实的 Token 的划分遵循一套特殊的算法,后续文章会介绍。
1 2 3 4 5 6 7 8 9 10 11 def tokenize (lines, token='word' ): 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)
词表
词元的类型是字符串,而模型需要的输入是数字,因此词元类型不方便模型使用。幸运的是,我们可以借助 Python 等工具创建一个词表 (Vocabulary ),将字符串类型的词元映射为从 0 0 0 开始的数字索引中。
首先,我们需要将 Token 化后所有的词元进行整合与统计,得到一个语料库 (Corpus ),其记录了每个出现词元的频数。我们一般按照词元的频率高低分配对应的数字索引,而频率高的词一般优先分配,频率低的词靠后分配。特别地,对于一些很少出现的词元,我们一般将其剔除,原因有二
当模型的训练文本数据较少时,面对某个新的未知输入词元,我们可能无法匹配一个语料库中的已有索引,此时我们规定一个未知词元 <unk>,将语料库中不存在或者被删除的词元映射为 <unk>。
与此同时,我们可以增加一些特殊功能词元。
<pad>:填充词元,作为占位无意义词
<bos>:序列开始词元,作为文本启动词
<eos>:序列结束词元,作为文本休止词
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class Vocab : 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 ) self .idx_to_token = ['<unk>' ] + reserved_tokens self .token_to_idx = {token: idx for idx, token in enumerate (self .idx_to_token)} for token, freq in self ._token_freqs: if freq < min_freq: break if token not in self .token_to_idx: 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 ): 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 ): if not isinstance (indices, (list , tuple )): return self .idx_to_token[indices] return [self .idx_to_token[index] for index in indices] @property def unk (self ): return 0 @property def token_freqs (self ): return self ._token_freqs def count_corpus (tokens ): if len (tokens) == 0 or isinstance (tokens[0 ], list ): tokens = [token for line in tokens for token in line] return collections.Counter(tokens)
我们尝试获取 The Time Machine 的语料库和词库大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 def load_corpus_time_machine (max_tokens=-1 ): lines = read_time_machine() tokens = tokenize(lines, 'char' ) vocab = Vocab(tokens) 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() print (len (corpus), len (vocab))
这里,代码会自动下载文本文件 timemachine.txt 在如下路径。
1 2 3 4 5 Root\ ├── code\ │ └── TextPre.ipynb └── data\ └── timemachine.txt
语言模型
模型目标
对于某个文本序列,其词元分别为 x 1 , x 2 , ⋯ , x T x_1,x_2,\cdots,x_T x 1 , x 2 , ⋯ , x T ,那么 x t ( 1 ⩽ t ⩽ T ) x_t(1\leqslant t\leqslant T) x t ( 1 ⩽ t ⩽ T ) 可以视作文本序列在时间 t t t 处的值。因此,给定这样一个文本序列,语言模型 (Language Model )要做的就是估计序列的联合概率
P ( x 1 , x 2 , ⋯ , x T ) = ∏ t = 1 T P ( x t ∣ x t − τ ⋯ x t − 1 ) P(x_1,x_2,\cdots,x_T)=\prod_{t=1}^T P(x_t|x_{t-\tau}\cdots x_{t-1})
P ( x 1 , x 2 , ⋯ , x T ) = t = 1 ∏ T P ( x t ∣ x t − τ ⋯ x t − 1 )
根据前文有关时间序列的讨论,为了训练语言模型,我们需要计算单词的概率,以及给定前面几个单词后出现某个单词的条件概率。这些概率本质上就是语言模型的参数。假设 n ( x i ) n(x_i) n ( x i ) 表示词元 x i x_i x i 出现的频数,n ( x i , x j , ⋯ ) n(x_i,x_j,\cdots) n ( x i , x j , ⋯ ) 表示连续词元对 x i , x j , ⋯ x_i,x_j,\cdots x i , x j , ⋯ 出现的频数,那么有估计概率
P ^ ( x 2 ∣ x 1 ) = n ( x 1 , x 2 ) n ( x 1 ) \hat{P}(x_2|x_1)=\frac{n(x_1,x_2)}{n(x_1)}
P ^ ( x 2 ∣ x 1 ) = n ( x 1 ) n ( x 1 , x 2 )
N元语法
当序列很长时,由于文本量不够大,通常会导致某个连续多词元对的出现频数 n ( x i , x j , ⋯ ) ⩽ 1 n(x_i,x_j,\cdots)\leqslant 1 n ( x i , x j , ⋯ ) ⩽ 1 ,因此上述估计通常是不准确的。此时可以考虑马尔科夫假设。对于最简单的情况,如果每个词元之间相互独立,那么就有一元语法模型 (Unigram )
P ( x 1 , x 2 , ⋯ , x T ) = P ( x 1 ) P ( x 2 ) ⋯ P ( x T ) P(x_1,x_2,\cdots,x_T)=P(x_1)P(x_2)\cdots P(x_T)
P ( x 1 , x 2 , ⋯ , x T ) = P ( x 1 ) P ( x 2 ) ⋯ P ( x T )
这通常是不成立的,因为文本序列是一种高度依赖前后文的序列。进一步的,假设每个词元之和前一个词元有关,这就是一个二元语法模型 (Bigram )。模型可以表示为
P ( x 1 , x 2 , ⋯ , x T ) = P ( x 1 ) ∏ t = 2 T P ( x t ∣ x t − 1 ) P(x_1,x_2,\cdots,x_{T})=P(x_1)\prod_{t=2}^T P(x_t|x_{t-1})
P ( x 1 , x 2 , ⋯ , x T ) = P ( x 1 ) t = 2 ∏ T P ( x t ∣ x t − 1 )
根据大数定律,当训练数据足够多时有
P ( x t ∣ x t − 1 ) ≈ n ( x t − 1 , x t ) n ( x t − 1 ) P(x_t|x_{t-1})\approx \frac{n(x_{t-1},x_t)}{n(x_{t-1})}
P ( x t ∣ x t − 1 ) ≈ n ( x t − 1 ) n ( x t − 1 , x t )
如果假设每个词元和前两个词元有关,便得到一个三元语法模型 (Trigram )
P ( x 1 , x 2 , ⋯ , x T ) = P ( x 1 , x 2 ) ∏ t = 3 T P ( x t ∣ x t − 1 x t − 2 ) P(x_1,x_2,\cdots,x_{T})=P(x_1,x_2)\prod_{t=3}^T P(x_t|x_{t-1}x_{t-2})
P ( x 1 , x 2 , ⋯ , x T ) = P ( x 1 , x 2 ) t = 3 ∏ T P ( x t ∣ x t − 1 x t − 2 )
一般的,假设某个词元只和前 n − 1 n-1 n − 1 个词元有关,那么该预测模型为 n n n 元语法模型 。但是,当 n n n 变大时,连续词元组合仍然有可能很少甚至不出现,这就导致模型再次失效。一种常见的策略是执行拉普拉斯平滑 ,对每个计数加上一个小正数(通常为1)来避免零概率问题出现。
齐普夫定律困境
我们根据前文的 The Time Machine 词表,画出双对数坐标下的词频图。
首先考虑单个词元(一元语法),第 i i i 个最常用词元的频率 n i n_i n i 近似满足
n i ∝ 1 i α n_i\propto \frac{1}{i^{\alpha}}
n i ∝ i α 1
这一统计规律通常记为齐普夫定律 (Zipf’s Law ),其表明在自然语言文本中,一个词出现的频率(的幂次)与它在频率表里的排名近似成反比 。我们可以打印出 The Time Machine 词表中前十个频数最高的词元如下
1 2 3 4 5 6 7 8 9 10 [('the' , 2261 ), ('i' , 1267 ), ('and' , 1245 ), ('of' , 1155 ), ('a' , 816 ), ('to' , 695 ), ('was' , 552 ), ('in' , 541 ), ('that' , 443 ), ('my' , 440 )]
这些词通常称为高频停用词 ,他们在文本含义上通常简简单单,但是出现的频率是很高的;相反的,对于更多具有直观含义的词语,他们出现的次数可能只有一两次,但是这样的词有很多。这在常规坐标下的频数分布图上体现为长尾分布 。
从上图中还可以看出,除了一元语法词,多元单词序列似乎也遵循齐普夫定律。由于 n n n 元词元组通常很少出现,这使得拉普拉斯平滑也非常不适合语言建模。因此我们需要使用基于深度学习的模型来解决这一问题。
读取长序列
由于序列数据具有连续性,当序列变得太长而不能被模型一次性全部处理时,我们可能希望拆分这样的序列方便模型读取。下面,我们将描述如何借助随机采样 (Random Sampling )和顺序分区 (Sequential Partitioning )策略实现长序列的读取。
在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。 在迭代过程中,来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元,因此标签是移位了一个词元的原始序列。
在顺序分区中,我们保证两个相邻的小批量中的子序列在原始序列上也是相邻的。这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此称为顺序分区。
循环神经网络
隐状态
前面我们提到,n n n 元语法模型通常不是一个理想的语言模型,因此我们回过头来看第一节中提出的潜变量假设 。通常,我们可以基于当前输入 x t \boldsymbol{x}_{t} x t 和先前隐状态 h t − 1 h_{t-1} h t − 1 来计算时间步 t t t 处的任何时间的隐状态 (Hidden State )
h t = f ( x t , h t − 1 ) h_t=f(\boldsymbol{x}_t,h_{t-1})
h t = f ( x t , h t − 1 )
循环神经网络 (*Recurrent Neural Network,RNN)是具有隐状态的神经网络。在介绍循环神经网络模型之前,我们首先回顾一下多层感知机模型。对于一个单隐藏层的MLP而言,其模型可以表示为
H = σ ( X W x h + b h ) \boldsymbol{H}=\sigma(\boldsymbol{XW}_{xh}+\boldsymbol{b}_h)
H = σ ( XW x h + b h )
O = H W h q + b q \boldsymbol{O}=\boldsymbol{HW}_{hq}+\boldsymbol{b}_q
O = HW h q + b q
其中参数矩阵 W x h , b h \boldsymbol{W}_{xh},\boldsymbol{b}_h W x h , b h 表示其将输入 X \boldsymbol{X} X 转化为隐藏层变量 H \boldsymbol{H} H ,σ ( ⋅ ) \sigma(\cdot) σ ( ⋅ ) 是激活函数,W h q , b q \boldsymbol{W}_{hq},\boldsymbol{b}_q W h q , b q 表示其将隐藏层变量 H \boldsymbol{H} H 转化为输出层 O \boldsymbol{O} O 。对于一个分类问题,我们可以把 s o f t m a x ( O ) \mathrm{softmax}(\boldsymbol{O}) softmax ( O ) 作为输出类别的概率分布。
如果我们要引入隐状态,即对于时间步 t t t 而言有小批量输入 X t ∈ R m × n \boldsymbol{X}_t\in\mathbb{R}^{m\times n} X t ∈ R m × n ,其每一行对应该序列在时间步 t t t 的一个样本。令该时间步的隐变量为 H t ∈ R m × h \boldsymbol{H}_t\in\mathbb{R}^{m\times h} H t ∈ R m × h ,并引入新的权重参数 W h h ∈ R h × h \boldsymbol{W}_{hh}\in\mathbb{R}^{h\times h} W hh ∈ R h × h 来描述前一个时间步的 H t − 1 \boldsymbol{H}_{t-1} H t − 1 对当前时间步的贡献。具体而言有
H t = σ ( X t W x h + H t − 1 W h h + b h ) \boldsymbol{H}_t=\sigma(\boldsymbol{X}_t\boldsymbol{W}_{xh}+\boldsymbol{H}_{t-1}\boldsymbol{W}_{hh}+\boldsymbol{b}_h)
H t = σ ( X t W x h + H t − 1 W hh + b h )
从上述关系式中看出,隐藏变量 H t \boldsymbol{H}_{t} H t 等捕获并保留了序列直到其当前时间步的历史信息,就如当前时间步下神经网络的状态或记忆,因此这样的隐藏变量被称为隐状态 (Hidden State )。对于每一个时间步 t t t 而言,其输出可以表示为
O t = H t W h q + b q \boldsymbol{O}_t=\boldsymbol{H}_t\boldsymbol{W}_{hq}+\boldsymbol{b}_q
O t = H t W h q + b q
对于序列模型而言,每一个时间步 t = 1 , 2 , ⋯ t=1,2,\cdots t = 1 , 2 , ⋯ 都应该有对应的输出。值得一提的是,即使在不同的时间步,循环神经网络也总是使用这些模型参数。 因此,循环神经网络的参数开销不会随着时间步的增加而增加。
注意到,在隐状态的循环神经网络中,我们将当前输入和上一步的隐藏变量用加法链接,即 X t W x h + H t − 1 W h h \boldsymbol{X}_t\boldsymbol{W}_{xh}+\boldsymbol{H}_{t-1}\boldsymbol{W}_{hh} X t W x h + H t − 1 W hh 。事实上,这可以看做输入和隐藏变量的拼接矩阵运算,即有分块矩阵形式
X t W x h + H t − 1 W h h = [ X t H t − 1 ] [ W x h W h h ] \boldsymbol{X}_t\boldsymbol{W}_{xh}+\boldsymbol{H}_{t-1}\boldsymbol{W}_{hh}=\begin{bmatrix}\boldsymbol{X}_t&\boldsymbol{H}_{t-1}\end{bmatrix}\begin{bmatrix}\boldsymbol{W}_{xh}\\\boldsymbol{W}_{hh}\end{bmatrix}
X t W x h + H t − 1 W hh = [ X t H t − 1 ] [ W x h W hh ]
这从某种程度上,更直观的展示了循环神经网络和隐状态的融合使用。
字符级语言建模
Bengio 等人首先提出了使用神经网络进行语言建模的概念。在一个循环神经网络模型中,设小批量大小为1,批量中的文本序列为一个单词。为了简化后续部分的训练,我们考虑使用字符级语言模型 (Character-level Language Model ),将文本词元化为字符而不是单词。下图演示了如何通过基于字符级语言建模的循环神经网络, 使用当前的和先前的字符预测下一个字符。
实践中,我们使用批量大小为 m > 1 m>1 m > 1 ,每个词元由一个 n n n 维向量表示,因此输入 X ∈ R m × n \boldsymbol{X}\in\mathbb{R}^{m\times n} X ∈ R m × n 。
困惑度
最后,让我们讨论如何度量语言模型的质量,这将在后续部分中用于评估基于循环神经网络的模型。信息论知识表明,一个长度为 T T T 序列的交叉熵可以表示为
H = − 1 T ∑ t = 1 T log P ( x t ∣ x 1 ⋯ x t − 1 ) H=-\frac{1}{T}\sum_{t=1}^T \log P(x_t|x_1\cdots x_{t-1})
H = − T 1 t = 1 ∑ T log P ( x t ∣ x 1 ⋯ x t − 1 )
定义其困惑度 (Perplexity )为
P P = 2 H PP=2^{H}
P P = 2 H
困惑度的最好的理解是“下一个词元的实际选择数的调和平均数”,其等价形式为
P P = P ( x 1 , x 2 , ⋯ , x T ) − 1 T PP=P(x_1,x_2,\cdots,x_T)^{-\frac{1}{T}}
P P = P ( x 1 , x 2 , ⋯ , x T ) − T 1
如果困惑度较低,说明语言模型对序列的预测较为准确
如果困惑度较高,说明模型在预测下一个词时存在较大的不确定性,性能较差
代码
主播先run去期末周复习了,有缘更新哦(づ ̄3 ̄)づ╭❤~