Примечания к исследованию CB014: модель TensorFlow seq2seq шаг за шагом

TensorFlow Python GitHub Нейронные сети

Нейронные сети. «Создайте свою собственную нейронную сеть» объясняет принцип искусственной нейронной сети с очень простым для понимания описанием и реализует его с помощью кода, и эффект тестирования очень хороший.

Рекуррентные нейронные сети и LSTM. Кристофер Ола http://colah.github.io/posts/2015-08-Understanding-LSTMs/.

Модель seq2seq основана на рекуррентной модели нейронной сети последовательностей, языковом переводе, автоматических ответах на вопросы и других сценариях последовательностей последовательностей, все из которых могут использовать модель seq2seq и использовать seq2seq для реализации принципа чат-бота. http://suriyadeepan.github.io/2016-06-28-easy-seq2seq/.

Модель внимания (модель внимания) предназначена для решения проблемы, заключающейся в том, что декодер seq2seq принимает только тот факт, что последний вывод кодировщика находится далеко от предыдущего вывода, что приводит к потере информации. Ответ обычно основывается на ключевой информации о местоположении в вопросе, на котором сосредоточено внимание, http://www.wildml.com/2016/01/attention-and-memory-in-deep-learning-and-nlp/ .

tensorflow seq2seq для создания чат-ботов. tensorflow предоставляет ключевые интерфейсы: https://www.tensorflow.org/api_docs/python/tf/contrib/legacy_seq2seq/embedding_attention_seq2seq.

embedding_attention_seq2seq(
    encoder_inputs,
    decoder_inputs,
    cell,
    num_encoder_symbols,
    num_decoder_symbols,
    embedding_size,
    num_heads=1,
    output_projection=None,
    feed_previous=False,
    dtype=None,
    scope=None,
    initial_state_attention=False
)

Параметр encoder_inputs — это список, каждый элемент списка — это одномерный тензор, форма тензора — [batch_size], а каждый элемент тензора — это целое число, подобное:

[array([0, 0, 0, 0], dtype=int32), 
array([0, 0, 0, 0], dtype=int32), 
array([8, 3, 5, 3], dtype=int32), 
array([7, 8, 2, 1], dtype=int32), 
array([6, 2, 10, 9], dtype=int32)]

5 массивов, указывающих, что длина предложения составляет 5 слов. Каждый массив имеет 4 числа, указывающие на то, что партия равна 4 и всего имеется 4 образца. Первый образец: [[0],[0],[8],[7],[6]] и второй образец: [[0],[0],[3],[8],[2] ], число различает разные идентификаторы слов, обычно получаемые с помощью статистики, идентификатор представляет слово.

Параметр decoder_inputs имеет ту же структуру, что и encoder_inputs.

Ячейка параметра представляет собой рекуррентную единицу нейронной сети типа tf.nn.rnn_cell.RNNCell, в качестве которой можно использовать tf.contrib.rnn.BasicLSTMCell, tf.contrib.rnn.GRUCell.

Параметр num_encoder_symbols представляет собой целое число, указывающее количество целочисленных идентификаторов слов encoder_inputs.

num_decoder_symbols представляет количество целочисленных идентификаторов слов в decoder_inputs.

embedding_size указывает, что внутреннее вложение слова преобразуется в многомерный вектор, который должен быть равен размеру RNNCell.

num_heads представляет собой количество нажатий, включающих в себя «внимание_состояний».

output_projection представляет собой кортеж структуры (W, B), W представляет собой весовую матрицу формы [output_size x num_decoder_symbols], а B представляет собой вектор смещения формы [num_decoder_symbols]. Выход каждой RNNCell отображается в вектор размерности num_decoder_symbols через WX+B, а значение вектора представляет любую вероятность decoder_symbol, softmax.

feed_previous указывает, обеспечивают ли входные данные decoder_input непосредственно ввод обучающих данных или используют предыдущее отображение вывода RNNCell, если значение feed_previous равно True, использовать предыдущий вывод RNNCell после отображения WX+B.

dtype — это тип данных состояния RNN, по умолчанию — tf.float32.

scope — это имя подграфа, значение по умолчанию — «embedding_attention_seq2seq».

initial_state_attention указывает, следует ли инициализировать внимание, по умолчанию — нет, что указывает на то, что полная инициализация равна 0. Возвращаемое значение представляет собой кортеж структуры (выходы, состояние), выходы представляют собой список, длина которого равна длине предложения (количество слов, такое же, как длина списка encoder_inputs), каждый элемент списка представляет собой 2D тензор типа tf.float32, а первое измерение — это количество выборок, например, 4 выборки имеют четыре набора тензоров, а длина каждого тензора равна embedding_size. описание выходов 548 чисел с плавающей запятой, 5 — длина предложения, 4 — количество выборок и 8 — размерность вектора слова.

Состояние возврата, num_layers LSTMStateTuple формируют большой кортеж, num_layers — параметр ячейки инициализации, указывающий, что блок нейронной сети имеет несколько слоев, один состоит из 3 слоев нейронов LSTM для формирования многослойной рекуррентной нейронной сети кодер-декодер. Входные данные encoder_inputs входят в первый слой LSTM-нейронов энкодера, выходные данные нейрона передаются второму слою LSTM-нейронов, выходные данные второго слоя передаются третьему слою, выходное состояние первого слоя кодер передается на первый слой LSTM-нейронов декодера, и, в свою очередь, аналогия.

Структура LSTMStateTuple состоит из двух тензорных кортежей.Первый тензор называется c и состоит из четырех 8-мерных векторов (4 — пакет, 8 — размерность вектора слова state_size), а второй тензор называется h, который также состоит из четырех 8-мерная векторная композиция.

c — данные, переданные в следующее хранилище временных рядов, а h — скрытый вывод.

реализация кода тензорного потока:

concat = _linear([inputs, h], 4 * self._num_units, True)
i, j, f, o = array_ops.split(value=concat, num_or_size_splits=4, axis=1)
new_c = (c * sigmoid(f + self._forget_bias) + sigmoid(i) * self._activation(j))
new_h = self._activation(new_c) * sigmoid(o)

Непосредственно используйте обучение embedding_attention_seq2seq, состояние возврата обычно не используется.

Создайте входные параметры, чтобы обучить модель seq2seq. Возьмите 1, 3, 5, 7, 9... нечетные последовательности в качестве примеров для построения выборок, например, две выборки: [[1,3,5],[7,9,11]] и [[3,5 ,7],[9,11,13]]:

train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]]

Чтобы удовлетворить последовательности разной длины, обучающая последовательность длиннее, чем примерная последовательность, которая длиннее, чем настройка 5.

input_seq_len = 5
output_seq_len = 5

Длина выборки меньше длины обучающей последовательности, дополненной 0

PAD_ID = 0

Первый пример encoder_input:

encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0])) + train_set[0][0]

Второй пример encoder_input:

encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0])) + train_set[1][0]

decoder_input начинается с GO_ID, затем вводит образец последовательности и, наконец, заполняет его PAD_ID.

GO_ID = 1
decoder_input_0 = [GO_ID] + train_set[0][1] 
    + [PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1)
decoder_input_1 = [GO_ID] + train_set[1][1] 
    + [PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1)

Преобразуйте входные данные в формат входных параметров embedding_attention_seq2seq encoder_inputs и decoder_inputs:

encoder_inputs = []
decoder_inputs = []
for length_idx in xrange(input_seq_len):
    encoder_inputs.append(np.array([encoder_input_0[length_idx], 
                          encoder_input_1[length_idx]], dtype=np.int32))
for length_idx in xrange(output_seq_len):
    decoder_inputs.append(np.array([decoder_input_0[length_idx], 
                          decoder_input_1[length_idx]], dtype=np.int32))

Автономная функция:

# coding:utf-8
import numpy as np

# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1

def get_samples():
    """构造样本数据

    :return:
        encoder_inputs: [array([0, 0], dtype=int32), 
                         array([0, 0], dtype=int32), 
                         array([1, 3], dtype=int32),
                         array([3, 5], dtype=int32), 
                         array([5, 7], dtype=int32)]
        decoder_inputs: [array([1, 1], dtype=int32), 
                         array([7, 9], dtype=int32), 
                         array([ 9, 11], dtype=int32),
                         array([11, 13], dtype=int32), 
                         array([0, 0], dtype=int32)]
    """
    train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]]
    encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0])) 
                      + train_set[0][0]
    encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0])) 
                      + train_set[1][0]
    decoder_input_0 = [GO_ID] + train_set[0][1] 
                      + [PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1)
    decoder_input_1 = [GO_ID] + train_set[1][1] 
                      + [PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1)

    encoder_inputs = []
    decoder_inputs = []
    for length_idx in xrange(input_seq_len):
        encoder_inputs.append(np.array([encoder_input_0[length_idx], 
                              encoder_input_1[length_idx]], dtype=np.int32))
    for length_idx in xrange(output_seq_len):
        decoder_inputs.append(np.array([decoder_input_0[length_idx], 
                              decoder_input_1[length_idx]], dtype=np.int32))
    return encoder_inputs, decoder_inputs

Чтобы построить модель, текущий процесс тензорного потока состоит в том, чтобы сначала построить график, а затем подключить данные для расчета.Процесс построения модели на самом деле состоит в построении графика. Сначала создайте заполнители для encoder_inputs и decoder_inputs:

import tensorflow as tf
encoder_inputs = []
decoder_inputs = []
for i in xrange(input_seq_len):
    encoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="encoder{0}".format(i)))
for i in xrange(output_seq_len):
    decoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="decoder{0}".format(i)))

Создайте нейронную структуру LSTM с размером ячеек памяти = 8:

size = 8
cell = tf.contrib.rnn.BasicLSTMCell(size)

Максимальное значение тренировочной нечетной последовательности — это входной максимум 10 и выходной максимум 16.

num_encoder_symbols = 10
num_decoder_symbols = 16

Передайте параметры в embedding_attention_seq2seq, чтобы получить вывод

from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq
outputs, _ = seq2seq.embedding_attention_seq2seq(
                    encoder_inputs,
                    decoder_inputs[:output_seq_len],
                    cell,
                    cell,
                    num_encoder_symbols=num_encoder_symbols,
                    num_decoder_symbols=num_decoder_symbols,
                    embedding_size=size,
                    output_projection=None,
                    feed_previous=False,
                    dtype=tf.float32)

Соберите часть модели и поместите отдельную функцию:

def get_model():
    """构造模型
    """
    encoder_inputs = []
    decoder_inputs = []
    for i in xrange(input_seq_len):
        encoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="encoder{0}".format(i)))
    for i in xrange(output_seq_len):
        decoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="decoder{0}".format(i)))

    cell = tf.contrib.rnn.BasicLSTMCell(size)

    # 这里输出的状态我们不需要
    outputs, _ = seq2seq.embedding_attention_seq2seq(
                        encoder_inputs,
                        decoder_inputs,
                        cell,
                        num_encoder_symbols=num_encoder_symbols,
                        num_decoder_symbols=num_decoder_symbols,
                        embedding_size=size,
                        output_projection=None,
                        feed_previous=False,
                        dtype=tf.float32)
    return encoder_inputs, decoder_inputs, outputs

Создайте сеанс выполнения и заполните образцы данных:

with tf.Session() as sess:
    sample_encoder_inputs, sample_decoder_inputs = get_samples()
    encoder_inputs, decoder_inputs, outputs = get_model()
    input_feed = {}
    for l in xrange(input_seq_len):
        input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
    for l in xrange(output_seq_len):
        input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]

    sess.run(tf.global_variables_initializer())
    outputs = sess.run(outputs, input_feed)
    print outputs

Выходные данные представляют собой список, состоящий из 5 массивов (5 — длина последовательности), и каждый массив состоит из двух списков размером 16 (2 означает 2 выборки, 16 означает, что имеется 16 выходных символов). outputs соответствует выводу seq2seq, W, X, Y, Z, EOS, decoder_inputs[1:], [7,9,11] и [9,11,13] в образце.

структура decoder_inputs:

[array([1, 1], dtype=int32), array([ 7, 29], dtype=int32), array([ 9, 31], dtype=int32), array([11, 33], dtype=int32), array([0, 0], dtype=int32)]

Описание функции потерь: https://www.tensorflow.org/api_docs/python/tf/contrib/legacy_seq2seq/sequence_loss

sequence_loss(
    logits,
    targets,
    weights,
    average_across_timesteps=True,
    average_across_batch=True,
    softmax_loss_function=None,
    name=None
)

Функция потерь, средняя отрицательная логарифмическая вероятность целевого слова, является наименьшей. logits — это список, состоящий из нескольких тензоров 2D-формы [batch * num_decoder_symbols], пакет — 2, num_decoder_symbols — 16, а количество тензоров в списке — output_seq_len. target — это список той же длины, что и логиты (output_seq_len), каждый элемент списка — целое число для формирования одномерного тензора, каждая форма тензора — [batch], тип данных — tf.int32, а decoder_inputs[1:] W, X, Y, Z, структура EOS такая же. Веса имеют ту же структуру, что и цели, а тип данных — tf.float32.

Чтобы вычислить взвешенную кросс-энтропийную потерю, веса должны инициализировать заполнители:

target_weights = []
    target_weights.append(tf.placeholder(tf.float32, shape=[None], 
                          name="weight{0}".format(i)))

Рассчитайте стоимость убытка:

targets = [decoder_inputs[i + 1] for i in xrange(len(decoder_inputs) - 1)]
loss = seq2seq.sequence_loss(outputs, targets, target_weights)

Длина целей на единицу меньше, чем у decoder_inputs, длина такая же, а длина инициализации decoder_inputs увеличена на 1. Рассчитайте взвешенную кросс-энтропийную потерю, вес значимого числа велик, вес бессмысленного мал, целевое значение присваивается как 1, а незначащее значение присваивается как 0:

# coding:utf-8
import numpy as np
import tensorflow as tf
from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq

# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1
# LSTM神经元size
size = 8
# 最大输入符号数
num_encoder_symbols = 10
# 最大输出符号数
num_decoder_symbols = 16

def get_samples():
    """构造样本数据

    :return:
        encoder_inputs: [array([0, 0], dtype=int32), 
                         array([0, 0], dtype=int32), 
                         array([1, 3], dtype=int32),
                         array([3, 5], dtype=int32), 
                         array([5, 7], dtype=int32)]
        decoder_inputs: [array([1, 1], dtype=int32), 
                         array([7, 9], dtype=int32), 
                         array([ 9, 11], dtype=int32),
                         array([11, 13], dtype=int32), 
                         array([0, 0], dtype=int32)]
    """
    train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]]
    encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0])) 
                         + train_set[0][0]
    encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0])) 
                         + train_set[1][0]
    decoder_input_0 = [GO_ID] + train_set[0][1] 
                         + [PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1)
    decoder_input_1 = [GO_ID] + train_set[1][1] 
                         + [PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1)

    encoder_inputs = []
    decoder_inputs = []
    target_weights = []
    for length_idx in xrange(input_seq_len):
        encoder_inputs.append(np.array([encoder_input_0[length_idx], 
                         encoder_input_1[length_idx]], dtype=np.int32))
    for length_idx in xrange(output_seq_len):
        decoder_inputs.append(np.array([decoder_input_0[length_idx], 
                         decoder_input_1[length_idx]], dtype=np.int32))
        target_weights.append(np.array([
            0.0 if length_idx == output_seq_len - 1 
                         or decoder_input_0[length_idx] == PAD_ID else 1.0,
            0.0 if length_idx == output_seq_len - 1 
                         or decoder_input_1[length_idx] == PAD_ID else 1.0,
        ], dtype=np.float32))
    return encoder_inputs, decoder_inputs, target_weights

def get_model():
    """构造模型
    """
    encoder_inputs = []
    decoder_inputs = []
    target_weights = []
    for i in xrange(input_seq_len):
        encoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="encoder{0}".format(i)))
    for i in xrange(output_seq_len + 1):
        decoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="decoder{0}".format(i)))
    for i in xrange(output_seq_len):
        target_weights.append(tf.placeholder(tf.float32, shape=[None],
                          name="weight{0}".format(i)))

    # decoder_inputs左移一个时序作为targets
    targets = [decoder_inputs[i + 1] for i in xrange(output_seq_len)]

    cell = tf.contrib.rnn.BasicLSTMCell(size)

    # 这里输出的状态我们不需要
    outputs, _ = seq2seq.embedding_attention_seq2seq(
                        encoder_inputs,
                        decoder_inputs[:output_seq_len],
                        cell,
                        num_encoder_symbols=num_encoder_symbols,
                        num_decoder_symbols=num_decoder_symbols,
                        embedding_size=size,
                        output_projection=None,
                        feed_previous=False,
                        dtype=tf.float32)

    # 计算加权交叉熵损失
    loss = seq2seq.sequence_loss(outputs, targets, target_weights)
    return encoder_inputs, decoder_inputs, target_weights, outputs, loss

def main():
    with tf.Session() as sess:
        sample_encoder_inputs, sample_decoder_inputs, sample_target_weights 
                          = get_samples()
        encoder_inputs, decoder_inputs, target_weights, outputs, loss = get_model()

        input_feed = {}
        for l in xrange(input_seq_len):
            input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
        for l in xrange(output_seq_len):
            input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
            input_feed[target_weights[l].name] = sample_target_weights[l]
        input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

        sess.run(tf.global_variables_initializer())
        loss = sess.run(loss, input_feed)
        print loss

if __name__ == "__main__":
    main()

Обучите модель, уменьшите потери после нескольких раундов вычислений и используйте градиентный спуск для обновления параметров. tensorflow предоставляет классы градиентного спуска: https://www.tensorflow.org/api_docs/python/tf/train/GradientDescentOptimizer.

Конструктор класса GradientDescentOptimizer:

__init__(
    learning_rate,
    use_locking=False,
    name='GradientDescent'
)

Ключом является скорость обучения первого параметра. Метод расчета градиента:

compute_gradients(
    loss,
    var_list=None,
    gate_gradients=GATE_OP
    aggregation_method=None,
    colocate_gradients_with_ops=False,
    grad_loss=None
)

Потеря ключевого параметра — это входящее значение ошибки, а возвращаемое значение — (градиент, переменная) для формирования списка. Метод обновления параметра:

apply_gradients(
    grads_and_vars,
    global_step=None,
    name=None
)

grads_and_vars — это возвращаемое значение calculate_gradients. Рассчитайте метод параметра обновления градиента в соответствии с потерями:

learning_rate = 0.1
opt = tf.train.GradientDescentOptimizer(learning_rate)
update = opt.apply_gradients(opt.compute_gradients(loss))

Основная функция увеличивает итерацию цикла:

def main():
    with tf.Session() as sess:
        sample_encoder_inputs, sample_decoder_inputs, sample_target_weights 
                          = get_samples()
        encoder_inputs, decoder_inputs, target_weights, outputs, loss, update 
                          = get_model()

        input_feed = {}
        for l in xrange(input_seq_len):
            input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
        for l in xrange(output_seq_len):
            input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
            input_feed[target_weights[l].name] = sample_target_weights[l]
        input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

        sess.run(tf.global_variables_initializer())
        while True:
            [loss_ret, _] = sess.run([loss, update], input_feed)
            print loss_ret

Реализуйте логику прогнозирования, вводите только образец encoder_input и автоматически прогнозируйте decoder_input. Обученная модель сохраняется и загружается при перезапуске прогнозирования:

def get_model():
      ...
saver = tf.train.Saver(tf.global_variables())
      return ..., saver

Реализация после тренировки

saver.save(sess, './model/demo')

Модели хранятся в файлах, начинающихся с demo в каталоге ./model.Чтобы загрузить, сначала вызовите:

saver.restore(sess, './model/demo')

Во время предсказания в принципе не может быть ввода decoder_inputs.При выполнении decoder_inputs берет предыдущий вывод временного ряда, и параметр embedding_attention_seq2seq feed_previous.Если он равен True, то ввод каждого шага декодера заполняется выводом предыдущего шаг.

get_model передает параметры для различения обучения и прогнозирования, это разные конфигурации feed_previous, и основная функция для прогнозирования также отличается.Отдельно две функции для обучения и прогнозирования.

# coding:utf-8
import sys
import numpy as np
import tensorflow as tf
from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq

# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1
# 结尾标记
EOS_ID = 2
# LSTM神经元size
size = 8
# 最大输入符号数
num_encoder_symbols = 10
# 最大输出符号数
num_decoder_symbols = 16
# 学习率
learning_rate = 0.1

def get_samples():
    """构造样本数据

    :return:
        encoder_inputs: [array([0, 0], dtype=int32), 
                         array([0, 0], dtype=int32), 
                         array([5, 5], dtype=int32),
                         array([7, 7], dtype=int32), 
                         array([9, 9], dtype=int32)]
        decoder_inputs: [array([1, 1], dtype=int32), 
                         array([11, 11], dtype=int32), 
                         array([13, 13], dtype=int32),
                         array([15, 15], dtype=int32), 
                         array([2, 2], dtype=int32)]
    """
    train_set = [[[5, 7, 9], [11, 13, 15, EOS_ID]], [[7, 9, 11], [13, 15, 17, EOS_ID]]]
    raw_encoder_input = []
    raw_decoder_input = []
    for sample in train_set:
        raw_encoder_input.append([PAD_ID] * (input_seq_len - len(sample[0])) + sample[0])
        raw_decoder_input.append([GO_ID] + sample[1] 
                         + [PAD_ID] * (output_seq_len - len(sample[1]) - 1))

    encoder_inputs = []
    decoder_inputs = []
    target_weights = []

    for length_idx in xrange(input_seq_len):
        encoder_inputs.append(np.array([encoder_input[length_idx] 
                          for encoder_input in raw_encoder_input], 
                                                  dtype=np.int32))
    for length_idx in xrange(output_seq_len):
        decoder_inputs.append(np.array([decoder_input[length_idx] 
                          for decoder_input in raw_decoder_input], 
                                                  dtype=np.int32))
        target_weights.append(np.array([
            0.0 if length_idx == output_seq_len - 1 
                         or decoder_input[length_idx] == PAD_ID else 1.0 
                         for decoder_input in raw_decoder_input
        ], dtype=np.float32))
    return encoder_inputs, decoder_inputs, target_weights

def get_model(feed_previous=False):
    """构造模型
    """
    encoder_inputs = []
    decoder_inputs = []
    target_weights = []
    for i in xrange(input_seq_len):
        encoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="encoder{0}".format(i)))
    for i in xrange(output_seq_len + 1):
        decoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="decoder{0}".format(i)))
    for i in xrange(output_seq_len):
        target_weights.append(tf.placeholder(tf.float32, shape=[None], 
                         name="weight{0}".format(i)))

    # decoder_inputs左移一个时序作为targets
    targets = [decoder_inputs[i + 1] for i in xrange(output_seq_len)]

    cell = tf.contrib.rnn.BasicLSTMCell(size)

    # 这里输出的状态我们不需要
    outputs, _ = seq2seq.embedding_attention_seq2seq(
                        encoder_inputs,
                        decoder_inputs[:output_seq_len],
                        cell,
                        num_encoder_symbols=num_encoder_symbols,
                        num_decoder_symbols=num_decoder_symbols,
                        embedding_size=size,
                        output_projection=None,
                        feed_previous=feed_previous,
                        dtype=tf.float32)

    # 计算加权交叉熵损失
    loss = seq2seq.sequence_loss(outputs, targets, target_weights)
    # 梯度下降优化器
    opt = tf.train.GradientDescentOptimizer(learning_rate)
    # 优化目标:让loss最小化
    update = opt.apply_gradients(opt.compute_gradients(loss))
    # 模型持久化
    saver = tf.train.Saver(tf.global_variables())
    return encoder_inputs, decoder_inputs, target_weights, 
                          outputs, loss, update, saver, targets

def train():
    """
    训练过程
    """
    with tf.Session() as sess:
        sample_encoder_inputs, sample_decoder_inputs, sample_target_weights 
                          = get_samples()
        encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver, targets 
                          = get_model()

        input_feed = {}
        for l in xrange(input_seq_len):
            input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
        for l in xrange(output_seq_len):
            input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
            input_feed[target_weights[l].name] = sample_target_weights[l]
        input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

        # 全部变量初始化
        sess.run(tf.global_variables_initializer())

        # 训练200次迭代,每隔10次打印一次loss
        for step in xrange(200):
            [loss_ret, _] = sess.run([loss, update], input_feed)
            if step % 10 == 0:
                print 'step=', step, 'loss=', loss_ret

        # 模型持久化
        saver.save(sess, './model/demo')

def predict():
    """
    预测过程
    """
    with tf.Session() as sess:
        sample_encoder_inputs, sample_decoder_inputs, sample_target_weights 
                          = get_samples()
        encoder_inputs, decoder_inputs, target_weights, 
                          outputs, loss, update, saver, targets 
                          = get_model(feed_previous=True)
        # 从文件恢复模型
        saver.restore(sess, './model/demo')

        input_feed = {}
        for l in xrange(input_seq_len):
            input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
        for l in xrange(output_seq_len):
            input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
            input_feed[target_weights[l].name] = sample_target_weights[l]
        input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

        # 预测输出
        outputs = sess.run(outputs, input_feed)
        # 一共试验样本有2个,所以分别遍历
        for sample_index in xrange(2):
            # 因为输出数据每一个是num_decoder_symbols维的
            # 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
            outputs_seq = [int(np.argmax(logit[sample_index], axis=0)) for logit in outputs]
            # 如果是结尾符,那么后面的语句就不输出了
            if EOS_ID in outputs_seq:
                outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
            outputs_seq = [str(v) for v in outputs_seq]
            print " ".join(outputs_seq)

if __name__ == "__main__":
    if sys.argv[1] == 'train':
        train()
    else:
        predict()

Файл называется demo.py, запустите ./demo.py train для обучения модели и запустите ./demo.py predict для предсказания.

При прогнозировании вычисляйте в соответствии с полными encoder_inputs и decoder_inputs, продолжайте улучшать прогноз и вручную вводите строку чисел (только часть кодировщика).

Учтите, что входная строка цифрового идентификатора, разделенная пробелами, преобразуется в функции кодировщика, декодера и target_weight для предсказания.

def seq_to_encoder(input_seq):
    """从输入空格分隔的数字id串,转成预测用的encoder、decoder、target_weight等
    """
    input_seq_array = [int(v) for v in input_seq.split()]
    encoder_input = [PAD_ID] * (input_seq_len - len(input_seq_array)) + input_seq_array
    decoder_input = [GO_ID] + [PAD_ID] * (output_seq_len - 1)
    encoder_inputs = [np.array([v], dtype=np.int32) for v in encoder_input]
    decoder_inputs = [np.array([v], dtype=np.int32) for v in decoder_input]
    target_weights = [np.array([1.0], dtype=np.float32)] * output_seq_len
    return encoder_inputs, decoder_inputs, target_weights

Затем мы перепишем функцию прогнозирования следующим образом:

def predict():
    """
    预测过程
    """
    with tf.Session() as sess:
        encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver 
                          = get_model(feed_previous=True)
        saver.restore(sess, './model/demo')
        sys.stdout.write("> ")
        sys.stdout.flush()
        input_seq = sys.stdin.readline()
        while input_seq:
            input_seq = input_seq.strip()
            sample_encoder_inputs, sample_decoder_inputs, sample_target_weights 
                          = seq_to_encoder(input_seq)

            input_feed = {}
            for l in xrange(input_seq_len):
                input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
            for l in xrange(output_seq_len):
                input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
                input_feed[target_weights[l].name] = sample_target_weights[l]
            input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

            # 预测输出
            outputs_seq = sess.run(outputs, input_feed)
            # 因为输出数据每一个是num_decoder_symbols维的
            # 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
            outputs_seq = [int(np.argmax(logit[0], axis=0)) for logit in outputs_seq]
            # 如果是结尾符,那么后面的语句就不输出了
            if EOS_ID in outputs_seq:
                outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
            outputs_seq = [str(v) for v in outputs_seq]
            print " ".join(outputs_seq)

            sys.stdout.write("> ")
            sys.stdout.flush()
            input_seq = sys.stdin.readline()

Выполнить прогноз ./demo.py.

Установите num_encoder_symbols = 10, 11 не может быть выражено, измените параметры и увеличьте выборки:

# 最大输入符号数
num_encoder_symbols = 32
# 最大输出符号数
num_decoder_symbols = 32
……
train_set = [
              [[5, 7, 9], [11, 13, 15, EOS_ID]], 
              [[7, 9, 11], [13, 15, 17, EOS_ID]], 
              [[15, 17, 19], [21, 23, 25, EOS_ID]]
            ]
……

Количество итераций было увеличено до 10 000.

Входные образцы, эффект прогнозирования очень хороший. Если вы замените их другими входными данными, вы все равно должны найти наиболее похожие результаты в выходных данных выборки, чтобы предсказать результаты. Вы не можете думать и не обладаете интеллектом. Модель более подходит. для классификации, а не для вывода.

Во время обучения китайский словарь преобразуется в числовой идентификатор, а предсказанный идентификатор преобразуется в китайский во время предсказания.

Создайте новый файл word_token.py и создайте класс WordToken. Функция load отвечает за загрузку образцов и создание словарей word2id_dict и id2word_dict. Функция word2id отвечает за преобразование слов в идентификаторы, а id2word отвечает за преобразование идентификаторов в слова:

# coding:utf-8
import sys
import jieba

class WordToken(object):
    def __init__(self):
        # 最小起始id号, 保留的用于表示特殊标记
        self.START_ID = 4
        self.word2id_dict = {}
        self.id2word_dict = {}

    def load_file_list(self, file_list):
        """
        加载样本文件列表,全部切词后统计词频,按词频由高到低排序后顺次编号
        并存到self.word2id_dict和self.id2word_dict中
        """
        words_count = {}
        for file in file_list:
            with open(file, 'r') as file_object:
                for line in file_object.readlines():
                    line = line.strip()
                    seg_list = jieba.cut(line)
                    for str in seg_list:
                        if str in words_count:
                            words_count[str] = words_count[str] + 1
                        else:
                            words_count[str] = 1

        sorted_list = [[v[1], v[0]] for v in words_count.items()]
        sorted_list.sort(reverse=True)
        for index, item in enumerate(sorted_list):
            word = item[1]
            self.word2id_dict[word] = self.START_ID + index
            self.id2word_dict[self.START_ID + index] = word

    def word2id(self, word):
        if not isinstance(word, unicode):
            print "Exception: error word not unicode"
            sys.exit(1)
        if word in self.word2id_dict:
            return self.word2id_dict[word]
        else:
            return None

    def id2word(self, id):
        id = int(id)
        if id in self.id2word_dict:
            return self.id2word_dict[id]
        else:
            return None

demo.py изменяет get_train_set:

def get_train_set():
    global num_encoder_symbols, num_decoder_symbols
    train_set = []
    with open('./samples/question', 'r') as question_file:
        with open('./samples/answer', 'r') as answer_file:
            while True:
                question = question_file.readline()
                answer = answer_file.readline()
                if question and answer:
                    question = question.strip()
                    answer = answer.strip()

                    question_id_list = get_id_list_from(question)
                    answer_id_list = get_id_list_from(answer)
                    answer_id_list.append(EOS_ID)
                    train_set.append([question_id_list, answer_id_list])
                else:
                    break
    return train_set

get_id_list_from реализация:

def get_id_list_from(sentence):
    sentence_id_list = []
    seg_list = jieba.cut(sentence)
    for str in seg_list:
        id = wordToken.word2id(str)
        if id:
            sentence_id_list.append(wordToken.word2id(str))
    return sentence_id_list

словоТокен:

import word_token
import jieba
wordToken = word_token.WordToken()

# 放在全局的位置,为了动态算出num_encoder_symbols和num_decoder_symbols
max_token_id = wordToken.load_file_list(['./samples/question', './samples/answer'])
num_encoder_symbols = max_token_id + 5
num_decoder_symbols = max_token_id + 5

тренировочный код:

        # 训练很多次迭代,每隔10次打印一次loss,可以看情况直接ctrl+c停止
        for step in xrange(100000):
            [loss_ret, _] = sess.run([loss, update], input_feed)
            if step % 10 == 0:
                print 'step=', step, 'loss=', loss_ret

                # 模型持久化
                saver.save(sess, './model/demo')

Модификация кода прогноза:

def predict():
    """
    预测过程
    """
    with tf.Session() as sess:
        encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver 
                        = get_model(feed_previous=True)
        saver.restore(sess, './model/demo')
        sys.stdout.write("> ")
        sys.stdout.flush()
        input_seq = sys.stdin.readline()
        while input_seq:
            input_seq = input_seq.strip()
            input_id_list = get_id_list_from(input_seq)
            if (len(input_id_list)):
                sample_encoder_inputs, sample_decoder_inputs, sample_target_weights 
                        = seq_to_encoder(' '.join([str(v) for v in input_id_list]))

                input_feed = {}
                for l in xrange(input_seq_len):
                    input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
                for l in xrange(output_seq_len):
                    input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
                    input_feed[target_weights[l].name] = sample_target_weights[l]
                input_feed[decoder_inputs[output_seq_len].name] 
                        = np.zeros([2], dtype=np.int32)

                # 预测输出
                outputs_seq = sess.run(outputs, input_feed)
                # 因为输出数据每一个是num_decoder_symbols维的
                # 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
                outputs_seq = [int(np.argmax(logit[0], axis=0)) for logit in outputs_seq]
                # 如果是结尾符,那么后面的语句就不输出了
                if EOS_ID in outputs_seq:
                    outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
                outputs_seq = [wordToken.id2word(v) for v in outputs_seq]
                print " ".join(outputs_seq)
            else:
                print "WARN:词汇不在服务区"

            sys.stdout.write("> ")
            sys.stdout.flush()
            input_seq = sys.stdin.readline()

Тренируйтесь с 1000 образцами диалогов, хранящимися в ['./samples/question', './samples/answer'], чтобы выходные данные потерь сходились ниже определенного уровня (например, 1,0):

python demo.py train

Когда он ниже 1.0, остановите вручную Ctrl+C и сохраняйте модель каждые 10 шагов.

Модель сходится очень медленно, установите скорость обучения 0,1. Во-первых, скорость обучения больше, и каждый раз, когда потери отскакивают (увеличиваются) на следующем шаге по сравнению с предыдущим шагом, старайтесь уменьшать скорость обучения. Вместо прямого использования learning_rate инициализируйте скорость обучения:

init_learning_rate = 1

Создайте переменную в get_model и инициализируйте ее с помощью init_learning_rate:

learning_rate = tf.Variable(float(init_learning_rate), trainable=False, dtype=tf.float32)

Создайте еще одну операцию, которая при необходимости снижает скорость обучения на 10:

learning_rate_decay_op = learning_rate.assign(learning_rate * 0.9)

Корректировка тренировочного кода:

        # 训练很多次迭代,每隔10次打印一次loss,可以看情况直接ctrl+c停止
        previous_losses = []
        for step in xrange(100000):
            [loss_ret, _] = sess.run([loss, update], input_feed)
            if step % 10 == 0:
                print 'step=', step, 'loss=', 
                        loss_ret, 'learning_rate=', learning_rate.eval()

                if loss_ret > max(previous_losses[-5:]):
                    sess.run(learning_rate_decay_op)
                previous_losses.append(loss_ret)

                # 模型持久化
                saver.save(sess, './model/demo')

Обучение может сходиться быстро.

использованная литература http://colah.github.io/posts/2015-08-Понимание-LSTMs/ http://suriyadeepan.github.io/2016-06-28-easy-seq2seq/ http://www.wildml.com/2016/01/внимание-и-память-в-глубоком-обучении-и-нлп/ https://arxiv.org/abs/1406.1078 https://arxiv.org/abs/1409.3215 https://arxiv.org/abs/1409.0473

Загрузка полного количества сэмплов, обучение с большим количеством сэмплов, память не выдерживает, и всегда Недостаточно памяти. Метод заключается в изменении полной загрузки образцов на пакетную загрузку.Независимо от того, насколько велик размер образца, память не будет увеличиваться бесконечно.

https://github.com/warmheartli/ChatBotCourse/tree/master/chatbotv5

Размер выборки увеличивается, а память растет. Размер выборки достигает 10 000, а использование памяти достигает 10G. Полная выборка загружается в память на каждой итерации, и модель обновляется после обучения за один раз. Большой словарный запас большой, модель большая, и памяти больше.

Оптимизация. Измените образцы полной загрузки на пакетную загрузку и измените функцию train().

    # 训练很多次迭代,每隔10次打印一次loss,可以看情况直接ctrl+c停止
    previous_losses = []
    for step in xrange(20000):
        sample_encoder_inputs, sample_decoder_inputs, sample_target_weights = get_samples(train_set, 1000)
        input_feed = {}
        for l in xrange(input_seq_len):
            input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
        for l in xrange(output_seq_len):
            input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
            input_feed[target_weights[l].name] = sample_target_weights[l]
        input_feed[decoder_inputs[output_seq_len].name] = np.zeros([len(sample_decoder_inputs[0])], dtype=np.int32)
        [loss_ret, _] = sess.run([loss, update], input_feed)
        if step % 10 == 0:
            print 'step=', step, 'loss=', loss_ret, 'learning_rate=', learning_rate.eval()

            if len(previous_losses) > 5 and loss_ret > max(previous_losses[-5:]):
                sess.run(learning_rate_decay_op)
            previous_losses.append(loss_ret)

            # 模型持久化
            saver.save(sess, './model/demo')

get_samples(train_set, 1000) Получить образцы партиями, 1000 — это размер выборки для каждого сбора данных:

    if batch_num >= len(train_set):
        batch_train_set = train_set
    else:
        random_start = random.randint(0, len(train_set)-batch_num)
        batch_train_set = train_set[random_start:random_start+batch_num]
    for sample in batch_train_set:
        raw_encoder_input.append([PAD_ID] * (input_seq_len - len(sample[0])) + sample[0])
        raw_decoder_input.append([GO_ID] + sample[1] + [PAD_ID] * (output_seq_len - len(sample[1]) - 1))

Каждый раз берется 1000 последовательных выборок в случайных позициях полной выборки.

Минимальное ограничение частоты слов при загрузке образца словаря:

    def load_file_list(self, file_list, min_freq):
    ......
        for index, item in enumerate(sorted_list):
            word = item[1]
            if item[0] < min_freq:
                break
            self.word2id_dict[word] = self.START_ID + index
            self.id2word_dict[self.START_ID + index] = word
        return index

https://github.com/warmheartli/ChatBotCourse/tree/master/chatbotv5

Использованная литература: «Обработка естественного языка Python» «Основное руководство NLTK по созданию приложений машинного обучения с помощью библиотек NLTK и Python» http://www.shareditor.com/blogshow?blogId=136 http://www.shareditor.com/blogshow?blogId=137

Добро пожаловать, чтобы порекомендовать вакансии по машинному обучению в Шанхае, мой WeChat: qingxingfengzi