序列标注 Sequence Labeling
Structured Learning 4: Sequence Labeling on YouTube
序列标注是一种在NLP中很基础但是也很重要的任务。以POS词性标注为例,输入是一个句子,输出是每个单词的词性。
如果每个单词只有一种词性,我们可以简单的做一个hash table,读到单词直接去查找就可以了。然而问题就在于很多单词不只一种词性,我们需要根据上下文对其词性进行推测,这要求我们读到整个句子的信息。
HMM 隐式马尔科夫模型
HMM的假设
HMM的假设,认为你在说一句话的时候,是会现在脑中呈现一个词性序列,然后在词性序列的基础上构建对应语义的单词。
Steps
你的脑子里现在有一个马尔可夫链,表示了你构建一个句子,从一个词性\(u\)后面再接一个另一个词性\(v\)的概率\(p(v|u)\),起点是start,终点是end。整个句子的概率就是:
如果我们还知道了,给出每一个词性之后我选择某一个单词的概率\(p(v_j|w_i)\),那么我可以得到这个词性序列对应的一个确定单词的句子的概率:
且有:
我们称\(p(y)\)中的连乘为转移概率(transition probability),\(p(x|y)\)中的连乘为发射概率(emission probability)。这两个概率我们都可以从数据中统计出来。
回到标注任务
现在我们已经知道了\(x\),求序列\(y\),一个猜测就是我要的应该满足下面这个条件。
Viterbi Algorithm
利用上面的式子,我们可以通过枚举\(y\)序列选出最大值,然而这种做法相当低效。下面介绍一种名为Viterbi的算法在\(O(L|S|^2)\)的时间里解决这个问题。
通过对式子的观察,我们容易发现,一个位置选择词性之后的概率只和这个位置的单词和前一个词性有关,那我们直接简单的动态规划解决就完了,这个做法就叫Viterbi Algorithm。
HMM 的问题
HMM虽然简单,但是它却有一个问题:他会从训练数据中预测出训练数据本来没有的结果。注意这些结果不一定是正确的。
问题在于HMM认为发射概率和转移概率是无关的,分开训练,也就是用\(p( x_i |y_i)\)而不是\(p(x_i | y_i,y_{ i -1} )\)。如果你还没有明白这个问题,可以想一下这两个概率表达式的区别。
CRF
CRF全称为条件随机场。之前HMM产生的问题,CRF可以很好的解决。
CRF 的假设
令
CRF 与 HMM 相似性
在HMM里面,有
两边取对数:
train
我们找一个目标函数
由之前的推导,我们知道
因为要最大化这个函数,我们需要 Gradient Ascent。在\(w\)里面,有两部分需要求导:
CRF 之所以能够改善 HMM 存在的问题,就在于矩阵\(w\)是一个 learnable 的参数,它的 weights 并不是拘泥于数据中的概率的,它可以通过下面的 steps 一点点改善其中的概率。
从中观察到2点:
如果\((s,t)\)在训练数据样本中出现次数多,\(w_{s,t}\)就应该增加。
如果\((s,t)\)在其他数据中出现也很多,\(w_{s,t}\)就会减小。
这种改进方法相比HMM就好太多了,它可以自己调出需要的\(P_{s,t}(x,y)\)。
将梯度写出来:
判定
这一项也可以用 Viterbi Algorithm 进行计算。
Structed Perceptron
Structed Perceptron 有助于我们联系理解Structed Learning 和 CRF。与 CRF 相比,Structed Perceptron 的 train 是这样的:
而 CRF 是
如果不看 learning rate,它们的相似性是很大的。CRF 是对所有的\(y\)以不同权重减去他们的特征,而 Structed Perceptron 是只减去当前判断概率最大的那个\(\tilde y^n\)。
由于 Structed SVM 还不太明白,也没有办法和深度模型作结合,暂且搁置,回头再更 🐦咕咕咕~
Update 2020.4.30
最近做序列标注任务注意到pytorch官网有关于CRF的教程。看完之后觉得有些地方说的不是很清楚,特此记录。
-
log_sum_exp
如果不写出来还挺明白,刚看的时候越看越蒙,这里面还有最大值什么事,后来发现这是为了防止数值过大所以提前指数减去最大值。def log_sum_exp(vec): max_score = vec[0, argmax(vec)] max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1]) return max_score + \ torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))
-
_forward_alg
对给定的序列\(x\)计算\(\sum_{y' \in Y} e^{\phi(x, y')}\),我个人认为代码很容易看懂,但是不是很清楚为什么这样算是正确的。def _forward_alg(self, feats): # Do the forward algorithm to compute the partition function init_alphas = torch.full((1, self.tagset_size), -10000.) # START_TAG has all of the score init_alphas[0][self.tag_to_ix[START_TAG]] = 0. forward_var = init_alphas # Iterate through the sentence for feat in feats: alphas_t = [] for next_tag in range(self.tagset_size): emit_score = feat[next_tag].view( 1, -1).expand(1, self.tagset_size) trans_score = self.transitions[next_tag].view(1, -1) next_tag_var = forward_var + trans_score + emit_score alphas_t.append(log_sum_exp(next_tag_var).view(1)) forward_var = torch.cat(alphas_t).view(1, -1) terminal_val = forward_var + \ self.transitions[self.tag_to_ix[STOP_TAG]] alpha = log_sum_exp(terminal_val) return alpha
\(\log \sum_{y'} \exp (\phi(x, y'))=\log \sum_{y'} \exp (\sum_{i} \phi_{i}(x,y'))\),不难看出,任意两层之间是乘法关系,tag与tag之间是相加关系,所以先枚举每一层的关键就是保留指数求和,然后\(\exp\)后再tag间求和。可以用下面这个公式理解一下(但注意我们要求的和下面公式并非等价,因为下面公式没有考虑两个tag间的转移)
\[\prod _{i} \sum_{tag} \exp(\phi(x_i, tag)) = \sum_{y'} \exp( \sum_i \phi(x_i, y'_i)) \]正是因为要考虑到转移概率,所以我们不能像上式左侧那样一次求出来,而是要在
forward_var
中保存下来转移用。
添加时间测试后得到如下结果:
(tensor(16.6814), [0, 2, 0, 2, 0, 0, 2, 0, 2, 0, 2])
total time: 23.6868896484375
(tensor(25.6628), [0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2])
将代码稍作修改,用数据标签和viterbi算出的标签序列分数差作为损失函数,就得到了 Structed Perceptron。从效果上看要比 CRF 好一些。
def forward(self, sentence, label_tags=None):
lstm_feats = self._get_lstm_features(sentence)
if label_tags is not None:
label_score = self._score_sentence(lstm_feats, label_tags)
score, tag_seq = self._viterbi_decode(lstm_feats)
if label_tags is not None:
return score, tag_seq, label_score
return score, tag_seq
pred_score, tag_seq, label_score = model(sentence_in, label_tags=targets)
loss = -(label_score - pred_score)
输出:
(tensor(8.8818), [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], tensor([-3.5409]))
total time: 13.593068838119507
(tensor(2.0456), [0, 1, 1, 1, 2, 2, 2, 0, 1, 2, 2], tensor([2.0456]))
感觉虽然单组速度快,但是从epoch上讲,CRF好像收敛的更快。