In [0]:
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
学习目标:
在此练习中,我们将探讨稀疏数据,并使用影评文本数据(来自 ACL 2011 IMDB 数据集)进行嵌入。这些数据已被处理成 tf.Example
格式。
我们导入依赖项并下载训练数据和测试数据。tf.keras
中包含一个文件下载和缓存工具,我们可以用它来检索数据集。
In [0]:
from __future__ import print_function
import collections
import io
import math
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import tensorflow as tf
from IPython import display
from sklearn import metrics
tf.logging.set_verbosity(tf.logging.ERROR)
train_url = 'https://download.mlcc.google.cn/mledu-datasets/sparse-data-embedding/train.tfrecord'
train_path = tf.keras.utils.get_file(train_url.split('/')[-1], train_url)
test_url = 'https://download.mlcc.google.cn/mledu-datasets/sparse-data-embedding/test.tfrecord'
test_path = tf.keras.utils.get_file(test_url.split('/')[-1], test_url)
我们根据这些数据训练一个情感分析模型,以预测某条评价总体上是好评(标签为 1)还是差评(标签为 0)。
为此,我们会使用词汇表(即我们预计将在数据中看到的每个术语的列表),将字符串值 terms
转换为特征矢量。在本练习中,我们创建了侧重于一组有限术语的小型词汇表。其中的大多数术语明确表示是好评或差评,但有些只是因为有趣而被添加进来。
词汇表中的每个术语都与特征矢量中的一个坐标相对应。为了将样本的字符串值 terms
转换为这种矢量格式,我们按以下方式处理字符串值:如果该术语没有出现在样本字符串中,则坐标值将为 0;如果出现在样本字符串中,则值为 1。未出现在该词汇表中的样本中的术语将被弃用。
注意:我们当然可以使用更大的词汇表,而且有创建此类词汇表的专用工具。此外,我们可以添加少量的 OOV(未收录词汇)分桶,您可以在其中对词汇表中未包含的术语进行哈希处理,而不仅仅是弃用这些术语。我们还可以使用特征哈希法对每个术语进行哈希处理,而不是创建显式词汇表。这在实践中很有效,但却不具备可解读性(这对本练习非常实用)。如需了解处理此类词汇表的工具,请参阅 tf.feature_column 模块。
首先,我们来配置输入管道,以将数据导入 TensorFlow 模型中。我们可以使用以下函数来解析训练数据和测试数据(格式为 TFRecord),然后返回一个由特征和相应标签组成的字典。
In [0]:
def _parse_function(record):
"""Extracts features and labels.
Args:
record: File path to a TFRecord file
Returns:
A `tuple` `(labels, features)`:
features: A dict of tensors representing the features
labels: A tensor with the corresponding labels.
"""
features = {
"terms": tf.VarLenFeature(dtype=tf.string), # terms are strings of varying lengths
"labels": tf.FixedLenFeature(shape=[1], dtype=tf.float32) # labels are 0 or 1
}
parsed_features = tf.parse_single_example(record, features)
terms = parsed_features['terms'].values
labels = parsed_features['labels']
return {'terms':terms}, labels
为了确认函数是否能正常运行,我们为训练数据构建一个 TFRecordDataset
,并使用上述函数将数据映射到特征和标签。
In [0]:
# Create the Dataset object.
ds = tf.data.TFRecordDataset(train_path)
# Map features and labels with the parse function.
ds = ds.map(_parse_function)
ds
运行以下单元,以从训练数据集中获取第一个样本。
In [0]:
n = ds.make_one_shot_iterator().get_next()
sess = tf.Session()
sess.run(n)
现在,我们构建一个正式的输入函数,可以将其传递给 TensorFlow Estimator 对象的 train()
方法。
In [0]:
# Create an input_fn that parses the tf.Examples from the given files,
# and split them into features and targets.
def _input_fn(input_filenames, num_epochs=None, shuffle=True):
# Same code as above; create a dataset and map features and labels.
ds = tf.data.TFRecordDataset(input_filenames)
ds = ds.map(_parse_function)
if shuffle:
ds = ds.shuffle(10000)
# Our feature data is variable-length, so we pad and batch
# each field of the dataset structure to whatever size is necessary.
ds = ds.padded_batch(25, ds.output_shapes)
ds = ds.repeat(num_epochs)
# Return the next batch of data.
features, labels = ds.make_one_shot_iterator().get_next()
return features, labels
对于我们的第一个模型,我们将使用 50 个信息性术语来构建 LinearClassifier
模型;始终从简单入手!
以下代码将为我们的术语构建特征列。categorical_column_with_vocabulary_list
函数可使用“字符串-特征矢量”映射来创建特征列。
In [0]:
# 50 informative terms that compose our model vocabulary.
informative_terms = ("bad", "great", "best", "worst", "fun", "beautiful",
"excellent", "poor", "boring", "awful", "terrible",
"definitely", "perfect", "liked", "worse", "waste",
"entertaining", "loved", "unfortunately", "amazing",
"enjoyed", "favorite", "horrible", "brilliant", "highly",
"simple", "annoying", "today", "hilarious", "enjoyable",
"dull", "fantastic", "poorly", "fails", "disappointing",
"disappointment", "not", "him", "her", "good", "time",
"?", ".", "!", "movie", "film", "action", "comedy",
"drama", "family")
terms_feature_column = tf.feature_column.categorical_column_with_vocabulary_list(key="terms", vocabulary_list=informative_terms)
接下来,我们将构建 LinearClassifier
,在训练集中训练该模型,并在评估集中对其进行评估。阅读上述代码后,运行该模型以了解其效果。
In [0]:
my_optimizer = tf.train.AdagradOptimizer(learning_rate=0.1)
my_optimizer = tf.contrib.estimator.clip_gradients_by_norm(my_optimizer, 5.0)
feature_columns = [ terms_feature_column ]
classifier = tf.estimator.LinearClassifier(
feature_columns=feature_columns,
optimizer=my_optimizer,
)
classifier.train(
input_fn=lambda: _input_fn([train_path]),
steps=1000)
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([train_path]),
steps=1000)
print("Training set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([test_path]),
steps=1000)
print("Test set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
上述模型是一个线性模型,效果非常好。但是,我们可以使用 DNN 模型实现更好的效果吗?
我们将 LinearClassifier
切换为 DNNClassifier
。运行以下单元,看看您的模型效果如何。
In [0]:
##################### Here's what we changed ##################################
classifier = tf.estimator.DNNClassifier( #
feature_columns=[tf.feature_column.indicator_column(terms_feature_column)], #
hidden_units=[20,20], #
optimizer=my_optimizer, #
) #
###############################################################################
try:
classifier.train(
input_fn=lambda: _input_fn([train_path]),
steps=1000)
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([train_path]),
steps=1)
print("Training set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([test_path]),
steps=1)
print("Test set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
except ValueError as err:
print(err)
注意:从计算方面而言,embedding_column 通常是用于在稀疏数据中训练模型最有效的选项。在此练习末尾的可选部分,我们将更深入地讨论使用 embedding_column
与 indicator_column
之间的实现差异,以及如何在这两者之间做出权衡。
在下面的代码中,执行以下操作:
embedding_column
来为模型定义特征列(如需详细了解 embedding_column
的函数签名,请参阅相关 TF 文档)。DNNClassifier
:gradient_clip_norm 值为 5.0
注意:在实践中,我们可能会将数据投射到 2 维以上(比如 50 或 100)的空间中。但就目前而言,2 维是比较容易可视化的维数。
In [0]:
# Here's a example code snippet you might use to define the feature columns:
terms_embedding_column = tf.feature_column.embedding_column(terms_feature_column, dimension=2)
feature_columns = [ terms_embedding_column ]
In [0]:
########################## YOUR CODE HERE ######################################
terms_embedding_column = # Define the embedding column
feature_columns = # Define the feature columns
classifier = # Define the DNNClassifier
################################################################################
classifier.train(
input_fn=lambda: _input_fn([train_path]),
steps=1000)
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([train_path]),
steps=1000)
print("Training set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([test_path]),
steps=1000)
print("Test set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
In [0]:
########################## SOLUTION CODE ########################################
terms_embedding_column = tf.feature_column.embedding_column(terms_feature_column, dimension=2)
feature_columns = [ terms_embedding_column ]
my_optimizer = tf.train.AdagradOptimizer(learning_rate=0.1)
my_optimizer = tf.contrib.estimator.clip_gradients_by_norm(my_optimizer, 5.0)
classifier = tf.estimator.DNNClassifier(
feature_columns=feature_columns,
hidden_units=[20,20],
optimizer=my_optimizer
)
#################################################################################
classifier.train(
input_fn=lambda: _input_fn([train_path]),
steps=1000)
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([train_path]),
steps=1000)
print("Training set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([test_path]),
steps=1000)
print("Test set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
In [0]:
classifier.get_variable_names()
好的,我们可以看到这里有一个嵌入层:'dnn/input_from_feature_columns/input_layer/terms_embedding/...'
。(顺便说一下,有趣的是,该层可以与模型的其他层一起训练,就像所有隐藏层一样。)
嵌入层的形状是否正确?请运行以下代码来查明。
注意:在我们的示例中,嵌入是一个矩阵,可让我们将一个 50 维矢量投射到 2 维空间。
In [0]:
classifier.get_variable_value('dnn/input_from_feature_columns/input_layer/terms_embedding/embedding_weights').shape
花些时间来手动检查各个层及其形状,以确保一切都按照您预期的方式互相连接。
In [0]:
import numpy as np
import matplotlib.pyplot as plt
embedding_matrix = classifier.get_variable_value('dnn/input_from_feature_columns/input_layer/terms_embedding/embedding_weights')
for term_index in range(len(informative_terms)):
# Create a one-hot encoding for our term. It has 0s everywhere, except for
# a single 1 in the coordinate that corresponds to that term.
term_vector = np.zeros(len(informative_terms))
term_vector[term_index] = 1
# We'll now project that one-hot vector into the embedding space.
embedding_xy = np.matmul(term_vector, embedding_matrix)
plt.text(embedding_xy[0],
embedding_xy[1],
informative_terms[term_index])
# Do a little setup to make sure the plot displays nicely.
plt.rcParams["figure.figsize"] = (15, 15)
plt.xlim(1.2 * embedding_matrix.min(), 1.2 * embedding_matrix.max())
plt.ylim(1.2 * embedding_matrix.min(), 1.2 * embedding_matrix.max())
plt.show()
看看您能否优化该模型以改进其效果。您可以尝试以下几种做法:
informative_terms
中添加其他术语。此数据集有一个完整的词汇表文件,其中包含 30716 个术语,您可以在以下位置找到该文件:https://download.mlcc.google.cn/mledu-datasets/sparse-data-embedding/terms.txt 您可以从该词汇表文件中挑选出其他术语,也可以通过 categorical_column_with_vocabulary_file
特征列使用整个词汇表文件。
In [0]:
# Download the vocabulary file.
terms_url = 'https://download.mlcc.google.cn/mledu-datasets/sparse-data-embedding/terms.txt'
terms_path = tf.keras.utils.get_file(terms_url.split('/')[-1], terms_url)
In [0]:
# Create a feature column from "terms", using a full vocabulary file.
informative_terms = None
with io.open(terms_path, 'r', encoding='utf8') as f:
# Convert it to a set first to remove duplicates.
informative_terms = list(set(f.read().split()))
terms_feature_column = tf.feature_column.categorical_column_with_vocabulary_list(key="terms",
vocabulary_list=informative_terms)
terms_embedding_column = tf.feature_column.embedding_column(terms_feature_column, dimension=2)
feature_columns = [ terms_embedding_column ]
my_optimizer = tf.train.AdagradOptimizer(learning_rate=0.1)
my_optimizer = tf.contrib.estimator.clip_gradients_by_norm(my_optimizer, 5.0)
classifier = tf.estimator.DNNClassifier(
feature_columns=feature_columns,
hidden_units=[10,10],
optimizer=my_optimizer
)
classifier.train(
input_fn=lambda: _input_fn([train_path]),
steps=1000)
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([train_path]),
steps=1000)
print("Training set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
evaluation_metrics = classifier.evaluate(
input_fn=lambda: _input_fn([test_path]),
steps=1000)
print("Test set metrics:")
for m in evaluation_metrics:
print(m, evaluation_metrics[m])
print("---")
embedding_column
与 indicator_column
之间进行权衡从概念上讲,在训练 LinearClassifier
或 DNNClassifier
时,需要根据实际情况使用稀疏列。TF 提供了两个选项:embedding_column
或 indicator_column
。
在训练 LinearClassifier(如任务 1 中所示)时,系统在后台使用了 embedding_column
。正如任务 2 中所示,在训练 DNNClassifier
时,您必须明确选择 embedding_column
或 indicator_column
。本部分通过一个简单的示例讨论了这两者之间的区别,以及如何在二者之间进行权衡。
假设我们的稀疏数据包含 "great"
、"beautiful"
和 "excellent"
这几个值。由于我们在此处使用的词汇表大小为 $V = 50$,因此第一层中的每个单元(神经元)的权重将为 50。我们用 $s$ 表示稀疏输入中的项数。对于此示例稀疏数据,$s = 3$。对于具有 $V$ 个可能值的输入层,带有 $d$ 个单元的隐藏层需要运行一次“矢量 - 矩阵”乘法运算:$(1 \times V) * (V \times d)$。此运算会产生 $O(V * d)$ 的计算成本。请注意,此成本与隐藏层中的权重数成正比,而与 $s$ 无关。
如果输入使用 indicator_column
进行了独热编码(长度为 $V$ 的布尔型矢量,存在用 1 表示,其余则为 0),这表示很多零进行了相乘和相加运算。
当我们通过使用大小为 $d$ 的 embedding_column
获得完全相同的结果时,我们将仅查询与示例输入中存在的 3 个特征 "great"
、"beautiful"
和 "excellent"
相对应的嵌入并将这三个嵌入相加:$(1 \times d) + (1 \times d) + (1 \times d)$。由于不存在的特征的权重在“矢量-矩阵”乘法中与 0 相乘,因此对结果没有任何影响;而存在的特征的权重在“矢量-矩阵”乘法中与 1 相乘。因此,将通过嵌入查询获得的权重相加会获得与“矢量-矩阵”乘法相同的结果。
当使用嵌入时,计算嵌入查询是一个 $O(s * d)$ 计算;从计算方面而言,它比稀疏数据中的 indicator_column
的 $O(V * d)$ 更具成本效益,因为 $s$ 远远小于 $V$。(请注意,这些嵌入是临时学习的结果。在任何指定的训练迭代中,都是当前查询的权重。
正如我们在任务 3 中看到的,通过在训练 DNNClassifier
过程中使用 embedding_column
,我们的模型学习了特征的低维度表示法,其中点积定义了一个针对目标任务的相似性指标。在本例中,影评中使用的相似术语(例如 "great"
和 "excellent"
)在嵌入空间中彼此之间距离较近(即具有较大的点积),而相异的术语(例如 "great"
和 "bad"
)在嵌入空间中彼此之间距离较远(即具有较小的点积)。