剛學習lstm+crf時,就閱讀過crf層的源碼,發現時間一久,就忘了。這次準備重新閱讀一下,順便做個筆記。主要的目的是深入理解代碼細節,提高自己編寫模型的能力。本文假定大家對lstm+crf的基本原理基本清楚。如果還不是很清楚,建議看看這篇論文:Bidirectional LSTM-CRF Models for Sequence Tagging公眾號後臺回復 crf (全部小寫) 即可獲得論文。
crf源碼的版本網絡上crf的實現版本很多,我選擇的是這個:https://github.com/tensorflow/addons/blob/v0.11.2/tensorflow_addons/text/crf.py
概覽首先來看看源碼的大概結構:
有1個類,11個方法。我們從crf_sequence_score這個方法開始。
閱讀代碼當然是非常抽象,我們最好在腦子裡構建一個例子,閱讀的過程中,想像這個例子在被計算。這樣就會好理解很多。比如這個:
crf_sequence_score先看籤名和注釋:
inputs就是我們從bilstm層得到的輸出。tag_indices 就是我們要計算的一條標籤序列。根據前面的例子,可以想像成(B-ORG O B-MISC O)sequence_lengths 就是句子的真實長度,主要用來計算masktransition_params 就是crf的核心結構,狀態轉移矩陣
該方法返回tag_indices這個標籤序列的得分。
這裡面內部又定義了兩個方法。看注釋可以了解,當句子長度是1時,就沒必要計算轉移分數。直接從inputs中取出對應tag的值即可。也就是這個方法,相應解釋也寫在代碼注釋中了:
def _single_seq_fn():
'''
因為要從inputs中取出相應的元素,所以
使用 tf.gather_nd
難點在於構建indices,這就是本方法的主體。
indexes一共三維,構建一維,拼接一維。
'''
batch_size = tf.shape(inputs, out_type=tf.int32)[0]
# 構建第一維,即batch size維度
batch_inds = tf.reshape(tf.range(batch_size), [-1, 1])
# 拼接第二維,因為句子長度是1,所以第二維全是0
indices = tf.concat([batch_inds, tf.zeros_like(batch_inds)], axis=1)
#從tag_indices中取出第三維的值
tag_inds = tf.gather_nd(tag_indices, indices)
tag_inds = tf.reshape(tag_inds, [-1, 1])
# 拼接第三維
indices = tf.concat([indices, tag_inds], axis=1)
# 根據拼接好的indices從inputs中取出相應的元素
sequence_scores = tf.gather_nd(inputs, indices)
# 如果句子長度為0,則相應的score為0
sequence_scores = tf.where(
tf.less_equal(sequence_lengths, 0),
tf.zeros_like(sequence_scores),
sequence_scores,
)
return sequence_scores當句子長度大於1時,就必須計算轉移分數,這才是正常的狀態。方法如下:
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可以看到,這裡面主要由兩個方法組成:
crf_binary_score我們一個一個看。
crf_unary_score這個方法,是從inputs中,取出tag_indices對應位置的分數,然後把每個句子中的分數相加。
def crf_unary_score(
tag_indices: TensorLike, sequence_lengths: TensorLike, inputs: TensorLike
) -> tf.Tensor:
"""Computes the unary scores of tag sequences.
Args:
tag_indices: A [batch_size, max_seq_len] matrix of tag indices.
sequence_lengths: A [batch_size] vector of true sequence lengths.
inputs: A [batch_size, max_seq_len, num_tags] tensor of unary potentials.
Returns:
unary_scores: A [batch_size] vector of unary scores.
"""
tag_indices = tf.cast(tag_indices, dtype=tf.int32)
sequence_lengths = tf.cast(sequence_lengths, dtype=tf.int32)
batch_size = tf.shape(inputs)[0]
max_seq_len = tf.shape(inputs)[1]
num_tags = tf.shape(inputs)[2]
# 將整個輸入拉平,變成一維數組
flattened_inputs = tf.reshape(inputs, [-1])
# 確定鋪平後batch中每個句子的起點
offsets = tf.expand_dims(tf.range(batch_size) * max_seq_len * num_tags, 1)
# 確定鋪平後,batch中每個句子中的每個字的起點
offsets += tf.expand_dims(tf.range(max_seq_len) * num_tags, 0)
# Use int32 or int64 based on tag_indices' dtype.
if tag_indices.dtype == tf.int64:
offsets = tf.cast(offsets, tf.int64)
# 將tag_indices轉化成鋪平後的indices
flattened_tag_indices = tf.reshape(offsets + tag_indices, [-1])
# 獲取單獨的值,並重新reshape
unary_scores = tf.reshape(
tf.gather(flattened_inputs, flattened_tag_indices), [
batch_size, max_seq_len]
)
# 根據句子長度,生成mask
masks = tf.sequence_mask(
sequence_lengths, maxlen=tf.shape(tag_indices)[1], dtype=tf.float32
)
# 將每個字符的分數相加,作為標籤序列的unary_scores
unary_scores = tf.reduce_sum(unary_scores * masks, 1)
return unary_scores如果你對上面的reshape有所陌生,提供如下例子供參考:
好了,今天先到這裡吧。下次繼續,再長你也不會看了。