概率图模型知识记录
基本概念
-
概率图模型:在概率模型的基础上,使用基于图的方法来表示概率分布,是一种通用化的不确定性知识表示和处理方法。节点表示随机变量,边表示依赖关系。
-
概率图模型分类:
有向图模型:一般是生成式模型
无向图模型:一般是判别式模型
生成模型与判别模型的本质区别:模型中观测序列x与状态序列y的决定关系。
【见《统计自然语言处理方法》105页】 -
图模型的三个基本问题:
(1)表示问题:对于一个概率模型,如何通过图结构来描述变量之间的依赖关
系.
(2)学习问题:图模型的学习包括图结构的学习和参数的学习.
(3) 推断问题:在已知部分变量时,计算其他变量的条件概率分布.
图来源:《神经网络与深度学习》用图模型只是换一种表示,真正重要的是其中的条件独立性假设,能够减少参数个数。
-
自然语言处理中概率图模型的演变过程
朴素贝叶斯与逻辑回归解决一般的分类问题;
HMM与线性的CRF解决“线式”序列问题,比如序列标注问题。
生成式有向图模型、通用的CRF解决一般性的图问题。
有向图模型
有向图模型(Directed Graphical model),也称为贝叶斯网络(Bayesian
Network),或信念网络(Belief Network,BN),是一类用有向图来描述随机向
量概率分布的模型.
- 条件独立:贝叶斯网络所依赖的一个核心概念,两节点没连接表示 两个随机变量在某些情况下条件独立,两个节点连接表示两个随机变量在任何情况下都不条件独立。
隐马尔可夫模型
- 模型抽象表示:
λ
=
(
A
,
B
,
π
)
\lambda = (A,B,\pi )
λ=(A,B,π)
(1)HMM由初始状态概率向量 π \pi π、状态转移概率矩阵A和观测概率矩阵B决定。
(2) π \pi π和A确定了隐藏的马尔卡夫链,生成不可观测的状态序列。
(3)B确定了如何从状态生成观测,与状态序列综合确定了如何产生观测序列。 - 基本假设:
(1)齐次马尔可夫性:任意时刻的状态只依赖上一时刻的状态。
P ( i t ∣ i t − 1 , . . . . . . . ) = P ( i t ∣ i t − 1 ) P(i_t|i_{t-1},.......) = P(i_t|i_{t-1}) P(it∣it−1,.......)=P(it∣it−1)【转移概率】
(2)观测独立性假设:任意时刻的观测只依赖当前时刻状态。
P ( o t ∣ i t . . . . . . . ) = P ( o t ∣ i t ) P(o_t|i_t.......) =P(o_t|i_t) P(ot∣it.......)=P(ot∣it) 【发射概率】 - 三个基本问题:
(1)概率计算: P ( O ∣ λ ) P(O|\lambda) P(O∣λ)
(2)学习问题:估计参数,使得$max P(O|\lambda) $
(3)推断问题:给定观测序列O, m a x P ( I ∣ O ) max P(I|O) maxP(I∣O)
概率计算:
分为前向算法与后向算法
学习问题
参数估计可以分为有监督、无监督。
有监督方法基于极大似然估计的思想,用频率估计概率。
{
(
O
1
,
I
1
)
,
(
O
2
,
I
2
)
.
.
.
.
.
.
.
.
.
.
.
}
\{(O_1,I_1),(O_2,I_2)...........\}
{(O1,I1),(O2,I2)...........} ——>
(
π
,
A
,
B
)
(\pi,A,B)
(π,A,B)
无监督方法基于EM算法,这边还没理解:意思是给了一堆观测序列,就可以得到参数?
【留个坑】
推断问题【解码】
维特比算法:动态规划的思想求概率最大的路径,每个路径对应一个状态序列。
输入:模型和观测序列(一个句子)
输出:状态序列(每个词的标签)
维护两个变量。
- 时刻t(第t个词)状态为
i
t
i_t
it时的最优路径。
递推计算:上一时刻的所有状态对应的最优路径转移到状态 i t i_t it,再由状态 i t i_t it发射 到观测 o t o_t ot所有路径中概率最大的那个。 - 时刻t状态
i
t
i_t
it所有单个路径中概率最大路径(上面计算得到的)的前一个节点(即第t-1个词)
目的:方便求得最后一个节点的最优路径后,进行回溯。
简单来说:计算时是前向逐步计算,直到最后一个节点(词)。每个节点(词)都有对应所有状态的单个最优路径值。到最后一个节点(词)求得最优路径后,开始回溯,返回这条路径上每个节点对应的状态。
图来源:隐马尔科夫模型HMM(四)维特比算法解码隐藏状态序列【都是统计学习方法书上的例子】
代码的实现也比较简单,自己简单罗列了个伪代码,看思路和别人一样就没写了。
具体可参考:
维特比算法:算法详解+示例讲解+Python实现
无向图
条件随机场
条件随机场的理论说明-《统计学习方法》
网上的帖子都很详细。
几个不错的学习链接
- 通俗易懂!BiLSTM上的CRF,用命名实体识别任务来解释CRF(一)
- 通俗易懂!BiLSTM上的CRF,用命名实体识别任务来解释CRF(二)
- 如何轻松愉快地理解条件随机场(CRF)?
- 手撕 BiLSTM-CRF
- 《统计学习方法》的电子版学习笔记
- Tensorflow中的源码
两个问题:
- 再谈理论的时候,都是说crf的条件概率是两种特征函数,(t 与 s)。实际实现中,通常说法是转移矩阵与发射得分向量,应该是对应t与s。但在书上,并没有对t于s的数量做要求(转移矩阵与发射向量应该是固定的)。可能理论描述是更General的情况?
- 在Bilstm+CRF中,发射得分是BiLSTM的输出(可以不用softmax,因为无向图得分只是一个权重,不是概率。用softmax反而对于降低那些比较小值得权重,不必要归一化)。那不用Bilstm,直接embedding+映射,作为Baseline。
- 在进行参数更新时,实际中损失函数比较好形容,就是实际路径得分/所有路径得分之和。然后CRF部分的参数是转移矩阵,利用梯度下降进行参数更新。但没有谈论具体求解梯度的细节,因为是通过工具完成的。这点在电子版学习笔记中有涉及。
代码部分
先看看tensorflow中的代码【tf2.0以后移到了tfa模块】:
Module: tfa.text
-
训练部分
损失函数:tfa.text.crf_log_likelihood
def crf_log_likelihood(
inputs: TensorLike,
tag_indices: TensorLike,
sequence_lengths: TensorLike,
transition_params: Optional[TensorLike] = None,
) -> tf.Tensor:
num_tags = inputs.shape[2]
# cast type to handle different types
tag_indices = tf.cast(tag_indices, dtype=tf.int32)
sequence_lengths = tf.cast(sequence_lengths, dtype=tf.int32)
if transition_params is None:
initializer = tf.keras.initializers.GlorotUniform()
transition_params = tf.Variable(
initializer([num_tags, num_tags]), "transitions"
)
sequence_scores = crf_sequence_score(
inputs, tag_indices, sequence_lengths, transition_params
)
log_norm = crf_log_norm(inputs, sequence_lengths, transition_params)
# Normalize the scores to get the log-likelihood per example.
log_likelihood = sequence_scores - log_norm
return log_likelihood, transition_params
简单来说,输入句子+标签+长度+转移矩阵(迭代的),输出似然值+新的转移矩阵
计算这个式子:log_likelihood = sequence_scores - log_norm
第一部分:sequence_scores的函数:
def crf_sequence_score(
inputs: TensorLike,
tag_indices: TensorLike,
sequence_lengths: TensorLike,
transition_params: TensorLike,
) -> tf.Tensor:
tag_indices = tf.cast(tag_indices, dtype=tf.int32)
sequence_lengths = tf.cast(sequence_lengths, dtype=tf.int32)
def _single_seq_fn():
batch_size = tf.shape(inputs, out_type=tf.int32)[0]
batch_inds = tf.reshape(tf.range(batch_size), [-1, 1])
indices = tf.concat([batch_inds, tf.zeros_like(batch_inds)], axis=1)
tag_inds = tf.gather_nd(tag_indices, indices)
tag_inds = tf.reshape(tag_inds, [-1, 1])
indices = tf.concat([indices, tag_inds], axis=1)
sequence_scores = tf.gather_nd(inputs, indices)
sequence_scores = tf.where(
tf.less_equal(sequence_lengths, 0),
tf.zeros_like(sequence_scores),
sequence_scores,
)
return sequence_scores
def _multi_seq_fn():
# Compute the scores of the given tag sequence.
unary_scores = crf_unary_score(tag_indices, sequence_lengths, inputs)
binary_scores = crf_binary_score(
tag_indices, sequence_lengths, transition_params
)
sequence_scores = unary_scores + binary_scores
return sequence_scores
return tf.cond(tf.equal(tf.shape(inputs)[1], 1), _single_seq_fn, _multi_seq_fn)
如果是序列长度为1,则是_single_seq_fn,否则就是_multi_seq_fn。
一般肯定一个句子长度不为1。所以看看_multi_seq_fn中:
sequence_scores = unary_scores + binary_scores
unary_scores:对应的是句子每个单词选择这个标签的得分。【之和】
binary_scores:对应的是有转移矩阵计算出标签转移得分。【之和】
sequence_scores:对应的是正确的路径得分
第二部分: log_norm的函数
只看序列长度大于1的:
def _multi_seq_fn():
"""Forward computation of alpha values."""
rest_of_input = tf.slice(inputs, [0, 1, 0], [-1, -1, -1])
# Compute the alpha values in the forward algorithm in order to get the
# partition function.
alphas = crf_forward(
rest_of_input, first_input, transition_params, sequence_lengths
)
log_norm = tf.reduce_logsumexp(alphas, [1])
# Mask `log_norm` of the sequences with length <= zero.
log_norm = tf.where(
tf.less_equal(sequence_lengths, 0), tf.zeros_like(log_norm), log_norm
)
return log_norm
序列长度正常的话,就是tf.reduce_logsumexp(alphas, [1])。
再来看看计算alphas的函数crf_forward。
alphas就是根据转移矩阵,前向计算出所有路径的得分。(是一个N*1的向量)N是所有路径的数目。
计算方法:代码中给的参考链接是:The Forward-Backward Algorithm
应该是对应通俗易懂,理解BiLSTM+CRF中所阐述的。
- 解码部分:
维特比算法。
def viterbi_decode(score: TensorLike, transition_params: TensorLike) -> tf.Tensor:
trellis = np.zeros_like(score)
backpointers = np.zeros_like(score, dtype=np.int32)
trellis[0] = score[0]
for t in range(1, score.shape[0]):
v = np.expand_dims(trellis[t - 1], 1) + transition_params
trellis[t] = score[t] + np.max(v, 0)
backpointers[t] = np.argmax(v, 0)
viterbi = [np.argmax(trellis[-1])]
for bp in reversed(backpointers[1:]):
viterbi.append(bp[viterbi[-1]])
viterbi.reverse()
viterbi_score = np.max(trellis[-1])
return viterbi, viterbi_score
输入的是发射得分以及转移矩阵,输出维特比解码的路径以及得分。
动态规划的写法是简洁的,思想是优雅的。
主要搞清楚状态转移方程:第t步每个状态的最优是由上一步每个状态最优转移过来的。然后记录下节点。
总结来说:
- 训练部分:
- 对数似然–log_likelihood = sequence_scores - log_norm
- sequence_scores包括了发射得分,以及转移得分。
- log_norm是根据转移矩阵,利用前向-后向算法计算出所有路径的得分之和。
- 利用梯度下降法更新参数。(见统计学习方法的笔记)
- 预测部分:
- 根据得到的发射得分以及转移矩阵
- 利用维特比算法解码,得到最优得分以及最优路径。