问题模型:根据历史值 x t − 1 , … , x 1 x_{t-1},\dots,x_1 xt−1,…,x1预测 x t x_t xt的值,即 x t ∼ P ( x t ∣ x t − 1 , … , x 1 ) x_t\sim P(x_t|x_{t-1},\dots,x_1) xt∼P(xt∣xt−1,…,x1)
为了实现这个预测,可以使用回归模型。但存在一个主要问题:输入数据( x t − 1 , … , x 1 x_{t-1},\dots,x_1 xt−1,…,x1)的数量与 t t t有关,即输入数据的数量随着数据量的增加而增加。有以下2种策略可以进行近似处理:
自回归模型:假设在现实情况下相当长的序列 x t − 1 , … , x 1 x_{t-1},\dots,x_1 xt−1,…,x1是不必要的,我们只需要满足某个长度为 τ \tau τ的时间跨度, 即使用观测序列 x t − 1 , … , x t − τ x_{t-1},\dots,x_{t-\tau} xt−1,…,xt−τ。这样获得的最直接的好处就是参数的数量总是不变的。
隐变量自回归模型:保留一些对过去观测的总结 h t − 1 h_{t-1} ht−1,并且同时更新预测 x ^ t \hat{x}_t x^t和总结 h t h_{t} ht,即 x ^ t = P ( x t ∣ h t ) , h t = g ( h t − 1 , x t − 1 ) \hat{x}_t=P(x_t|h_t),~~~h_t=g(h_{t-1},x_{t-1}) x^t=P(xt∣ht),ht=g(ht−1,xt−1)
如何生成训练数据? 经典方法是使用历史观测来预测下一个未来观测。这种方法基于以下假设:虽然特定值 x t x_t xt可能会改变, 但是序列产生逻辑不变。即 P ( x 1 , … , x T ) = ∏ t = 1 T P ( x t ∣ x t − 1 , … , x 1 ) P(x_1,\dots,x_T)=\prod_{t=1}^T P(x_t|x_{t-1},\dots,x_1) P(x1,…,xT)=t=1∏TP(xt∣xt−1,…,x1) 这种假设是合理的,因为新的序列产生逻辑一定受新的数据影响, 而我们不可能用目前所掌握的数据来预测新的序列产生逻辑。
如果使用 x t − 1 , … , x t − τ x_{t-1},\dots,x_{t-\tau} xt−1,…,xt−τ来估计 x t x_t xt是近似精确的,则称序列满足马尔可夫条件。特别地,如果 τ = 1 \tau=1 τ=1,得到一个 一阶马尔可夫模型 P ( x 1 , … , x T ) = ∏ t = 1 T P ( x t ∣ x t − 1 ) , P ( x 1 ∣ x 0 ) = P ( x 1 ) P(x_1,\dots,x_T)=\prod_{t=1}^T P(x_t|x_{t-1}),P(x_1|x_0)=P(x_1) P(x1,…,xT)=t=1∏TP(xt∣xt−1),P(x1∣x0)=P(x1)
当假设 x t x_t xt仅是离散值时,可以使用动态规划沿着马尔可夫链精确地计算结果,简而言之,可以利用临近值的概率求解跨距离值的概率,由此我们只需要考虑历史临近值。比如 P ( x t + 1 ∣ x t − 1 ) = ∑ x t P ( x t + 1 , x t , x t − 1 ) P ( x t − 1 ) = ∑ x t P ( x t + 1 ∣ x t , x t − 1 ) P ( x t , x t − 1 ) P ( x t − 1 ) = ∑ x t P ( x t + 1 ∣ x t ) P ( x t , x t − 1 ) P ( x t − 1 ) ( 马尔可夫条件 ) = ∑ x t P ( x t + 1 ∣ x t ) P ( x t ∣ x t − 1 ) \begin{aligned} P(x_{t+1}|x_{t-1})&=\frac{\sum_{x_t}P(x_{t+1},x_t,x_{t-1})}{P(x_{t-1})}\\ &=\frac{\sum_{x_t}P(x_{t+1}|x_t,x_{t-1})P(x_t,x_{t-1})}{P(x_{t-1})}\\ &=\frac{\sum_{x_t}P(x_{t+1}|x_t)P(x_t,x_{t-1})}{P(x_{t-1})}(马尔可夫条件)\\ &=\sum_{x_t}P(x_{t+1}|x_t)P(x_t|x_{t-1}) \end{aligned} P(xt+1∣xt−1)=P(xt−1)∑xtP(xt+1,xt,xt−1)=P(xt−1)∑xtP(xt+1∣xt,xt−1)P(xt,xt−1)=P(xt−1)∑xtP(xt+1∣xt)P(xt,xt−1)(马尔可夫条件)=xt∑P(xt+1∣xt)P(xt∣xt−1)
import re, collections
defreadlines(file):"""将UTF-8文件内容加载到文本行的列表中"""withopen(file,"r", encoding="utf-8")as f:
lines = f.readlines()# 为简单起见,忽略标点符号,并将所有字母转换为小写return[re.sub("[^A-Za-z]+"," ", line).strip().lower()for line in lines]deftokenize(lines:list[str], token="word"):"""将文本行拆分为单词或字符词元"""if token =="word":return[line.split()for line in lines]elif token =="char":return[list(line)for line in lines]else:raise Exception(f"不支持的词元类型:{token}")classVocab:"""文本词表"""def__init__(
self,
tokens:list[list[str]],
min_freq:int=0,
reserved_tokens:list[str]=[],):# 按频率降序排序
self.__token_freqs =sorted(
self.count_tokens(tokens).items(), key=lambda x: x[1], reverse=True)
self.__ordered_tokens =["<unk>"]+ reserved_tokens
for token, freq in self.__token_freqs:if freq < min_freq:breakif token notin self.__ordered_tokens:
self.__ordered_tokens.append(token)
self.__idx_of_tokens ={
token: idx for idx, token inenumerate(self.__ordered_tokens)}def__len__(self):"""词表大小"""returnlen(self.__ordered_tokens)defcount_tokens(self, tokens:list[list[str]]):"""统计词元的频率"""
tokens =[token for line in tokens for token in line]return collections.Counter(tokens)defto_index(self, tokens:list[str]|tuple[str]|str):"""根据词元获取索引"""ifisinstance(tokens,str):return self.__idx_of_tokens.get(tokens, self.idx_of_unk)return[self.to_index(token)for token in tokens]defto_token(self, indices:list[int]|int):"""根据索引获取词元"""ifisinstance(indices,int):return self.__ordered_tokens[indices]return[self.__ordered_tokens[index]for index in indices]@propertydefidx_of_unk(self):"""未知词元的索引"""return0@propertydeftoken_freqs(self):"""词元频率"""return self.__token_freqs
@propertydefcontent(self):"""词表内容"""returnlist(self.__idx_of_tokens.items())defload_corpus(max_tokens:int=None):"""加载文本训练语料"""
lines = readlines("time_machine.txt")
tokens = tokenize(lines,"char")
vocab = Vocab(tokens)
corpus =[vocab.to_index(token)for line in tokens for token in line]if max_tokens isnotNone:
corpus = corpus[:max_tokens]return corpus, vocab
if __name__ =="__main__":
corpus, vocab = load_corpus()print(len(corpus),len(vocab))print(vocab.token_freqs[:10])print(vocab.content[:10])
3. 语言模型
假设长度为 T T T的文本序列中的词元依次为 x 1 , x 2 , … , x T x_1,x_2,\dots,x_T x1,x2,…,xT,在给定这样的文本序列时,语言模型的目标是估计序列的联合概率 P ( x 1 , x 2 , … , x T ) P(x_1,x_2,\dots,x_T) P(x1,x2,…,xT)
只需要一次抽取一个词元 x t ∼ P ( x t ∣ x t − 1 , … , x 1 ) x_t\sim P(x_t|x_{t-1},\dots,x_1) xt∼P(xt∣xt−1,…,x1),即根据模型估计的概率分布抽取第一个词元 x 1 x_1 x1,然后基于 x 1 x_1 x1抽取第二个词元 x 2 x_2 x2,依此类推,一个理想的语言模型就能够基于模型本身生成自然文本。
估计给定几个单词后出现某个单词的条件概率使用连续单词对出现频率之比的方法存在问题:连续单词对出现频率很低,很难估计条件概率。处理这种问题的常见策略是执行某种形式的拉普拉斯平滑, 具体方法是在所有计数中添加一个小常量。例如:( n n n表示训练集中单词的总数, m m m表示唯一单词的数量) P ^ ( x ) = n ( x ) + ϵ 1 / m n + ϵ 1 P ^ ( x ′ ∣ x ) = n ( x , x ′ ) + ϵ 2 P ^ ( x ′ ) n + ϵ 2 P ^ ( x ′ ′ ∣ x , x ′ ) = n ( x , x ′ , x ′ ′ ) + ϵ 3 P ^ ( x ′ ′ ) n + ϵ 3 \begin{aligned} \hat{P}(x)&=\frac{n(x)+\epsilon_1/m}{n+\epsilon_1}\\ \hat{P}(x'|x)&=\frac{n(x,x')+\epsilon_2 \hat{P}(x')}{n+\epsilon_2}\\ \hat{P}(x''|x,x')&=\frac{n(x,x',x'')+\epsilon_3 \hat{P}(x'')}{n+\epsilon_3}\\ \end{aligned} P^(x)P^(x′∣x)P^(x′′∣x,x′)=n+ϵ1n(x)+ϵ1/m=n+ϵ2n(x,x′)+ϵ2P^(x′)=n+ϵ3n(x,x′,x′′)+ϵ3P^(x′′)其中 ϵ 1 , ϵ 2 , ϵ 3 \epsilon_1,\epsilon_2,\epsilon_3 ϵ1,ϵ2,ϵ3是超参数,取值控制平滑程度。当 ϵ i = 0 \epsilon_i=0 ϵi=0时,不应用平滑,当 ϵ i → + ∞ \epsilon_i\rightarrow+\infin ϵi→+∞时, P ^ ( x ) \hat{P}(x) P^(x)接近 1 / m 1/m 1/m, P ^ ( x ′ ∣ x ) \hat{P}(x'|x) P^(x′∣x)接近 P ^ ( x ′ ) \hat{P}(x') P^(x′), P ^ ( x ′ ′ ∣ x , x ′ ) \hat{P}(x''|x,x') P^(x′′∣x,x′)接近 P ^ ( x ′ ′ ) \hat{P}(x'') P^(x′′),即越与上下文没有关系。 然而,这样的模型很容易变得无效:
需要存储所有的计数。
完全忽略了单词的意思,想根据上下文调整这类模型其实是相当困难的。
长单词序列大部分是没出现过的。
如果 P ( x t + 1 ∣ x t , … , x 1 ) = P ( x t + 1 ∣ x 1 ) P(x_{t+1}|x_t,\dots,x_1)=P(x_{t+1}|x_1) P(xt+1∣xt,…,x1)=P(xt+1∣x1),则序列上的分布满足一阶马尔可夫性质。 阶数越高,对应的依赖关系就越长。利用这种性质可以推导出许多可以应用于序列建模的近似公式: P ( x 1 , x 2 , x 3 , x 4 ) = P ( x 1 ) P ( x 2 ) P ( x 3 ) P ( x 4 ) P ( x 1 , x 2 , x 3 , x 4 ) = P ( x 1 ) P ( x 2 ∣ x 1 ) P ( x 3 ∣ x 2 ) P ( x 4 ∣ x 3 ) P ( x 1 , x 2 , x 3 , x 4 ) = P ( x 1 ) P ( x 2 ∣ x 1 ) P ( x 3 ∣ x 1 , x 2 ) P ( x 4 ∣ x 2 , x 3 ) \begin{aligned} P(x_1,x_2,x_3,x_4)&=P(x_1)P(x_2)P(x_3)P(x_4)\\ P(x_1,x_2,x_3,x_4)&=P(x_1)P(x_2|x_1)P(x_3|x_2)P(x_4|x_3)\\ P(x_1,x_2,x_3,x_4)&=P(x_1)P(x_2|x_1)P(x_3|x_1,x_2)P(x_4|x_2,x_3)\\ \end{aligned} P(x1,x2,x3,x4)P(x1,x2,x3,x4)P(x1,x2,x3,x4)=P(x1)P(x2)P(x3)P(x4)=P(x1)P(x2∣x1)P(x3∣x2)P(x4∣x3)=P(x1)P(x2∣x1)P(x3∣x1,x2)P(x4∣x2,x3)通常,涉及一个、两个和三个变量的概率公式分别被称为 一元语法、二元语法和三元语法模型。
英语最流行的词看起来很无聊, 这些词通常被称为停顿词,另外词频衰减的速度相当地快,大致遵循双对数坐标图上的一条直线,即词频满足齐普夫定律 n i ∝ 1 i α n_i \propto \frac{1}{i^\alpha} ni∝iα1这告诉我们想要通过计数统计和平滑来建模单词是不可行的, 因为这样建模的结果会大大高估尾部单词的频率,也就是所谓的不常用单词。
英语除了一元语法词,单词序列也遵循齐普夫定律(也就是说有高频词组和低频词组);另外 n n n元组的词表大小并没有很大,这说明语言中存在相当多的结构, 这些结构给了我们应用模型的希望。
使用 n n n元语法模型 P ( x t ∣ x t − 1 , … , x t − n + 1 ) P(x_t|x_{t-1},\dots,x_{t-n+1}) P(xt∣xt−1,…,xt−n+1),只能考虑到前 n − 1 n-1 n−1个词元的影响,如果想考虑前 n − 1 n-1 n−1个词元之前的词的影响,则需要增加 n n n,由此模型参数的数量也会随之呈指数增长,因为词表需要存储 ∣ N x ∣ n |N_x|^n ∣Nx∣n个词组。因而不如使用隐变量模型 P ( x t ∣ x t − 1 , … , x 1 ) ≈ P ( x t ∣ h t − 1 ) P(x_t|x_{t-1},\dots,x_1)\approx P(x_t|h_{t-1}) P(xt∣xt−1,…,x1)≈P(xt∣ht−1) 其中 h t − 1 h_{t-1} ht−1是隐状态或者隐变量,它存储了到时间步 t − 1 t-1 t−1的序列信息,我们可以基于当前输入 x t x_t xt和先前隐状态 h t − 1 h_{t-1} ht−1 来计算时间步 t t t处的隐状态: h t = f ( x t , h t − 1 ) h_t=f(x_t,h_{t-1}) ht=f(xt,ht−1)
假设我们在时间步 t t t有小批量输入 X t ∈ R n × d \mathrm{X_t}\in \mathbb{R}^{n\times d} Xt∈Rn×d,用 H t ∈ R n × h \mathrm{H_t}\in \mathbb{R}^{n\times h} Ht∈Rn×h表示时间步 t t t的隐变量,并引入新的权重参数 W h h ∈ R h × h \mathrm{W_{hh}}\in \mathbb{R}^{h\times h} Whh∈Rh×h来描述如何在当前时间步中使用前一个时间步的隐变量。 当前间步隐变量由当前时间步的输入与前一个时间步的隐变量一起计算得出( ϕ \phi ϕ是隐藏层的激活函数, W x h \mathrm{W_{xh}} Wxh是隐藏层的权重参数, b h \mathrm{b_h} bh是隐藏层的偏置参数) H t = ϕ ( X t W x h + H t − 1 W h h + b h ) \mathrm{H_t}=\phi(\mathrm{X_tW_{xh}+H_{t-1}W_{hh}+b_{h}}) Ht=ϕ(XtWxh+Ht−1Whh+bh) 从隐变量 H t \mathrm{H_t} Ht和 H t − 1 \mathrm{H_{t-1}} Ht−1的的关系可知, 这些变量捕获并保留了序列直到其当前时间步的历史信息, 就如当前时间步下神经网络的状态或记忆。由于在当前时间步中, 隐变量使用的定义与前一个时间步中使用的定义相同, 因此隐变量的计算是循环的。于是基于循环计算的隐状态神经网络被命名为循环神经网络(recurrent neural network,RNN)。 在循环神经网络中执行循环计算的层称为循环层。直接在隐藏层中添加上一时间步的隐变量是构建循环层最常见的方法。
时间步 t t t的输出层与多层感知机类似 O t = H t W h q + b q \mathrm{O_t=H_tW_{hq}+b_q} Ot=HtWhq+bq
循环神经网络的参数包括循环层的权重 W x h , W h h \mathrm{W_{xh},W_{hh}} Wxh,Whh和偏置 b h \mathrm{b_h} bh以及输出层的权重 W h q \mathrm{W_{hq}} Whq和偏置 b q \mathrm{b_q} bq。值得一提的是,即使在不同的时间步,循环神经网络也总是使用这些模型参数。
隐状态中 X t W x h + H t − 1 W h h = c a t ( X t , H t − 1 , 1 ) c a t ( W x h , W h h , 0 ) \mathrm{X_tW_{xh}+H_{t-1}W_{hh}=cat(X_t,H_{t-1},1)cat(W_{xh},W_{hh},0)} XtWxh+Ht−1Whh=cat(Xt,Ht−1,1)cat(Wxh,Whh,0) 即沿列(轴1)拼接矩阵 X t \mathrm{X_t} Xt和 H t − 1 \mathrm{H_{t-1}} Ht−1,沿行(轴0)拼接矩阵 W x h \mathrm{W_{xh}} Wxh和 W h h \mathrm{W_{hh}} Whh,得到的两个矩阵再相乘与分别相乘再相加的结果相等。
6. 困惑度
一个更好的语言模型应该能让我们更准确地预测下一个词元。 因此,它应该允许我们在压缩序列时花费更少的比特。 所以我们可以通过一个序列中所有的 n n n个词元的交叉熵损失的平均值来衡量: 1 n ∑ t = 1 n ( − log P ( x t ∣ x t − 1 , … , x 1 ) ) \frac{1}{n}\sum_{t=1}^{n}(-\log P(x_t|x_{t-1},\dots,x_1)) n1t=1∑n(−logP(xt∣xt−1,…,x1)) 由于历史原因,自然语言处理的科学家更喜欢使用一个叫做困惑度的量,上式是它的指数 exp ( 1 n ∑ t = 1 n ( − log P ( x t ∣ x t − 1 , … , x 1 ) ) ) \exp(\frac{1}{n}\sum_{t=1}^{n}(-\log P(x_t|x_{t-1},\dots,x_1))) exp(n1t=1∑n(−logP(xt∣xt−1,…,x1))) 困惑度的最好的理解是下一个词元的实际选择数的调和平均数。