深度学习处理文本(4)

二元语法的二进制编码

当然,舍弃词序的做法是非常简化的,因为即使是很简单的概念也需要用多个单词来表达:​“United States”​(美利坚合众国)所传达的概念与“states”​(州)和“united”​(联合的)这两个单词各自的含义完全不同。因此,你通常会使用N元语法(最常用的是二元语法)而不是单个单词,将局部顺序信息重新注入词袋表示中。利用二元语法,前面的句子变成如下所示。

{
    
    "the", "the cat", "cat", "cat sat", "sat",
 "sat on", "on", "on the", "the mat", "mat"}

你可以设置TextVectorization层返回任意N元语法,如二元语法、三元语法等。只需传入参数ngrams=N,如代码清单11-7所示。

代码清单11-7 设置TextVectorization层返回二元语法

text_vectorization = TextVectorization(
    ngrams=2,
    max_tokens=20000,
    output_mode="multi_hot",
)

我们在这个二进制编码的二元语法袋上训练模型,并测试模型性能,如代码清单11-8所示。

代码清单11-8 对二元语法二进制模型进行训练和测试

text_vectorization.adapt(text_only_train_ds)
binary_2gram_train_ds = train_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
binary_2gram_val_ds = val_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
binary_2gram_test_ds = test_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)

model = get_model()
model.summary()
callbacks = [
    keras.callbacks.ModelCheckpoint("binary_2gram.keras",
                                    save_best_only=True)
]
model.fit(binary_2gram_train_ds.cache(),
          validation_data=binary_2gram_val_ds.cache(),
          epochs=10,
          callbacks=callbacks)
model = keras.models.load_model("binary_2gram.keras")
print(f"Test acc: {
      
      model.evaluate(binary_2gram_test_ds)[1]:.3f}")

现在测试精度达到了90.4%,有很大改进!事实证明,局部顺序非常重要。

二元语法的TF-IDF编码

你还可以为这种表示添加更多的信息,方法就是计算每个单词或每个N元语法的出现次数,也就是说,统计文本的词频直方图,如下所示。

{
    
    "the": 2, "the cat": 1, "cat": 1, "cat sat": 1, "sat": 1,
 "sat on": 1, "on": 1, "on the": 1, "the mat: 1", "mat": 1}

如果你做的是文本分类,那么知道一个单词在某个样本中的出现次数是很重要的:任何足够长的影评,不管是哪种情绪,都可能包含“可怕”这个词,但如果一篇影评包含许多个“可怕”​,那么它很可能是负面的。你可以用TextVectorization层来计算二元语法的出现次数,如代码清单11-9所示。

代码清单11-9 设置TextVectorization层返回词元出现次数

text_vectorization = TextVectorization(
    ngrams=2,
    max_tokens=20000,
    output_mode="count"
)

当然,无论文本的内容是什么,有些单词一定比其他单词出现得更频繁。​“the”​“a”​“is”​“are”等单词总是会在词频直方图中占据主导地位,远超其他单词,尽管它们对分类而言是没有用处的特征。我们怎么解决这个问题呢?你可能已经猜到了:利用规范化。我们可以将单词计数减去均值并除以方差,对其进行规范化(均值和方差是对整个训练数据集进行计算得到的)​。这样做是有道理的。但是,大多数向量化句子几乎完全由0组成(前面的例子包含12个非零元素和19 988个零元素)​,这种性质叫作稀疏性。这是一种很好的性质,因为它极大降低了计算负荷,还降低了过拟合的风险。如果我们将每个特征都减去均值,那么就会破坏稀疏性。因此,无论使用哪种规范化方法,都应该只用除法。那用什么作分母呢?最佳实践是一种叫作TF-IDF规范化(TF-IDF normalization)的方法。TF-IDF的含义是“词频–逆文档频次”​。TF-IDF非常常用,它内置于TextVectorization层中。要使用TF-IDF,只需将output_mode参数的值切换为"tf_idf",如代码清单11-10所示。

代码清单11-1 设置TextVectorization层返回TF-IDF加权输出

text_vectorization = TextVectorization(
    ngrams=2,
    max_tokens=20000,
    output_mode="tf_idf",
)

理解TF-IDF规范化

某个词在一个文档中出现的次数越多,它对理解文档的内容就越重要。同时,某个词在数据集所有文档中的出现频次也很重要:如果一个词几乎出现在每个文档中(比如“the”或“a”​)​,那么这个词就不是特别有信息量,而仅在一小部分文本中出现的词(比如“Herzog”​)则是非常独特的,因此也非常重要。TF-IDF指标融合了这两种思想。它将某个词的“词频”除以“文档频次”​,前者是该词在当前文档中的出现次数,后者是该词在整个数据集中的出现频次。TF-IDF的计算方法如下。

def tfidf(term, document, dataset):
    term_freq = document.count(term)
    doc_freq = math.log(sum(doc.count(term) for doc in dataset) + 1)
    return term_freq / doc_freq

我们用这种设置训练一个新模型,如代码清单11-11所示。

代码清单11-11 对TF-IDF二元语法模型进行训练和测试

text_vectorization.adapt(text_only_train_ds)

tfidf_2gram_train_ds = train_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
tfidf_2gram_val_ds = val_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
tfidf_2gram_test_ds = test_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)

model = get_model()
model.summary()
callbacks = [
    keras.callbacks.ModelCheckpoint("tfidf_2gram.keras",
                                    save_best_only=True)
]
model.fit(tfidf_2gram_train_ds.cache(),
          validation_data=tfidf_2gram_val_ds.cache(),
          epochs=10,
          callbacks=callbacks)
model = keras.models.load_model("tfidf_2gram.keras")
print(f"Test acc: {
      
      model.evaluate(tfidf_2gram_test_ds)[1]:.3f}")

在IMDB分类任务上的测试精度达到了89.8%,这种方法对本例似乎不是特别有用。然而,对于许多文本分类数据集而言,与普通二进制编码相比,使用TF-IDF通常可以将精度提高一个百分点。