Работа с текстом переменной длины в Tensorflow
Я строю модель Tensorflow для выполнения вывода по текстовым фразам. Для простоты предположим, что мне нужен классификатор с фиксированным количеством выходных классов, но с вводом текста переменной длины. Другими словами, моя мини-партия будет представлять собой последовательность фраз, но не все фразы имеют одинаковую длину.
data = ['hello',
'my name is Mark',
'What is your name?']
Моим первым шагом предварительной обработки было создание словаря из всех возможных слов в словаре и сопоставление каждого слова с целочисленным идентификатором слова. Вход становится:
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]]}
4 ответа
Два других ответа верны, но мало подробностей. Я просто смотрел, как сделать это сам.
В 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], [линии, слова, 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())
Я еще не проработал детали, но думаю, что все это, возможно, можно заменить одним https://www.tensorflow.org/api_docs/python/tf/contrib/learn/DynamicRnnEstimator.
Как насчет этого? (Я не реализовал это. Но, возможно, эта идея сработает.) Этот метод основан на представлении BOW.
- Получите ваши данные как
tf.string
- Разделите это, используя
tf.string_split
- Найти индексы ваших слов, используя
tf.contrib.lookup.string_to_index_table_from_file
или жеtf.contrib.lookup.string_to_index_table_from_tensor
, Длина этого тензора может варьироваться. - Найдите вложения ваших индексов.
word_embeddings = tf.get_variable ("word_embeddings", [vocabulary_size, embedding_size]) embedded_word_ids = tf.nn.embedding_lookup(word_embeddings, word_ids)`
- Подведите итоги вложений. И вы получите тензор фиксированной длины (= размер вложения). Может быть, вы можете выбрать другой метод, то
sum
. (avg
,mean
или что-то другое)
Может быть, слишком поздно:) Удачи.
Я строил последовательность для переводчика последовательности на днях. Я решил сделать это для фиксированной длины в 32 слова (что было немного выше средней длины предложения), хотя вы можете сделать это так долго, как захотите. Затем я добавил слово NULL в словарь и добавил к нему все мои векторы предложений. Таким образом, я мог сказать модели, где находится конец моей последовательности, и модель будет просто выводить NULL в конце своего вывода. Например, возьми выражение "Привет, как тебя зовут?" Это стало бы "Привет, как тебя зовут? NULL NULL NULL NULL ... NULL". Это сработало довольно хорошо, но ваши потери и точность во время обучения будут казаться немного выше, чем на самом деле, так как модель обычно дает значения NULL, которые учитывают стоимость.
Существует еще один подход, называемый маскированием. Это также позволяет построить модель для последовательности с фиксированной длиной, но оценивать стоимость только до конца более короткой последовательности. Вы можете искать первый экземпляр NULL в выходной последовательности (или ожидаемом выходе, в зависимости от того, что больше) и оценивать только стоимость до этого момента. Также я думаю, что некоторые тензорные функции потока, такие как tf.dynamic_rnn, поддерживают маскирование, которое может быть более эффективным при использовании памяти. Я не уверен, так как я только попробовал первый подход отступа.
Наконец, я думаю, что в примере с тензорным потоком модели Seq2Seq они используют сегменты для последовательностей разного размера. Это, вероятно, решит проблему с памятью. Я думаю, что вы могли бы разделить переменные между моделями разных размеров.
Итак, вот что я сделал (не уверен, что это 100% правильный способ быть честным):
В вашем словаре, где каждая клавиша - это число, указывающее на одно конкретное слово, добавьте еще одну клавишу, скажем, 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]
]
Теперь, если вы умножите это на свою потерю, вы в основном устраните все потери, возникшие в результате заполнения.
Надеюсь это поможет.