tensorflow程式碼全解析 -3- seq2seq 自動生成文字
模型概述
序列建模seq2seq,給定一個序列A,模型生產另一個序列B,然後模型再由序列B生成C,以此一直持續下去。
基本工作流程如下:
序列A中的每一個單詞通過word_embedding操作以後,作為input進seq2seq入模型,模型生成同樣維度的序列A_out
訓練的時候,模型的輸出序列A_out與序列B之間的交叉熵作為模型的目標函式,採用clip控制過的梯度進行收斂;
生成的時候,首先給定一個種子序列作為模型輸入,初始化模型,讓模型可以自迴圈,並生產指定長度的序列
模型資料流
第一部分:生成資料
資料來源 JayLyrics.txt
dataGenerator.py
定義引數
log_dir = './logs'
seq_length = 20 # 每一個序列的長度
batch_size = 32 #每一個batch的長度
datafiles = 'JayLyrics.txt'
讀取文字,進行詞頻統計,將文字與數字進行一一對應,方便進行處理
with open(datafiles, encoding='utf-8') as f:
data = f.read()
total_len = len(data)
words = list(set(data))
words.sort()
vocab_size = len(words)
char2id_dict = {w: i for i, w in enumerate(words)}
id2char_dict = {i: w for i, w in enumerate(words)}
進行這一步之後可以得到
將上面的資料對應表進行儲存,注意tsv與csv格式不同,是以tab進行分隔的
_pointer = 0
def char2id( c):
return char2id_dict[c]
def id2char(id):
return id2char_dict[id]
metadata = 'metadata2.tsv'
def save_metadata(file):
with open(file, 'w' ) as f:
f.write('id\tchar\n')
for i in range(vocab_size):
c = id2char(i)
f.write('{}\t{}\n'.format(i, c))
save_metadata(metadata)
下面進行關鍵的將原始文字轉換為輸入資料序列和輸入標籤
我們可以看到,文字資料與訓練資料集是如何進行轉換的。
def next_batch():
_pointer = 0
x_batches = []
y_batches = []
for i in range(batch_size):
if _pointer + seq_length + 1 >= total_len:
_pointer = 0
bx = data[_pointer: _pointer + seq_length]
by = data[_pointer +
1: _pointer + seq_length + 1]
_pointer += seq_length # update pointer position
# convert to idss
bx = [char2id(c) for c in bx]
by = [char2id(c) for c in by]
x_batches.append(bx)
y_batches.append(by)
return x_batches, y_batches
data[_pointer: _pointer + seq_length]
Out[22]: '作詞:黃俊郎 \n作曲:周杰倫\n編曲:黃雨'
# 以上是 bx
data[_pointer +1: _pointer + seq_length + 1]
Out[23]: '詞:黃俊郎 \n作曲:周杰倫\n編曲:黃雨勛'
# 以上是 by
再將上面的資料轉化為數字
eg = next_batch()
# 一個batch 就這樣生成了
#應注意到輸入資料是一個32*20維的資料矩陣
#輸入標籤同樣是一個32*20維的資料矩陣
[char2id(c) for c in bx]
第二部分 構建模型邏輯
構建seq2seq模型
3層LSTMCell 每一層100個神經元
state_size = 100
num_layers = 3
#定義神經網路
cell = rnn_cell.BasicLSTMCell(state_size)
cell = rnn_cell.MultiRNNCell([cell] * num_layers)
# 神經元的初始狀態,將神經元設定為全零狀態
initial_state = cell.zero_state(
batch_size, tf.float32)
將輸入資料傳遞的seq2seq模型中
採用tf.nn.dynamic_rnn
inputs: The RNN inputs.
Tensor of shape: [batch_size, max_time, …]dynamic_rnn 返回
outputs: The RNN outputTensor
.
If time_major == False (default), this will be aTensor
shaped:
[batch_size, max_time, cell.output_size]
.
state: The final state. Ifcell.state_size
is an int, this
will be shaped[batch_size, cell.state_size]
.
with tf.variable_scope('rnnlm'):
with tf.device("/cpu:0"):
embedding = tf.get_variable(
'embedding', [vocab_size, state_size])
inputs = tf.nn.embedding_lookup(embedding, input_data)
# 將輸入資料傳遞的seq2seq模型中
# 採用tf.nn.dynamic_rnn
# inputs: The RNN inputs.
# Tensor of shape: [batch_size, max_time, ...]
outputs, last_state = tf.nn.dynamic_rnn(cell, inputs, initial_state=initial_state)
embedding input 的詳細解釋 下面是看到的比較好的解釋
我們預處理了資料之後得到的是一個二維array,每個位置的元素表示這個word在vocabulary中的index。但是傳入graph的資料不能講word用index來表示,這樣詞和詞之間的關係就沒法刻畫了。我們需要將word用dense vector表示,這也就是廣為人知的word embedding。paper中並沒有使用預訓練的word embedding,所有的embedding都是隨機初始化,然後在訓練過程中不斷更新embedding矩陣的值。
123
with tf.device("/cpu:0"):
embedding = tf.get_variable("embedding", vocab_size, state_size])
inputs = tf.nn.embedding_lookup(embedding, self._input_data)
首先要明確幾點:
既然我們要在訓練過程中不斷更新embedding矩陣,那麼embedding必須是tf.Variable並且trainable=True(default)
目前tensorflow對於lookup embedding的操作只能再cpu上進行
embedding矩陣的大小是多少:每個word都需要有對應的embedding vector,總共就是vocab_size那麼多個embedding,每個word embed成多少維的vector呢?因為我們input embedding後的結果就直接輸入給了第一層cell,剛才我們知道cell的hidden units size,因此這個embedding dim要和hidden units size對應上(這也才能和內部的各種門的W和b完美相乘)。因此,我們就確定下來
embedding matrix shape=[vocab_size, hidden_units_size]
最後生成真正的inputs節點,也就是從embedding_lookup之後得到的結果,這個tensor的shape=[batch_size, num_stemps, size]
第三部分 構建模型損失函式和收斂方法
先定義引數w(100*2636)和b(2636)
再將output 展開成[-1*state_size]維即是[-1*100]
再用(output*w)+b獲得輸出序列logits
下面便可以使用sequence_loss_by_example將模型輸出logits和訓練標籤targets計算序列交叉熵
with tf.name_scope('model'):
with tf.variable_scope('rnnlm'):
w = tf.get_variable( 'softmax_w', [state_size, vocab_size])
b = tf.get_variable('softmax_b', [vocab_size])
with tf.name_scope('loss'):
output = tf.reshape(outputs, [-1, state_size])
logits = tf.matmul(output, w) + b
probs = tf.nn.softmax(logits)
last_state = last_state
targets = tf.reshape(target_data, [-1])
# 將targets 展平 維度從(32*20)轉化為640
# pass '[-1]' to flatten 't'
#reshape(t, [-1]) ==> [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6]
loss = seq2seq.sequence_loss_by_example([logits],
[targets],
[tf.ones_like(targets, dtype=tf.float32)])
cost = tf.reduce_sum(loss) / batch_size
#下面採用scalar 是用來在tensorboard 顯示資料的
tf.summary.scalar('loss', cost)
第四部分 優化器設定
首先 定義lr 是不可訓練的
關鍵地方 clip_by_global_norm的作用
設定梯度的最大範數
Gradient Clipping的方法,控制梯度的最大範數。
可以防止梯度爆炸的問題,如果梯度不加限制,則可能因為迭代中梯度過大導致訓練難以收斂。
optimizer.apply_gradients則將前面clip過的梯度應用到所有可以訓練的tvars上
with tf.name_scope('optimize'):
lr = tf.placeholder(tf.float32, [])
tf.summary.scalar('learning_rate', lr)
optimizer = tf.train.AdamOptimizer(lr)
#獲取全部可以訓練的引數tvars
tvars = tf.trainable_variables()
# 提前計算梯度
grads = tf.gradients(cost, tvars)
#顯示在tensorboard
for g in grads:
tf.summary.histogram(g.name, g)
# 由它們的範數之和之比求多個張量的值
grads, _ = tf.clip_by_global_norm(grads, grad_clip)
# 將前面clip過的梯度應用到可訓練的引數上
train_op = optimizer.apply_gradients(zip(grads, tvars))
merged_op = tf.summary.merge_all()
第五部分 訓練
啟動模型,設定tensorboard,匯入資料,記錄資料
with tf.Session() as sess:
#啟動模型
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver()
writer = tf.summary.FileWriter(log_dir, sess.graph)
# 設定tensorboard
# Add embedding tensorboard visualization. Need tensorflow version
# >= 0.12.0RC0
config = projector.ProjectorConfig()
embed = config.embeddings.add()
embed.tensor_name = 'rnnlm/embedding:0'
embed.metadata_path = metadata
projector.visualize_embeddings(writer, config)
# 匯入資料
max_iter = n_epoch * \
(data.total_len // seq_length) // batch_size
for i in range(max_iter):
learning_rate = learning_rate * \
(decay_rate ** (i // decay_steps))
x_batch, y_batch = data.next_batch()
feed_dict = {model.input_data: x_batch,
model.target_data: y_batch, model.lr: learning_rate}
train_loss, summary, _, _ = sess.run([model.cost, model.merged_op, model.last_state, model.train_op],
feed_dict)
# 記錄資料
if i % 10 == 0:
writer.add_summary(summary, global_step=i)
print('Step:{}/{}, training_loss:{:4f}'.format(i,
max_iter, train_loss))
if i % 2000 == 0 or (i + 1) == max_iter:
saver.save(sess, os.path.join(
log_dir, 'lyrics_model.ckpt'), global_step=i)
第五部分 生成文字
首先從儲存的模型中取出引數,初始化模型
再設定種子
將種子進行處理
初始化模型
按照設定的生成數量生成文字
#首先從儲存的模型中取出引數,初始化模型
saver = tf.train.Saver()
with tf.Session() as sess:
ckpt = tf.train.latest_checkpoint(args.log_dir)
print(ckpt)
saver.restore(sess, ckpt)
# 再設定種子
# initial phrase to warm RNN
prime = u'你要離開我知道很簡單'
state = sess.run(cell.zero_state(1, tf.float32))
# 將種子進行處理
# 初始化模型
for word in prime[:-1]:
x = np.zeros((1, 1))
x[0, 0] = char2id(word)
feed = {input_data: x, initial_state: state}
state = sess.run(last_state, feed)
# 按照設定的生成數量生成文字
word = prime[-1]
lyrics = prime
for i in range(args.gen_num):
x = np.zeros([1, 1])
x[0, 0] = char2id(word)
feed_dict = {input_data: x, initial_state: state}
probs, state = sess.run([probs, last_state], feed_dict)
p = probs[0]
word = id2char(np.argmax(p))
print(word, end='')
sys.stdout.flush()
time.sleep(0.05)
lyrics += word
return lyrics
程式碼還是很複雜很複雜的,看了好幾天,還是有些不明白的,
有些只能等以後再慢慢專研,現在主幹是抓住了。