学习笔记CB014:TensorFlow seq2seq模型步步进阶

简介: 神经网络。《Make Your Own Neural Network》,用非常通俗易懂描述讲解人工神经网络原理用代码实现,试验效果非常好。 循环神经网络和LSTM。Christopher Olah http://colah.github.io/posts/2015-08-Understanding-LSTMs/ 。

神经网络。《Make Your Own Neural Network》,用非常通俗易懂描述讲解人工神经网络原理用代码实现,试验效果非常好。

循环神经网络和LSTM。Christopher Olah http://colah.github.io/posts/2015-08-Understanding-LSTMs/

seq2seq模型基于循环神经网络序列到序列模型,语言翻译、自动问答等序列到序列场景,都可用seq2seq模型,用seq2seq实现聊天机器人的原理 http://suriyadeepan.github.io/2016-06-28-easy-seq2seq/

attention模型(注意力模型)是解决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是list,list每一项是1D Tensor,Tensor shape是[batch_size],Tensor每一项是整数,类似:

[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个array,表示一句话长度是5个词。每个array有4个数,表示batch是4,一共4个样本。第一个样本是[[0],[0],[8],[7],[6]],第二个样本是[[0],[0],[3],[8],[2]],数字区分不同词id,一般通过统计得出,一个id表示一个词。

参数decoder_inputs和encoder_inputs一样结构。

参数cell是tf.nn.rnn_cell.RNNCell类型循环神经网络单元,可用tf.contrib.rnn.BasicLSTMCell、tf.contrib.rnn.GRUCell。

参数num_encoder_symbols是整数,表示encoder_inputs整数词id数目。

num_decoder_symbols表示decoder_inputs中整数词id数目。

embedding_size表示内部word embedding转成几维向量,需要和RNNCell size大小相等。

num_heads表示attention_states抽头数量。

output_projection是(W, B)结构tuple,W是shape [output_size x num_decoder_symbols] weight矩阵,B是shape [num_decoder_symbols] 偏置向量,每个RNNCell输出经过WX+B映射成num_decoder_symbols维向量,向量值表示任意一个decoder_symbol可能性,softmax。

feed_previous表示decoder_inputs是直接提供训练数据输入,还是用前一个RNNCell输出映射,如果feed_previous为True,用前一个RNNCell输出,经过WX+B映射。

dtype是RNN状态数据类型,默认是tf.float32。

scope是子图命名,默认是“embedding_attention_seq2seq”。

initial_state_attention表示是否初始化attentions,默认为否,表示全初始化为0。返回值是(outputs, state)结构tuple,outputs是长度为句子长度(词数,与encoder_inputs list长度一样)list,list每一项是一个2D tf.float32类型 Tensor,第一维度是样本数,比如4个样本有四组Tensor,每个Tensor长度是embedding_size。outputs描述548个浮点数,5是句子长度,4是样本数,8是词向量维数。

返回state,num_layers个LSTMStateTuple组成大tuple,num_layers是初始化cell参数,表示神经网络单元有几层,一个由3层LSTM神经元组成encoder-decoder多层循环神经网络。encoder_inputs输入encoder第一层LSTM神经元,神经元output传给第二层LSTM神经元,第二层output再传给第三层,encoder第一层输出state传给decoder第一层LSTM神经元,依次类推。

LSTMStateTuple结构,由两个Tensor组成tuple,第一个tensor命名为c,由4个8维向量组成(4是batch, 8是state_size词向量维度), 第二个tensor命名为h,同样由4个8维向量组成。

c是传给下一个时序存储数据,h是隐藏的输出。

tensorflow代码实现:

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训练,返回state一般用不到。

构造输入参数训练一个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

构造模型,tensorflow运行过程是先构造图,再塞数据计算,构建模型过程实际上是构建一张图。
首先创建encoder_inputs和decoder_inputs的placeholder(占位符):

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)))

创建一个记忆单元数目为size=8的LSTM神经元结构:

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

训练奇数序列最大数值是输入最大10输出最大16

num_encoder_symbols = 10
num_decoder_symbols = 16

把参数传入embedding_attention_seq2seq获取output

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

构造运行时session,填入样本数据:

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

输出outputs是由5个array组成list(5是序列长度),每个array由两个size是16 list组成(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 shape [batch * num_decoder_symbols] Tensor组成list,batch是2,num_decoder_symbols是16,组成list Tensor 个数是output_seq_len。
targets是和logits一样长度(output_seq_len) list,list每一项是整数组成1D Tensor,每个Tensor shape是[batch],数据类型是tf.int32,和decoder_inputs[1:] W、X、Y、Z、EOS结构一样。
weights和targets结构一样,数据类型是tf.float32。

计算加权交叉熵损失,weights需要初始化占位符:

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)

targets长度比decoder_inputs少一个,长度保持一致,decoder_inputs的初始化长度加1。
计算加权交叉熵损失,有意义数权重大,无意义权重小,targets有值赋值为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()

训练模型,经过多轮计算让loss变得很小,运用梯度下降更新参数。tensorflow提供梯度下降类:
https://www.tensorflow.org/api_docs/python/tf/train/GradientDescentOptimizer

Class 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
)

关键参数loss是传入误差值,返回值是(gradient, variable)组成list。
更新参数方法:

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

grads_and_vars是compute_gradients返回值。
根据loss计算梯度更新参数方法:

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

main函数增加循环迭代:

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')

模型存储到./model目录下以demo开头文件,加载先调用:

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

预测候,原则上不能有decoder_inputs输入,执行时decoder_inputs取前一个时序输出,embedding_attention_seq2seq feed_previous参数,若为True则decoder每一步输入都用前一步输出来填充。

get_model传递参数区分训练和预测是不同feed_previous配置,预测时main函数也是不同,分开两个函数分别做train和predict。

# 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计算,继续改进predict,手工输入一串数字(只有encoder部分)。

实现从输入空格分隔数字id串,转成预测用encoder、decoder、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

然后我们改写predict函数如下:

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 predict。

设置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]]
            ]
……

迭代次数扩大到10000次。

输入样本,预测效果非常好,换成其他输入,还是在样本输出找最相近结果作预测结果,不会思考,没有智能,所模型更适合做分类,不适合做推理。

训练时把中文词汇转成id号,预测时把预测id转成中文。

新建word_token.py文件,并建一个WordToken类,load函数负责加载样本,生成word2id_dict和id2word_dict词典,word2id函数负责将词汇转成id,id2word负责将id转成词汇:

# 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

wordToken:

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()

用存储在['./samples/question', './samples/answer']1000个对话样本训练,使loss输出收敛到一定程度(比如1.0)以下:

python demo.py train

到1.0以下后手工ctrl+c停止,每隔10个step都store一次模型。

模型收敛非常慢,设置学习率是0.1。首先学习率大一些,每当下一步loss和上一步相比反弹(反而增大)时再尝试降低学习率。不再直接用learning_rate,初始化一个学习率:

init_learning_rate = 1

get_model中创建一个变量,用init_learning_rate初始化:

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

再创建一个操作,在适当时候把学习率打9折:

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-Understanding-LSTMs/
http://suriyadeepan.github.io/2016-06-28-easy-seq2seq/
http://www.wildml.com/2016/01/attention-and-memory-in-deep-learning-and-nlp/
https://arxiv.org/abs/1406.1078
https://arxiv.org/abs/1409.3215
https://arxiv.org/abs/1409.0473

样本全量加载,用大量样本训练,内存撑不住,总是Out of memory。方法是把全量加载样本改成批量加载,样本量再大,内存也不会无限增加。

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

样本量加大内存增长,样本量达到万级别,内存占用达到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

欢迎推荐上海机器学习工作机会

目录
相关文章
|
30天前
|
机器学习/深度学习 TensorFlow 算法框架/工具
深度学习之格式转换笔记(三):keras(.hdf5)模型转TensorFlow(.pb) 转TensorRT(.uff)格式
将Keras训练好的.hdf5模型转换为TensorFlow的.pb模型,然后再转换为TensorRT支持的.uff格式,并提供了转换代码和测试步骤。
68 3
深度学习之格式转换笔记(三):keras(.hdf5)模型转TensorFlow(.pb) 转TensorRT(.uff)格式
|
30天前
|
缓存 TensorFlow 算法框架/工具
TensorFlow学习笔记(一): tf.Variable() 和tf.get_variable()详解
这篇文章详细介绍了TensorFlow中`tf.Variable()`和`tf.get_variable()`的使用方法、参数含义以及它们之间的区别。
59 0
|
14天前
|
机器学习/深度学习 人工智能 算法
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
车辆车型识别,使用Python作为主要编程语言,通过收集多种车辆车型图像数据集,然后基于TensorFlow搭建卷积网络算法模型,并对数据集进行训练,最后得到一个识别精度较高的模型文件。再基于Django搭建web网页端操作界面,实现用户上传一张车辆图片识别其类型。
54 0
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
|
2月前
|
机器学习/深度学习 人工智能 算法
鸟类识别系统Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+ResNet50算法模型+图像识别
鸟类识别系统。本系统采用Python作为主要开发语言,通过使用加利福利亚大学开源的200种鸟类图像作为数据集。使用TensorFlow搭建ResNet50卷积神经网络算法模型,然后进行模型的迭代训练,得到一个识别精度较高的模型,然后在保存为本地的H5格式文件。在使用Django开发Web网页端操作界面,实现用户上传一张鸟类图像,识别其名称。
104 12
鸟类识别系统Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+ResNet50算法模型+图像识别
|
30天前
|
TensorFlow 算法框架/工具
Tensorflow学习笔记(二):各种tf类型的函数用法集合
这篇文章总结了TensorFlow中各种函数的用法,包括创建张量、设备管理、数据类型转换、随机数生成等基础知识。
28 0
|
30天前
|
机器学习/深度学习 移动开发 TensorFlow
深度学习之格式转换笔记(四):Keras(.h5)模型转化为TensorFlow(.pb)模型
本文介绍了如何使用Python脚本将Keras模型转换为TensorFlow的.pb格式模型,包括加载模型、重命名输出节点和量化等步骤,以便在TensorFlow中进行部署和推理。
68 0
|
3月前
|
API UED 开发者
如何在Uno Platform中轻松实现流畅动画效果——从基础到优化,全方位打造用户友好的动态交互体验!
【8月更文挑战第31天】在开发跨平台应用时,确保用户界面流畅且具吸引力至关重要。Uno Platform 作为多端统一的开发框架,不仅支持跨系统应用开发,还能通过优化实现流畅动画,增强用户体验。本文探讨了Uno Platform中实现流畅动画的多个方面,包括动画基础、性能优化、实践技巧及问题排查,帮助开发者掌握具体优化策略,提升应用质量与用户满意度。通过合理利用故事板、减少布局复杂性、使用硬件加速等技术,结合异步方法与预设缓存技巧,开发者能够创建美观且流畅的动画效果。
77 0
|
3月前
|
C# 开发者 前端开发
揭秘混合开发新趋势:Uno Platform携手Blazor,教你一步到位实现跨平台应用,代码复用不再是梦!
【8月更文挑战第31天】随着前端技术的发展,混合开发日益受到开发者青睐。本文详述了如何结合.NET生态下的两大框架——Uno Platform与Blazor,进行高效混合开发。Uno Platform基于WebAssembly和WebGL技术,支持跨平台应用构建;Blazor则让C#成为可能的前端开发语言,实现了客户端与服务器端逻辑共享。二者结合不仅提升了代码复用率与跨平台能力,还简化了项目维护并增强了Web应用性能。文中提供了从环境搭建到示例代码的具体步骤,并展示了如何创建一个简单的计数器应用,帮助读者快速上手混合开发。
75 0
|
3月前
|
开发者 算法 虚拟化
惊爆!Uno Platform 调试与性能分析终极攻略,从工具运用到代码优化,带你攻克开发难题成就完美应用
【8月更文挑战第31天】在 Uno Platform 中,调试可通过 Visual Studio 设置断点和逐步执行代码实现,同时浏览器开发者工具有助于 Web 版本调试。性能分析则利用 Visual Studio 的性能分析器检查 CPU 和内存使用情况,还可通过记录时间戳进行简单分析。优化性能涉及代码逻辑优化、资源管理和用户界面简化,综合利用平台提供的工具和技术,确保应用高效稳定运行。
78 0
|
2月前
|
机器学习/深度学习 数据挖掘 TensorFlow
解锁Python数据分析新技能,TensorFlow&PyTorch双引擎驱动深度学习实战盛宴
在数据驱动时代,Python凭借简洁的语法和强大的库支持,成为数据分析与机器学习的首选语言。Pandas和NumPy是Python数据分析的基础,前者提供高效的数据处理工具,后者则支持科学计算。TensorFlow与PyTorch作为深度学习领域的两大框架,助力数据科学家构建复杂神经网络,挖掘数据深层价值。通过Python打下的坚实基础,结合TensorFlow和PyTorch的强大功能,我们能在数据科学领域探索无限可能,解决复杂问题并推动科研进步。
58 0
下一篇
无影云桌面