语言建模
- 1. 统计语言模子
- 2. N-gram语言建模
- 3. 语言模子评估
- 4. 神经语言模子
- 5. 循环神经网络
- 5.1. Vanilla RNN
- 5.2. LSTM
1. 统计语言模子
统计语言模子旨在量化自然语言文本中序列的概率分布,即计算一个词序列(如一个句子或文档)出现的大概性。这类模子基于统计方法,使用大量文本数据学习语言的统计规律,进而推测未知文本的概率,大概为给定的文本序列生成最大概的后续词汇。
统计语言模子的核心思想是将语言视为一个随机过程,每一个词的选择都受到其上下文的影响。模子通常定义为一个概率函数,比如假设一个句子由 T T T个单词顺序组成:
W = w T : = ( w 1 , w 2 , ⋯ , w T ) W = w^{T} := (w_{1}, w_{2}, \cdots, w_{T}) W=wT:=(w1,w2,⋯,wT)
那么该句子的连合概率如下:
p ( w T ) = p ( w 1 ) ⋅ p ( w 2 ∣ w 1 ) ⋅ p ( w 3 ∣ w 1 2 ) ⋯ p ( w T ∣ x 1 T − 1 ) p(w^{T}) = p(w_{1}) \cdot p(w_{2}|w_{1}) \cdot p(w_{3}|w_{1}^{2}) \cdots p(w_{T}|x_{1}^{T-1}) p(wT)=p(w1)⋅p(w2∣w1)⋅p(w3∣w12)⋯p(wT∣x1T−1)
其中,模子参数为:
p ( w 1 ) , p ( w 2 ∣ w 1 ) , p ( w 3 ∣ w 1 2 ) , ⋯ , p ( w T ∣ w 1 T − 1 ) p(w_{1}), p(w_{2}|w_{1}) , p(w_{3}|w_{1}^{2}), \cdots, p(w_{T}|w_{1}^{T-1}) p(w1),p(w2∣w1),p(w3∣w12),⋯,p(wT∣w1T−1)
根据贝叶斯公式可得:
p ( w k ∣ w 1 k − 1 ) = p ( w 1 k ) p ( w 1 k − 1 ) p(w_{k}|w_{1}^{k-1}) = \frac{p(w_{1}^{k})}{p(w_{1}^{k-1})} p(wk∣w1k−1)=p(w1k−1)p(w1k)
根据大数定理可得:
p ( w k ∣ w 1 k − 1 ) ≈ c o u n t ( w 1 k ) c o u n t ( w 1 k − 1 ) p(w_{k}|w_{1}^{k-1}) \approx \frac{count(w_{1}^{k})}{count(w_{1}^{k-1})} p(wk∣w1k−1)≈count(w1k−1)count(w1k)
其中count体现统计词串在语料中的出现次数,当k比较大时,上述计算比较耗时。
2. N-gram 语言建模
N-gram模子是一种统计语言模子,用于推测给定文本中下一个词(或字符)的概率。该模子基于一个简化的假设:一个词(或字符)的出现概率只依赖于它前面的N-1个词(或字符),也就是n-1阶的马尔科夫假设。这里的N代表了模子考虑的上下文窗口大小,因此模子被称为N-gram。
p ( w k ∣ w 1 k − 1 ) ≈ p ( w k ∣ w k − n + 1 k − 1 ) ≈ count ( w k − n + 1 k ) count ( w k − n + 1 k − 1 ) \begin{aligned} p(w_{k}|w_{1}^{k-1}) &\approx p(w_{k}|w_{k-n+1}^{k-1}) \\ &\approx \frac{\text{count}(w_{k-n+1}^{k})}{\text{count}(w_{k-n+1}^{k-1})} \end{aligned} p(wk∣w1k−1)≈p(wk∣wk−n+1k−1)≈count(wk−n+1k−1)count(wk−n+1k)
N-gram模子中的概率通常通过从大型文本语料库中计算词序列的频次来估计。具体来说,使用最大似然估计(Maximum Likelihood Estimation, MLE)计算每个N-gram的概率,这些概率可以通过简单地统计每个N-gram在语料库中出现的频次来估计。
该模子基于这样一种假设,第N个词的出现只与前面N-1个词相干,而与别的任何词都不相干,整句的概率就是各个词出现概率的乘积。这些概率可以通过直接从语料中统计N个词同时出现的次数得到,然后可以使用这些概率来计算给定上下文情况下下一个词或字符的概率。常用的是二元(Bi-gram)建模和三元(Tri-gram)建模。
比方,在一个句子"ChatGPT is a powerful language model"中,如果我们使用2-gram,那么句子可以被分成以下2-gram序列:[“ChatGPT is”, “is a”, “a powerful”, “powerful language”, “language model”]。假设我们有一个充足大的文本语料库,其中包罗很多句子。我们可以使用2-gram语言模子来计算给定一个词的前提下,下一个词出现的概率。如果我们想要推测句子中的下一个词,我们可以使用前面的一个词作为上下文,并计算每个大概的下一个词的概率。比方,在句子"ChatGPT is a"中,我们可以计算出给定上下文"ChatGPT is"下一个词"powerful"的概率。通过统计语料库中"ChatGPT is"背面是"powerful"的次数,并将其除以"ChatGPT is"出现的次数,我们可以得到这个概率。
2.1. N-gram 语言模子中的平滑处理
在统计语言模子中,平滑操纵是至关重要的一个步骤,主要目的是办理以下几个关键题目:
- 零概率题目(Zero Frequency Problem):在基于计数的统计语言模子中,如果某个词或词序列在练习数据中没有出现过,那么其概率会被直接估计为零,这显然不符合实际情况,因为未观测到并不意味着不大概发生。平滑通太过配一些概率质量给这些零频率事件,确保所有大概的事件都有非零的概率。
- 数据稀疏性:自然语言具有极大的词汇量和布局多样性,即使是大型语料库也难以覆盖所有大概的词序列组合,导致很多长尾或罕见事件的计数非常少。平滑帮助模子更好地泛化到未见过的数据,减少因数据不足引起的过拟合。
- 模子稳固性:极端的计数(如极高或极低频词)大概会导致模子对练习数据中的噪声过度敏感。平滑通过减少这种极端情况的影响,进步模子的稳固性和鲁棒性。
- 促进泛化本事:通过减少对高频项的太过信任,同时给予低频项一定的机会,平滑有助于模子学习到更普遍的语言规律,进步在新数据上的体现。
常见的平滑技术包括但不限于:
- 加一平滑(Additive Smoothing, 拉普拉斯平滑):对所有计数加1,包括未出现的事件。
- 古德-图灵估计(Good-Turing Smoothing):基于实际观察到的频率分布来估计未见事件的概率。
- 绝对减值平滑(Absolute Discounting):从高频率计数中减去一个固定值,再举行重新分配。
- Kneser-Ney Smoothing:特别处理尾随事件,改进对未登录词的处理。
- 插值平滑(Interpolation):联合差别N-gram模子的结果,如Jelinek-Mercer平滑。
平滑不仅是统计语言模子构建中的一个必要步骤,也是提拔模子实用性和准确性的重要本领。
以2-gram为例,最大似然估计如下:
p ( w i ∣ w i − 1 ) = c ( w i − 1 w i ) c ( w i − 1 ) p(w_i|w_{i-1}) = \frac{c(w_{i-1} w_i)}{c(w_{i-1})} p(wi∣wi−1)=c(wi−1)c(wi−1wi)
以上其实就是简单的计数,然后我们就要在这里去做平滑,其实就是减少分母为0的出现。拉普拉斯平滑如下:
p add ( w i ∣ w i − 1 ) = c ( w i − 1 w i ) + δ c ( w i − 1 ) + δ ∣ V ∣ p_{\text{add}}(w_i|w_{i-1}) = \frac{c(w_{i-1} w_i) + \delta}{c(w_{i-1}) + \delta |V|} padd(wi∣wi−1)=c(wi−1)+δ∣V∣c(wi−1wi)+δ
一般地, δ \delta δ取1, ∣ V ∣ |V| ∣V∣体现词典库的大小。
基于二元模子的简单示例,包括数据预处理、构建模子、平滑处理以及基于模子举行推测。
- import collections
- class BigramLanguageModel:
- def __init__(self, sentences: list):
- """
- 初始化Bigram模型
- :param sentences: 训练语料,类型为字符串列表
- """
- self.sentences = sentences
- self.word_counts = collections.Counter()
- self.bigram_counts = collections.defaultdict(int)
- self.unique_words = set()
-
- # 预处理数据:分词并合并所有句子
- words = ' '.join(sentences).split()
- for w1, w2 in zip(words[:-1], words[1:]):
- self.word_counts[w1] += 1
- self.word_counts[w2] += 1
- self.bigram_counts[(w1, w2)] += 1
- self.unique_words.update([w1, w2])
-
- def laplace_smooth(self, delta: float = 1.0):
- """
- 拉普拉斯平滑
- :param delta: 平滑因子,默认为1.0
- """
- V = len(self.unique_words) # 词汇表大小
- self.model = {}
- for w1 in self.unique_words:
- total_count_w1 = self.word_counts[w1] + delta*V
- self.model[w1] = {}
- for w2 in self.unique_words:
- count_w1w2 = self.bigram_counts.get((w1, w2), 0) + delta
- self.model[w1][w2] = count_w1w2 / total_count_w1
-
- def generate_text(self, start_word: str, length: int = 10) -> str:
- """
- 生成文本
- :param start_word: 文本起始词
- :param length: 生成文本的长度
- :return: 生成的文本字符串
- """
- if start_word not in self.model:
- raise ValueError(f"Start word '{start_word}' not found in the model.")
- sentence = [start_word]
- current_word = start_word
-
- for _ in range(length):
- next_word_probs = self.model[current_word]
- next_word = max(next_word_probs, key=next_word_probs.get)
- sentence.append(next_word)
- current_word = next_word
-
- return ' '.join(sentence)
- # 示例使用
- corpus = [
- "ChatGPT is a powerful language model for multi task",
- "ChatGPT is a powerful language model",
- "ChatGPT can generate human-like text",
- "ChatGPT is trained using deep learning",
- ]
- model = BigramLanguageModel(corpus)
- model.laplace_smooth()
- generated_text = model.generate_text('ChatGPT', 5)
- print(generated_text) # ChatGPT is a powerful language model
复制代码 3. 语言模子评估
准确率作为语言模子的评估指标没有太多意义,语言是开放的序列推测题目,给定前面的文本,下一个词的大概性是非常多的,因此准确率值会非常低。
作为替换,应该在模子保留数据(held-out data)上,计算其对应的似然性(取平均值以消除长度影响)来评估语言模子。对数似然(log likelihood,LL)计算如下:
LL ( W ) = 1 n ∑ i = 1 n log P ( w i ∣ w 1 , ⋯ , w i − 1 ) \text{LL}(W) = \frac{1}{n} \sum_{i=1}^n \log P(w_i|w_1, \cdots, w_{i-1}) LL(W)=n1i=1∑nlogP(wi∣w1,⋯,wi−1)
保留数据:指在练习语言模子时,专门保留一部门数据不到场练习,用作评估模子性能的数据集。这确保了评估数据独立于练习数据,可以或许真实反映模子在新数据上的泛化本事。
困惑度(Perplexity,PPL)是评价语言模子质量的一个重要指标,可以或许更好地体现语言模子对句子流通性、语义连贯性的建模本事。困惑度是指数情势的平均负对数似然,计算如下:
PPL ( W ) = exp ( NLL ( W ) ) = exp ( − LL ( W ) ) \text{PPL}(W) = \exp (\text{NLL}(W) ) = \exp (- \text{LL}(W) ) PPL(W)=exp(NLL(W))=exp(−LL(W))
对数似然:给定语料库数据,对数似然衡量模子为这些数据赋予的概率的对数值。对数似然越高,模子对数据的建模本事越强。
负对数似然:由于对数似然值通常是个负值,取负号得到正值更利于分析比较。
平均负对数似然:将负对数似然值加总后除以数据长度(如词数),得到平均负对数似然。这样可以消除数据长度的影响,更公平地比较差别模子。
指数情势:将平均负对数似然值做指数运算,得到Perplexity值。由于似然自己很小,对数似然为负值,做指数能使结果值落在较合理的范围。
困惑度本质上反映了模子对数据的平均怀疑程度。值越低,说明模子对数据的建模质量越高、不确定性越小。通常良好模子的困惑度值在10-100之间,值越接近1越好。
4. 神经语言模子
神经语言模子(Neural Language Model, NLM)是一种使用神经网络来建模和生成自然语言序列的模子。相比传统的统计语言模子(如n-gram模子),神经语言模子具有以下几个主要特点:
- 神经网络的强大建模本事:神经网络可以或许自动从大量数据中学习复杂的特性模式,降服了传统模子需要人工计划特性的缺陷,无需人工特性工程。
- 分布式词向量体现:将每个词映射为一个连续的向量体现,可以或许自然地捕捉词与词之间的语义和句法关联。
- 长距离依赖建模:传统n-gram模子只考虑有限历史窗口,神经网络可以更好地学习长程语境依赖关系。
- 泛化本事更强:神经网络内部门布式体现有助于更好的泛化,可以很好地应对未见数据。
常见神经语言模子有基于RNN、LSTM、Transformer等差别网络架构,并广泛应用于语言建模、呆板翻译、对话系统等自然语言处理任务中。神经语言模子弥补了传统模子的不足,极大地推进了语言模子的发展,但也面临练习资源需求大、解释性较差等新的挑战。
5. 循环神经网络
前馈神经网络(Feedforward NNs)无法处理可变长度的输入,特性向量中的每个位置都有固定的语义。使用传统前馈神经网络作为语言模子时的主要缺陷和范围性如下:
- 前馈神经网络要求输入是固定长度的向量,输入向量中每个位置的元素都对应特定的语义寄义。但在自然语言处理任务中,输入序列(如句子或文本)的长度是可变的,差别的输入会有差别的序列长度。
- 前馈神经网络位置语义固定,对于差别长度的输入序列,每个位置的语义无法自动对应和调解。为了将变长序列映射到固定向量,不可避免需要截断或填充,这会导致信息损失或人为噪声的引入。
为了办理这个题目,出现了诸如循环神经网络(RNN)、长短期影象网络(LSTM)等可以或许更好地处理序列输入的神经网络架构。
5.1. Vanilla RNN
循环神经网络是一种可以接受变长输入和产生变长输出的网络架构类型,这与尺度的前馈神经网络形成对比。我们也可以考虑变长的输入,比如视频帧序列,并盼望在该视频的每一帧上都做出决策。
- 一对一:经典的前馈神经网络架构,其中有一个输入并期望一个输出。
- 一对多:可以将其视为图像形貌生成。有一个固定大小的图像作为输入,输出可以是长度可变的单词或句子。
- 多对一:用于感情分类。输入预期为一系列单词或乃至是段落。输出可以是具有连续值的回归输出,体现具有积极感情的大概性。
- 多对多:该模子非常适合呆板翻译。输入可以是变长的英语句子,输出将是相同句子的另一种语言版本,其长度也可变。末了一个多对多模子可以用于基于词级别的呆板翻译。将英语句子的每一个词输入神经网络,并期望立即得到输出。然而,由于词通常相相互关,因此需要将网络的隐藏状态从上一个词传递到下一个词。因此,我们需要循环神经网络来处理这种任务。
对于循环神经网络,我们可以在每个时间步应用递推公式来处理向量序列:
h t = f W ( h t − 1 , x t ) h_t = f_W(h_{t-1}, x_t) ht=fW(ht−1,xt)
对于简单的 Vanilla 循环神经网络来说,计算公式如下:
h t = tanh ( W h h h t − 1 + W x h x t + b h ) y t = W h y h t + b y \begin{aligned} h_t &= \tanh (W_{hh} h_{t-1} + W_{xh} x_t + b_h) \\ y_t &= W_{hy} h_t + b_y \end{aligned} htyt=tanh(Whhht−1+Wxhxt+bh)=Whyht+by
- import numpy as np
- np.random.seed(0)
- class RecurrentNetwork(object):
- """When we say W_hh, it means a weight matrix that accepts a hidden state and produce a new hidden state.
- Similarly, W_xh represents a weight matrix that accepts an input vector and produce a new hidden state. This
- notation can get messy as we get more variables later on with LSTM and I simplify the notation a little bit in
- LSTM notes.
- """
- def __init__(self):
- self.hidden_state = np.zeros((3, 3))
- self.W_hh = np.random.randn(3, 3)
- self.W_xh = np.random.randn(3, 3)
- self.W_hy = np.random.randn(3, 3)
- self.Bh = np.random.randn(3,)
- self.By = np.random.rand(3,)
- def forward_prop(self, x):
- # The order of which you do dot product is entirely up to you. The gradient updates will take care itself
- # as long as the matrix dimension matches up.
- self.hidden_state = np.tanh(np.dot(self.hidden_state, self.W_hh) + np.dot(x, self.W_xh) + self.Bh)
- return self.W_hy.dot(self.hidden_state) + self.By
-
-
- input_vector = np.ones((3, 3))
- rnn = RecurrentNetwork()
- # Notice that same input, but leads to different ouptut at every single time step.
- print(rnn.forward_prop(input_vector))
- print(rnn.forward_prop(input_vector))
- print(rnn.forward_prop(input_vector))
复制代码 5.2. LSTM
虽然 Vanilla RNN 在处理序列数据方面具有一定的本事,但它在长期依赖性建模方面存在一些挑战。长短期影象网络(Long Short Term Memory, LSTM )是一种特殊类型的 RNN,通过引入细胞状态(cell state)和门控机制来办理长期依赖性题目。以下是 LSTM 相对于 Vanilla RNN 的一些优点和特点:
- 处理长期依赖性题目:Vanilla RNN 在处理长序列时容易发生梯度消散或梯度爆炸的题目,导致无法有用地捕捉长期依赖关系。LSTM 通过细胞状态和门控机制,可以或许更好地捕捉和传递长期依赖性信息,使得网络可以或许更好地学习并记着与序列任务相干的远距离依赖关系。
- 门控机制:LSTM 引入了三个门控单元:输入门(input gate)、遗忘门(forget gate)和输出门(output gate)。这些门控单元通过使用得当的权重来控制信息的流动,决定哪些信息应该被保留、遗忘或输出。这种门控机制使得 LSTM 可以或许自适应地选择性地记着或遗忘相干信息,加强了网络的影象和泛化本事。
- 细胞状态:LSTM 引入了细胞状态(cell state),作为网络的影象单元。细胞状态在时间步长中一直传递,并且可以选择性地更新或打扫信息。这种机制使得 LSTM 可以或许更好地处理长期依赖性,同时减少了梯度流传中的题目。
- 灵活性和建模本事:LSTM 具有更强大的建模本事,可以或许处理更复杂和多样化的序列任务。通过得当的计划和调解,LSTM 可以学习到差别时间尺度上的模式,对输入序列中的关键事件和特性举行建模。
LSTM 相对于 Vanilla RNN 具有更强的影象和建模本事,可以或许更好地处理长期依赖性和序列任务。它通过引入细胞状态和门控机制,办理了 Vanilla RNN 在处理长序列时出现的梯度消散和梯度爆炸题目。
LSTM 计算过程如下:
f t = σ ( W h f h t − 1 + W x f x + b f ) i t = σ ( W h i h t − 1 + W x i x + b i ) o t = σ ( W h o h t − 1 + W x o x + b o ) c t = f t ⊙ c t 1 + i t ⊙ tanh ( W g x x + W g h h t − 1 + b g ) h t = o t ⊙ tanh ( c t ) \begin{aligned} f_t &= \sigma(W_{hf} h_{t-1} + W_{xf} x + b_f) \\ i_t &= \sigma(W_{hi} h_{t-1} + W_{xi} x + b_i) \\ o_t &= \sigma(W_{ho} h_{t-1} + W_{xo} x + b_o) \\ c_t &= f_t \odot c_{t_1} + i_t \odot \tanh(W_{gx} x + W_{gh} h_{t-1} + b_g) \\ h_t &= o_t \odot \tanh(c_t) \end{aligned} ftitotctht=σ(Whfht−1+Wxfx+bf)=σ(Whiht−1+Wxix+bi)=σ(Whoht−1+Wxox+bo)=ft⊙ct1+it⊙tanh(Wgxx+Wghht−1+bg)=ot⊙tanh(ct)
其中, f t f_t ft体现遗忘门,控制影象的遗忘程度; i t i_t it体现输入门,控制信息写入状态单元的程度; o t o_t ot体现输出门,控制状态单元的暴露程度; c t c_t ct体现状态单元,负责内部影象; h t h_t ht体现隐藏单元,负责对外暴露信息。
- import numpy as np
- np.random.seed(0)
- class LSTMNetwork(object):
- def __init__(self):
- self.hidden_state = np.zeros((3, 3))
- self.cell_state = np.zeros((3, 3))
- self.W_hh = np.random.randn(3, 3)
- self.W_xh = np.random.randn(3, 3)
- self.W_ch = np.random.randn(3, 3)
- self.W_fh = np.random.randn(3, 3)
- self.W_ih = np.random.randn(3, 3)
- self.W_oh = np.random.randn(3, 3)
- self.W_hy = np.random.randn(3, 3)
- self.Bh = np.random.randn(3,)
- self.By = np.random.randn(3,)
- def forward_prop(self, x):
- # Input gate
- i = sigmoid(np.dot(x, self.W_xh) + np.dot(self.hidden_state, self.W_hh) + np.dot(self.cell_state, self.W_ch))
- # Forget gate
- f = sigmoid(np.dot(x, self.W_xh) + np.dot(self.hidden_state, self.W_hh) + np.dot(self.cell_state, self.W_fh))
- # Output gate
- o = sigmoid(np.dot(x, self.W_xh) + np.dot(self.hidden_state, self.W_hh) + np.dot(self.cell_state, self.W_oh))
- # New cell state
- c_new = np.tanh(np.dot(x, self.W_xh) + np.dot(self.hidden_state, self.W_hh))
- self.cell_state = f * self.cell_state + i * c_new
- self.hidden_state = o * np.tanh(self.cell_state)
- return np.dot(self.hidden_state, self.W_hy) + self.By
- def sigmoid(x):
- return 1 / (1 + np.exp(-x))
- input_vector = np.ones((3, 3))
- lstm = LSTMNetwork()
- # Notice that the same input will lead to different outputs at each time step.
- print(lstm.forward_prop(input_vector))
- print(lstm.forward_prop(input_vector))
- print(lstm.forward_prop(input_vector))
复制代码
LSTM 可以一定程度上缓解梯度消散题目,但对于非常长的序列或复杂的任务,仍然存在一定的限制。除了梯度消散题目,LSTM 在处理序列时也存在性能上的限制。由于它们是渐渐处理序列的,无法充实使用并行计算的上风。对于长度为 n 的序列,LSTM 需要执行 O(n) 的非并行操纵来举行编码,导致速率较慢。
为了降服这些限制,提出了 Transformer 模子作为办理方案。Transformer 可以扩展到处理数千个单词的序列,并且可以或许并行计算。它引入了自留意力机制和位置编码,使得模子可以或许同时关注序列中差别位置的信息,并且可以或许以高效的方式对输入举行编码。这使得 Transformer 在处理长序列和大规模数据时具有上风,并且在呆板翻译和自然语言处理等范畴取得了显著的乐成。
Understanding LSTM Networks
Vanilla Recurrent Neural Network
LSTM Recurrent Neural Network
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |