在 TensorFlow 中处理可变长度文本

Posted

技术标签:

【中文标题】在 TensorFlow 中处理可变长度文本【英文标题】:Working with variable-length text in Tensorflow 【发布时间】:2016-12-01 20:26:30 【问题描述】:

我正在构建一个 Tensorflow 模型来对文本短语进行推理。 为简单起见,假设我需要一个具有固定数量的输出类但输入中的 可变长度文本 的分类器。换句话说,我的小批量将是一系列短语,但并非所有短语都具有相同的长度。

data = ['hello',
        'my name is Mark',
        'What is your name?']

我的第一个预处理步骤是构建字典中所有可能单词的字典,并将每个单词映射到其整数单词 ID。输入变为:

data = [[1],
        [2, 3, 4, 5],
        [6, 4, 7, 3]

处理这种输入的最佳方式是什么? tf.placeholder() 可以处理同一批数据中的可变大小输入吗? 或者我应该填充所有字符串,使它们都具有相同的长度,等于最长字符串的长度,使用一些占位符来代替缺失的单词?如果某些字符串比其他大多数字符串长得多,这似乎是非常低效的内存。

-- 编辑--

这是一个具体的例子。

当我知道我的数据点的大小(并且所有数据点都具有相同的长度,例如 3)时,我通常会使用以下内容:

input = tf.placeholder(tf.int32, shape=(None, 3)

with tf.Session() as sess:
  print(sess.run([...], feed_dict=input:[[1, 2, 3], [1, 2, 3]]))

占位符的第一个维度是小批量大小。

如果输入序列是不同长度的句子中的单词怎么办?

feed_dict=input:[[1, 2, 3], [1]]

【问题讨论】:

文本通常由序列模型处理。 IE,您的模型接受当前单词和上一步的输出,并且您堆叠模型的副本。作为基线,您可以从“词袋”开始——只需将所有词一起添加到单个字典向量中。 感谢您的回复。我的问题更多是关于 Tensorflow 的数据结构而不是模型。我可以使用以词袋表示的文本馈送的 RNN。仍然如果我的数据点有不同的长度,我应该在哪里或如何存储这种数据? 我编辑了删除对词嵌入的引用的问题,并举了一个更具体的例子来澄清我的问题。 【参考方案1】:

这个怎么样? (我没有实现这个。但也许这个想法会奏效。) 此方法基于BOW 表示。

    tf.string 获取您的数据 使用tf.string_split拆分它 使用tf.contrib.lookup.string_to_index_table_from_filetf.contrib.lookup.string_to_index_table_from_tensor 查找单词的索引。这个张量的长度可以变化。 查找索引的嵌入。
word_embeddings = tf.get_variable(“word_embeddings”, [词汇大小,嵌入大小]) Embedded_word_ids = tf.nn.embedding_lookup(word_embeddings, word_ids)`
    总结嵌入。你会得到一个固定长度的张量(=嵌入大小)。也许您可以选择其他方法然后sum。(avgmean 或其他)

也许为时已晚 :) 祝你好运。

【讨论】:

【参考方案2】:

其他两个答案是正确的,但细节很少。我只是在研究自己如何做到这一点。

TensorFlow 中的机制可以解决所有这些问题(对于某些部分来说,这可能是矫枉过正)。

从一个字符串张量开始(形状[3]):

import tensorflow as tf
lines = tf.constant([
    'Hello',
    'my name is also Mark',
    'Are there any other Marks here ?'])
vocabulary = ['Hello', 'my', 'name', 'is', 'also', 'Mark', 'Are', 'there', 'any', 'other', 'Marks', 'here', '?']

首先要做的是把它分成单词(注意问号前的空格。)

words = tf.string_split(lines," ")

单词现在将是一个稀疏张量(形状 [3,7])。其中索引的两个维度是[行号,位置]。这表示为:

indices    values
 0 0       'hello'
 1 0       'my'
 1 1       'name'
 1 2       'is'
 ...

现在您可以进行单词查找了:

table = tf.contrib.lookup.index_table_from_tensor(vocabulary)
word_indices = table.lookup(words)

这将返回一个稀疏张量,其中单词被其词汇索引替换。

现在您可以通过查看每行的最大位置来读出序列长度:

line_number = word_indices.indices[:,0]
line_position = word_indices.indices[:,1]
lengths = tf.segment_max(data = line_position, 
                         segment_ids = line_number)+1

因此,如果您正在处理可变长度序列,则可能需要放入一个 lstm ......所以让我们对输入使用词嵌入(它需要密集输入):

EMBEDDING_DIM = 100

dense_word_indices = tf.sparse_tensor_to_dense(word_indices)
e_layer = tf.contrib.keras.layers.Embedding(len(vocabulary), EMBEDDING_DIM)
embedded = e_layer(dense_word_indices)

现在嵌入的形状将是 [3,7,100], [lines, words, embedding_dim]。

那么就可以构建一个简单的lstm了:

LSTM_SIZE = 50
lstm = tf.nn.rnn_cell.BasicLSTMCell(LSTM_SIZE)

并在整个序列中运行,处理填充。

outputs, final_state = tf.nn.dynamic_rnn(
    cell=lstm,
    inputs=embedded,
    sequence_length=lengths,
    dtype=tf.float32)

现在输出的形状为 [3,7,50] 或 [line,word,lstm_size]。如果您想获取每行最后一个单词的状态,可以使用(隐藏!未记录!)select_last_activations 函数:

from tensorflow.contrib.learn.python.learn.estimators.rnn_common import select_last_activations
final_output = select_last_activations(outputs,tf.cast(lengths,tf.int32))

这会执行所有索引改组以选择上一个时间步的输出。这给出了 [3,50] 或 [line, lstm_size]

的大小
init_t = tf.tables_initializer()
init = tf.global_variables_initializer()
with tf.Session() as sess:
    init_t.run()
    init.run()
    print(final_output.eval().shape())

我还没有弄清楚细节,但我认为这可能都可以用一个 tf.contrib.learn.DynamicRnnEstimator 代替。

【讨论】:

【参考方案3】:

这就是我所做的(不确定它是否 100% 正确):

在每个键是指向一个特定单词的数字的 vocab dict 中,添加另一个键,例如 K,它指向 "<PAD>"(或您要用于填充的任何其他表示)

现在您的输入占位符将如下所示:

x_batch = tf.placeholder(tf.int32, shape=(batch_size, None))

其中 None 表示您的小批量中最大的短语/句子/记录。

我使用的另一个小技巧是将每个短语的长度存储在我的小批量中。例如:

如果我的输入是:x_batch = [[1], [1,2,3], [4,5]] 然后我存储:len_batch = [1, 3, 2]

稍后我在我的小批量中使用这个len_batch 和一个短语的最大大小(l_max)来创建一个二进制掩码。现在从上面看到l_max=3,所以我的面具看起来像这样:

mask = [
[1, 0, 0],
[1, 1, 1],
[1, 1, 0]
]

现在,如果将其与损失相乘,您基本上可以消除由于填充而引入的所有损失。

希望这会有所帮助。

【讨论】:

【参考方案4】:

前几天我正在构建一个序列到序列转换器。我决定做的是固定长度为 32 个单词(这比平均句子长度高一点),尽管你可以随心所欲。然后我在字典中添加了一个 NULL 词,并用它填充了我所有的句子向量。这样我就可以告诉模型我的序列结束在哪里,并且模型只会在其输出结束时输出 NULL。例如,使用“嗨,你叫什么名字?”这样的表达方式。这将变成“嗨,你叫什么名字?NULL NULL NULL NULL ... NULL”。它工作得很好,但是你在训练期间的损失和准确率会比实际高一些,因为模型通常会得到正确的 NULL,这会计入成本。

还有另一种方法称为屏蔽。这也允许您为固定长度的序列构建模型,但只评估到较短序列结束的成本。您可以在输出序列(或预期输出,以较大者为准)中搜索 NULL 的第一个实例,并仅评估到该点的成本。另外我认为一些张量流函数,如 tf.dynamic_rnn 支持屏蔽,这可能更节省内存。我不确定,因为我只尝试了第一种填充方法。

最后,我认为在 Seq2Seq 模型的 tensorflow 示例中,他们使用存储桶来存储不同大小的序列。这可能会解决您的内存问题。我认为您可以在不同大小的模型之间共享变量。

【讨论】:

以上是关于在 TensorFlow 中处理可变长度文本的主要内容,如果未能解决你的问题,请参考以下文章

如何在 UITableView 中优雅地处理来自网络的可变长度文本?

测试精度 0.5 TensorFlow RNN 可变长度字符串

TensorFlow:如何使用具有可变输入长度的 CudnnLSTM(如 dynamic_rnn)?

TensorFlow 中的生存分析

TensorFlow技术调研报告

tensorflow在文本处理中的使用——词袋