如何用 tf.estimators 进行文本分类?
原标题: 如何用 tf.estimators 进行文本分类?
来源:AI研习社 链接:https://www.yanxishe.com/TextTranslation/2352
这篇博文是一个使用TensorFlow Estimators进行文本分类的教程。
注:这篇文章是与令人敬畏的Julian Eisenschlos 一起写的,最初发表在
TensorFlow博客上。
使用Datasets进行数据加载
使用预先封装好的Estimator建立基线
使用词嵌入
使用LSTM和卷积构建通用的Estimator
加载预训练的词向量
使用TensorBoard进行模型评估和比较
欢迎来到TensorFlow Datasets和Estimators介绍系列博客的第4部分。其实,你并不需要阅读以前的资料,但是,如果你想复习一下以前的任何概念可以简单看一下。第1部分重点介绍了预设的Estimators,第2部分讨论了特征列,第3部分如何创建自定义Estimators。
在第4部分中,我们将在此前的基础上来解决自然语言处理(NLP)中的系列问题。特别是,本文示范了如何使用自定义的TensorFlow的estimators、embeddings和tf.layers模块来解决文本分类任务。在这个过程中,我们将学习word2vec和迁移学习(当处理有标签且数据稀缺时,可以将迁移学习作为一种新技术来提升模型的性能)。
我们将向您示范相关的代码片段。这是一个完整的Jupyter笔记本(Here),你可以在本地运行,也可以在Google Colaboratory上运行。此外,也提供了普通的.py源文件。注意:coding目的是演示Estimators运行,但是并没有针对最大性能进行优化。
任务
我们将使用的是IMDB大型电影评论数据集,其中包括25,00025,00025,000个用于训练的极性电影评论和25,00025,00025,000个用于测试。我们将使用这个数据集来训练一个二分类模型,能够预测评论是正面的还是负面的。
为了说明这一点,这里有一篇负面评论:
这一段数据样例,上原文
Now, I LOVE Italian horror films. The cheesier they are, the better. However, this is not cheesy Italian. This is week-old spaghetti sauce with rotting meatballs. It is amateur hour on every level. There is no suspense, no horror, with just a few drops of blood scattered around to remind you that you are in fact watching a horror film.
Keras为导入数据集提供了一个方便的处理程序,该数据集也可以作为一个序列化的numpy数组.npz文件进行下载(here.)。对于文本分类,标准的做法是限制词汇表的大小,以防止数据集过于稀疏和高维而导致的可能过度拟合情况。因此,每条评论都由一系列单词索引组成,从444(数据集中最常用的单词 the)到4999994999(对应于orange)。索引111代表句子的开头,索引222被分配给所有未知(也称为非词汇表词或OOV)标记。这些索引是通过在管道进行文本数据预处理获得而来,该管道包括数据清理、规范化和标记每个句子等操作,然后按频率为每个标记建立字典索引。
在我们将数据加载到内存中之后,我们用00填充每个句子,这样我们就有了两个25000×20025000×200数组,分别用于训练和测试。
vocab_size = 5000
sentence_size = 200
(x_train_variable, y_train), (x_test_variable, y_test) = imdb.load_data(num_words=vocab_size)
x_train = sequence.pad_sequences(
x_train_variable,
maxlen=sentence_size,
padding='post',
value=0)x_test = sequence.pad_sequences(
x_test_variable,
maxlen=sentence_size,
padding='post',
value=0)
输入函数
Estimator框架使用输入函数从模型本身分割数据管道。无论您的数据是在.csv文件中,还是在pandas.DataFrame中,无论它是否适合内存,都可以使用几个helper方法来创建它们。在我们的例子中,我们可以将Dataset.from_tensor_slices用于训练和测试数据集。
x_len_train = np.array([min(len(x), sentence_size) for x in x_train_variable])x_len_test = np.array([min(len(x), sentence_size) for x in x_test_variable])def parser(x, length, y): features = {"x": x, "len": length} return features, ydef train_input_fn(): dataset = tf.data.Dataset.from_tensor_slices((x_train, x_len_train, y_train)) dataset = dataset.shuffle(buffer_size=len(x_train_variable)) dataset = dataset.batch(100) dataset = dataset.map(parser) dataset = dataset.repeat() iterator = dataset.make_one_shot_iterator() return iterator.get_next()def eval_input_fn(): dataset = tf.data.Dataset.from_tensor_slices((x_test, x_len_test, y_test)) dataset = dataset.batch(100) dataset = dataset.map(parser) iterator = dataset.make_one_shot_iterator() return iterator.get_next()
我们对训练数据进行混洗,不预先定义要训练的epochs数量,而只需要一个epoch的测试数据进行评估。我们还添加了一个额外的“len”键,用于捕获原始的、未填充的序列的长度,稍后我们将使用它。
构建一个基线
开始任何机器学习项目的探索时,尝试确定基线模型是一个很好地做法。拥有一个简单而健壮的基线,越简单越好,这对于我们准确理解,通过增加模型额外复杂度,在模型性能方面获得了多少收益至关重要。很可能一个简单的解决方案足以满足我们的需求。
有鉴于此,让我们从尝试一个最简单的文本分类模型开始。这将是一个稀疏的线性模型,它为每个词赋予权重,并将所有结果相加,而不管顺序如何。由于这种模式不关心句子中单词的顺序,我们通常将其称为一个词袋方法。让我们看看如何使用Estimator实现这个模型。
我们首先定义作为分类器输入的feature列。正如我们在第2部分中所看到的,对于这个预处理的文本输入来说,categorical_column_with_identity是正确的输入。如果我们提供原始文本词,其他feature_columns可以为我们做很多预处理。我们现在可以使用预先制作的LinearClassifier。
column = tf.feature_column.categorical_column_with_identity('x', vocab_size)classifier = tf.estimator.LinearClassifier( feature_columns=[column], model_dir=os.path.join(model_dir, 'bow_sparse'))
最后,我们创建一个简单的函数来训练分类器,并另外创建一个精确的召回曲线。由于我们不打算在这篇博文中最大限度地提高性能,所以我们只对模型进行25000步的训练。
def train_and_evaluate(classifier): classifier.train(input_fn=train_input_fn, steps=25000) eval_results = classifier.evaluate(input_fn=eval_input_fn) predictions = np.array([p['logistic'][0] for p in classifier.predict(input_fn=eval_input_fn)]) tf.reset_default_graph() # Add a PR summary in addition to the summaries that the classifier writes pr = summary_lib.pr_curve('precision_recall', predictions=predictions, labels=y_test.astype(bool), num_thresholds=21) with tf.Session() as sess: writer = tf.summary.FileWriter(os.path.join(classifier.model_dir, 'eval'), sess.graph) writer.add_summary(sess.run(pr), global_step=0) writer.close()train_and_evaluate(classifier)
选择一个简单模型的好处之一是它更易于解释。模型越复杂,就越难检查,越容易像黑匣子一样工作。在本例中,我们可以从模型的最后一个checkpoint加载权重,并查看哪些词对应于绝对值最大的权重。结果和我们预期的一样。
# Load the tensor with the model weightsweights = classifier.get_variable_value('linear/linear_model/x/weights').flatten()# Find biggest weights in absolute valueextremes = np.concatenate((sorted_indexes[-8:], sorted_indexes[:8]))# word_inverted_index is a dictionary that maps from indexes back to tokensextreme_weights = sorted( [(weights[i], word_inverted_index[i - index_offset]) for i in extremes])# Create ploty_pos = np.arange(len(extreme_weights))plt.bar(y_pos, [pair[0] for pair in extreme_weights], align='center', alpha=0.5)plt.xticks(y_pos, [pair[1] for pair in extreme_weights], rotation=45, ha='right')plt.ylabel('Weight')plt.title('Most significant tokens') plt.show()
如我们所见,权重最大的词,如“refreshing”明显与正面情绪相关,而权重较大的词,则不可否认地引发负面情绪。一个简单但强大的调整是使用tf-idf分数来替代词的权重,从而提升模型性能。
进一步增加复杂性,我们可以添加词嵌入。嵌入是稀疏高维数据的密集低维表示形式。这允许我们的模型学习每个词的更有意义的表示,而不仅仅是简单的索引。虽然个别维度没有实际意义,但从足够大的语料库中学习到的低维度空间已经被证明可以捕捉诸如时态、复数、性别、主题关联性等关系信息。我们可以通过将现有的feature列转换为embedding_column来添加词嵌入。模型看到的表示是每个词嵌入的平均值(请参阅文档docs中的combiner参数)。我们可以将嵌入的功能插入预先封装的DNNClassifier。
对于敏锐的观察者需要注意的是:embedding_column只是将全连接层应用于词的稀疏二进制特征向量的一种有效方法,该特征向量乘以一个常数,该常数取决于所选组合器。这样做的一个直接后果是,直接在LinearClassifier中使用embedding_column是没有意义的,因为在这两个连续的线性层之间没有非线性,不会给模型增加任何预测能力,除非嵌入是预先训练的。
embedding_size = 50word_embedding_column = tf.feature_column.embedding_column( column, dimension=embedding_size)classifier = tf.estimator.DNNClassifier( hidden_units=[100], feature_columns=[word_embedding_column], model_dir=os.path.join(model_dir, 'bow_embeddings'))train_and_evaluate(classifier)
我们可以使用TensorBoard来可视化我们的5050维的词向量,使用t-SNE将5050维字向量投影到R3R3中。我们期望相似的词彼此接近。这是检验模型权重和发现意外行为的有用方法。
在这一点上,一种可能的方法是更深,进一步添加更多全连接层,以及充分利用层大小和训练函数。然而,这样做会增加额外的复杂性,也会忽略句子中的重要结构。词汇不是存在于真空中,含义是由词汇和它的邻居组成的。
卷积是利用这种结构的一种方法,类似于我们如何为图像分类建立显著的像素簇模型。直觉上,某些单词序列或n-grams通常具有相同的含义,而不管它们在句子中的相对位置如何变化。通过卷积运算引入结构先验,可以模拟相邻词之间的相互作用,从而为我们更好地表达这种意义提供一种方法。
下图展示了一个filter矩阵F∈Rd×mF∈Rd×m 三元组窗口中的词如何构建一个新的特征映射。之后,通常应用池化层来合并相邻的结果。
来源:Learning to Rank Short Text Pairs with Convolutional Deep Neural Networks by Severyn et al. [2015]
让我们看看,完整的模型架构。使用的dropout层是一种正则化技术,帮助模型不太可能出现过拟合现象。
创建一个普通的estimator
如前几篇博文所讲,tf.estimator框架为训练机器学习模型提供了一个高级API,这个API定义了train()、evaluate()和predict()操作、处理检查点、加载、初始化、服务、构建图和会话等功能。有一个小的预先创建的estimator功能家族,如之前我们使用的一样,但是很可能您需要建立自己的estimator。
编写自定义estimator意味着编写model_fn(特征、标签、模式、参数)并且返回一个EstimatorSpec。第一步是将特征映射到我们的嵌入层:
input_layer = tf.contrib.layers.embed_sequence( features['x'], vocab_size, embedding_size, initializer=params['embedding_initializer'])
然后使用tf.layers 来处理每个输出序列
training = (mode == tf.estimator.ModeKeys.TRAIN)dropout_emb = tf.layers.dropout(inputs=input_layer, rate=0.2, training=training)conv = tf.layers.conv1d( inputs=dropout_emb, filters=32, kernel_size=3, padding="same", activation=tf.nn.relu)pool = tf.reduce_max(input_tensor=conv, axis=1)hidden = tf.layers.dense(inputs=pool, units=250, activation=tf.nn.relu)dropout = tf.layers.dropout(inputs=hidden, rate=0.2, training=training)logits = tf.layers.dense(inputs=dropout_hidden, units=1)
最后,我们将使用Head来简化model_fn最后一部分的编写。Head已知如何计算predictions、loss、train_op、度量和导出输出,并且可以跨模型复用。前面讲的预先构建的的estimator也复用了这些;也为我们提供了一个统一的评估函数。我们将使用binary_classification_head,这是一个单标签二元分类head,使用sigmoid_cross_entropy_with_logits作为损失函数。
head = tf.contrib.estimator.binary_classification_head()optimizer = tf.train.AdamOptimizer() def _train_op_fn(loss): tf.summary.scalar('loss', loss) return optimizer.minimize( loss=loss, global_step=tf.train.get_global_step())return head.create_estimator_spec( features=features, labels=labels, mode=mode, logits=logits, train_op_fn=_train_op_fn)
正如上述代码所示,很容易运行这个模型;
initializer = tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0))params = {'embedding_initializer': initializer}cnn_classifier = tf.estimator.Estimator(model_fn=model_fn, model_dir=os.path.join(model_dir, 'cnn'), params=params)train_and_evaluate(cnn_classifier)
使用Estimator API和相同的模型head,我们还可以创建一个使用长短期记忆(LSTM)单元来代替卷积的分类器。递归模型是NLP应用程序最成功的构建块之一。LSTM按顺序处理整个文档,用它的单元在序列上递归,同时将序列的当前状态存储在记忆中。
与CNNs相比,递归模型的一个缺点是,由于递归的性质,模型变得更深更复杂,通常会产生较慢的训练速度和较差的收敛性。LSTMs(通常是RNNs)可能会遇到收敛性问题,例如梯度消失或爆炸,也就是说,只要进行足够的调整,它们可以获得许多问题的最新结果。根据经验,CNNs擅长于特征提取,而RNNs是擅长依赖于整个句子语义的任务,如问答或机器翻译等。
每个单元一次处理一个词嵌入,依赖于嵌入向量xtxt和前一状态ht-1ht-1的可微计算来更新其内部状态参数。为了更好地了解LSTMs是如何工作的,您可以参考Chris Olah的博客文章(here blog post)。
图片来源:Understanding LSTM Networks by Chris Olah
完整的LSTM模型,可以用如下简单流程图表示:
在本文的开头,我们将所有文档填充到200200200个词,这对于构建合适的张量是必要的。但是,当文档包含少于200200200个单词时,我们不希望LSTM继续处理填充标记,因为填充额部分不会添加任何信息,反而可能模型降低性能。出于这个原因,我们还想在填充前为网络提供原始序列的长度。在内部,模型将最后一个状态复制到序列的末尾。我们可以通过在输入函数中使用“len”特性来实现这一点。我们现在可以使用与上面相同的逻辑,简单地用LSTM单元替换卷积层、池化层和扁平层。
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(100)_, final_states = tf.nn.dynamic_rnn( lstm_cell, inputs, sequence_length=features['len'], dtype=tf.float32)logits = tf.layers.dense(inputs=final_states.h, units=1)
预训练向量
我们之前展示的大多数模型,都是依赖于词嵌入作为第一层。到目前,我们都是随机初始化的这个嵌入层。然而,许多以前的工作已经表明,使用在一个大的未标记语料库上预先训练的词嵌入作为初始化是有收益的,特别是当只训练少量的标签示例时。最流行的预训练词嵌入是word2vec。通过预先训练的词嵌入可以充分利用未标记数据中的知识,这也是一个转移学习的实例。
为此,我们将向您展示如何在Estimator中使用预训练词向量。我们将使用另一个流行模型GloVe,进行词向量的预先训练。
embeddings = {}with open('glove.6B.50d.txt', 'r', encoding='utf-8') as f: for line in f: values = line.strip().split() w = values[0] vectors = np.asarray(values[1:], dtype='float32') embeddings[w] = vectors
将向量从文件加载到内存后,我们使用与词汇表相同的索引将它们存储为numpy.array。创建的数组是的尺寸为(5000,50)。在每一行索引处,它包含50维向量,表示词汇表中同一索引处的单词。
embedding_matrix = np.random.uniform(-1, 1, size=(vocab_size, embedding_size))for w, i in word_index.items(): v = embeddings.get(w) if v is not None and i < vocab_size: embedding_matrix[i] = v
最后,我们可以使用一个通用的初始化函数,并在params对象中将其传递给cnn_model_fn,而无需任何其他修改。
def my_initializer(shape=None, dtype=tf.float32, partition_info=None): assert dtype is tf.float32 return embedding_matrixparams = {'embedding_initializer': my_initializer}cnn_pretrained_classifier = tf.estimator.Estimator( model_fn=cnn_model_fn, model_dir=os.path.join(model_dir, 'cnn_pretrained'), params=params)train_and_evaluate(cnn_pretrained_classifier)
运行TensorBoard
现在我们可以启动TensorBoard,看看我们所训练的不同模型在训练时间和性能方面是比较情况:
在终端,我们运行:
> tensorboard --logdir={model_dir}
我们可以将训练和测试过程中收集到的许多度量信息可视化,包括每个训练步骤中每个模型的损失函数值和精确召回曲线。当然,这对于选择哪个模型最适合我们的用例以及如何选择分类阈值是最有用的。
获取预测结果
为了获得对新句子的预测,我们可以在Estimator实例中使用predict方法,该方法将为每个模型加载最新的checkpoint,并预测实例进行评估。但是在将数据传递到模型之前,我们必须将数据映射到相应的索引,如下所示。
def text_to_index(sentence): # Remove punctuation characters except for the apostrophe translator = str.maketrans('', '', string.punctuation.replace("'", '')) tokens = sentence.translate(translator).lower().split() return np.array([1] + [word_index[t] + index_offset if t in word_index else 2 for t in tokens])def print_predictions(sentences, classifier): indexes = [text_to_index(sentence) for sentence in sentences] x = sequence.pad_sequences(indexes, maxlen=sentence_size, padding='post', value=-1) length = np.array([min(len(x), sentence_size) for x in indexes]) predict_input_fn = tf.estimator.inputs.numpy_input_fn(x={"x": x, "len": length}, shuffle=False) predictions = [p['logistic'][0] for p in classifier.predict(input_fn=predict_input_fn)] print(predictions)
值得注意的是,检查点本身不足以进行预测;为了将保存的权重映射到相应的张量,用于构建estimator的实际代码也是必要的。将保存的检查点与创建它们的代码分支相关联是一个好的实践。
如果您对以完全可恢复的方式将模型导出到磁盘感兴趣,那么您可能需要查看SavedModel类,该类对于通过使用TensorFlow服务的API为模型提供服务。
总结
在这篇博客文章中,我们探讨了如何使用estimators进行文本分类,使用了IMDB评论数据集。我们训练和可视化我们自己的词嵌入,以及加载的预先训练的词嵌入。我们从一个简单的基线开始,逐步迈向卷积神经网络和LSTMs。
更多详细信息,请查看:
可在本地运行或在Colaboratory上运行的notebook。
源码可以下载
TensorFlow嵌入指南
TensorFlow的词表达教程。
使用NLTK处理原始文本数据,及如何设计语言处理管道
发起:唐里 校对:唐里 审核:鸢尾
参与翻译(1人):
邓普斯•杰弗
英文原文:Text Classification with TensorFlow Estimators
一THE END一
免责声明:本文来自互联网新闻客户端自媒体,不代表本网的观点和立场。
合作及投稿邮箱:E-mail:editor@tusaishared.com
热门资源
应用笔画宽度变换...
应用背景:是盲人辅助系统,城市环境中的机器导航...
GAN之根据文本描述...
一些比较好玩的任务也就应运而生,比如图像修复、...
端到端语音识别时...
从上世纪 50 年代诞生到 2012 年引入 DNN 后识别效...
人体姿态估计的过...
人体姿态估计是计算机视觉中一个很基础的问题。从...
谷歌发布TyDi QA语...
为了鼓励对多语言问答技术的研究,谷歌发布了 TyDi...
智能在线
400-630-6780
聆听.建议反馈
E-mail: support@tusaishared.com