NLP 在 TensorFlow 2.x 中的最佳实战

本文来自社区投稿与征集,作者 段清华 DEAN,Google Developers Expert。
本文转自: NLP在TensorFlow 2.x中的最佳实战 - 知乎

本文会介绍TensorFlow 2.x在处理NLP任务中的一些工具和技巧,包括:

  • tf.keras.layers.experimental.preprocessing.TextVectorization
  • tf.strings
  • tf.data.experimental.bucket_by_sequence_length
  • BERT with strings

TextVectorization

在完成 NLP 任务的时候,经常需要把文字(一般是字符串),转换为具体的词向量(或字向量)。

或者说把文字转换为对应的词嵌入 (Word Embedding/Token Embedding)。

一般来说我们可能会这么做:制作一个词表,然后写程序把对应的词(字)映射到整数序号,然后就可以使用如tf.keras.layers.Embedding层,把这个整数映射到词嵌入。

但是这种做法有一个问题,就是你需要一个额外的程序,和一份此表,才能把文字(字符串)转换为具体的整数序号。

因为需要额外的程序,比如需要把一个 TensorFlow 保存后的模型传给别人,也同时需要传输给别人这个程序和词表,显然麻烦的多。

有没有一种不需要额外程序和采标的方法呢?TensorFlow 新加入的特性TextVectorization就是这样的功能。

TextVectorization默认输入以空格为分割的字符串,同时它和其他 TensorFlow/Keras 的层不同,它需要先进行学习,具体的代码如下:

x = [
    '你 好 啊',
    'I love you'
]
# 构建层
text_vector = tf.keras.layers.experimental.preprocessing.TextVectorization()
# 学习词表
text_vector.adapt(x)
# 我们可以通过这种方式获取词表(一个list)
print(text_vector.get_vocabulary())

# 输出:
# ['', '[UNK]', '好', '啊', '你', 'you', 'love', 'i']

# 可以看出结果已经
print(text_vector(x))

# 输出:
# tf.Tensor(
# [[4 2 3]
#  [7 6 5]], shape=(2, 3), dtype=int64)

然后就可以把 text_vector 加入一个普通的 TensorFlow 模型

model = tf.keras.Sequential([
    text_vector,
    tf.keras.layers.Embedding(
        len(text_vector.get_vocabulary()),
        32
    ),
    tf.keras.layers.Dense(2)
])

print(model(x))

# 输出:
# <tf.Tensor: shape=(2, 3, 2), dtype=float32, numpy=
# array([[[-0.01258635, -0.01506722],
#         [-0.02729277, -0.04474692],
#         [ 0.02955768,  0.00149873]],
#        [[ 0.01346388,  0.01626211],
#         [-0.03160518,  0.07346839],
#         [-0.01061894, -0.0035725 ]]], dtype=float32)>

tf.strings 是什么

那么 TextVectorization 是怎么实现的呢?其实我们自己也可以实现这个功能,这就要说到 TensorFlow 的字符串类型和相关的各种算子。

比如我们可以通过tf.strings.split来分割字符串

x = [
    '你 好 啊',
    'I love you'
]
print(tf.strings.split(x))

# 输出:
# <tf.RaggedTensor [[b'\xe4\xbd\xa0', b'\xe5\xa5\xbd', b'\xe5\x95\x8a'], [b'I', b'love', b'you']]>

词表怎么实现呢,我们就需要使用tf.lookup.StaticHashTable

keys_tensor = tf.constant(['你', '好', '啊'])
vals_tensor = tf.constant([1, 2, 3])

table = tf.lookup.StaticHashTable(
    tf.lookup.KeyValueTensorInitializer(keys_tensor, vals_tensor), -1)
print(table.lookup(tf.constant(['你', '好'])))

# 输出:
# tf.Tensor([1 2], shape=(2,), dtype=int32)

数据对齐: bucket_by_sequence_length

处理图片模型的时候,经常需要将图谱缩放到一个固定的大小,不过对于 NLP 任务来说,句子长度可是不同的,这虽然也可以通过增加 padding 的方式,即插入空字符的方式对齐。

但是实际上这样的处理是有一定的问题的,就是效率损失。

这种方式虽然能满足算法,但是实际上无论是 LSTM/Transform,其实效率都和句子长度有关。

例如有 4 个句子,两个是长度 2,两个是长度 100,假设分成两个批次 (batch),第一个批次是两个长度 2 的句子,第二批次是两个长度 100 的句子,那算法就只需要计算 (2 + 100) 的算力。

但是如果四个句子,把一个长度 2 的句子和一个长度 100 的句子凑一起,就需要在每个批次的长度 2 句子后面插入 98 个空字符,算法需要的算力就是 (100 + 100)的算力。

在 TensorFlow 中,可以使用tf.data.Dataset.experimental.bucket_by_sequence_length自动对齐输入数据。

基于BERT的举例:更简易的BERT

BERT 模型实际上是有 3 个输入的:token,mask,type

token 是经过分词的字符串,转换为的整数序号。

mask 是输入长度的遮盖,是在一个 batch 有中不同长度的句子时的情况。

type 是 0 或 1,是 bert 训练目标中的第二个 NSP 任务相对的 type embedding 所需的。

不过对于大多数情况,其实 BERT 只需要一个输入,就是字符串。

因为对于 BERT 很多单句/单文档模型的情况,type 只需要单一的 0 就可以了。

而 mask 也可以通过字符串本身计算出来,例如是否为空字符串。

这个时候我们就可以使用以上提到的字符串方法,让 BERT 模型直接输入字符串,配合 TensorFlow Hub,这就可以方便很多模型的计算。

当然对于 BERT 的分词器,就很难简单的直接用上面提到的 TextVectorization 了,这里需要配合 TensorFlow Text。

最简单算法可以简化如下:

# $ pip install tensorflow tensorflow-text tensorflow-hub
import tensorflow as tf
import tensorflow_text
import tensorflow_hub as hub
tokenizer = hub.load(
    'https://code.aliyun.com/qhduan/bert_v4/raw/500019068f2c715d4b344c3e2216cef280a7f800/bert_tokenizer_chinese.tar.gz'
)
albert = hub.load(
    'https://code.aliyun.com/qhduan/bert_v4/raw/500019068f2c715d4b344c3e2216cef280a7f800/albert_tiny.tar.gz'
)
out = albert(tokenizer(['你好']))

assert out['sequence_output'].shape == (1, 2, 312)
assert out['pooled_output'].shape == (1, 312)

中文:TensorFlow 公众号