本文来自社区投稿与征集,作者 段清华 DEAN,Google Developers Expert。
本文转自:https://zhuanlan.zhihu.com/p/293208563
本文会介绍TensorFlow 2.x在处理NLP任务中的一些工具和技巧,包括:
tf.keras.layers.experimental.preprocessing.TextVectorization
tf.strings
tf.data.experimental.bucket_by_sequence_length
BERT with strings
在完成 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))
# 输出:
#
那么 TextVectorization 是怎么实现的呢?其实我们自己也可以实现这个功能,这就要说到 TensorFlow 的字符串类型和相关的各种算子。
比如我们可以通过tf.strings.split
来分割字符串
x = [
'你 好 啊',
'I love you'
]
print(tf.strings.split(x))
# 输出:
#
词表怎么实现呢,我们就需要使用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)
处理图片模型的时候,经常需要将图谱缩放到一个固定的大小,不过对于 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 模型实际上是有 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)
如果你想深入自然语言处理(NLP)的相关知识,欢迎点击下方链接,进入 B 站 Google 中国,TensorFlow 官方频道。NLP 从入门到精通 6 讲课程已为你准备好,期待你的加入。
TensorFlow.google.cn