After training a skip-gram model in 5_word2vec.ipynb
, the goal of this notebook is to train a LSTM character model over Text8 data.
In [3]:
# These are all the modules we'll be using later. Make sure you can import them
# before proceeding further.
from __future__ import print_function
import os
import numpy as np
import random
import string
import tensorflow as tf
import zipfile
from six.moves import range
from six.moves.urllib.request import urlretrieve
In [4]:
url = 'http://mattmahoney.net/dc/'
def maybe_download(filename, expected_bytes):
"""Download a file if not present, and make sure it's the right size."""
if not os.path.exists(filename):
filename, _ = urlretrieve(url + filename, filename)
statinfo = os.stat(filename)
if statinfo.st_size == expected_bytes:
print('Found and verified %s' % filename)
else:
print(statinfo.st_size)
raise Exception(
'Failed to verify ' + filename + '. Can you get to it with a browser?')
return filename
filename = maybe_download('text8.zip', 31344016)
In [5]:
def read_data(filename):
f = zipfile.ZipFile(filename)
for name in f.namelist():
return tf.compat.as_str(f.read(name))
f.close()
text = read_data(filename)
print('Data size %d' % len(text))
Create a small validation set.
In [6]:
valid_size = 1000
valid_text = text[:valid_size]
train_text = text[valid_size:]
train_size = len(train_text)
print(train_size, train_text[:64])
print(valid_size, valid_text[:64])
Utility functions to map characters to vocabulary IDs and back.
In [7]:
vocabulary_size = len(string.ascii_lowercase) + 1 # [a-z] + ' '
first_letter = ord(string.ascii_lowercase[0])
def char2id(char):
if char in string.ascii_lowercase:
return ord(char) - first_letter + 1
elif char == ' ':
return 0
else:
print('Unexpected character: %s' % char)
return 0
def id2char(dictid):
if dictid > 0:
return chr(dictid + first_letter - 1)
else:
return ' '
print(char2id('a'), char2id('z'), char2id(' '), char2id('ï'))
print(id2char(1), id2char(26), id2char(0))
Function to generate a training batch for the LSTM model.
numpy.argmax(a, axis=None, out=None) Returns the indices of the maximum values along an axis.
In [8]:
batch_size=64 #how many parts the dataset is divided into
# if len(dataset)=20, batch_size=5, then len(words_in_each_batch)=4
# 'batches' has (num_unrolling+1) many 'batch'. For each 'batch', it has batch_size many words.
# the first 'batch' has the first word from 4 parts
# the second 'batch' has the second word from 4 parts
# the (num_unrolling+1)-th 'batch' has the (num_unrolling+1)-th word from 4 parts.
# parts are the addresses given by dataset and batch_size.
num_unrollings=10
class BatchGenerator(object):
def __init__(self, text, batch_size, num_unrollings):
self._text = text
self._text_size = len(text)
self._batch_size = batch_size
self._num_unrollings = num_unrollings
num_segment = self._text_size // batch_size
self._cursor = [ offset * num_segment for offset in range(batch_size)]
self._last_batch = self._next_batch()
def _next_batch(self):
"""Generate a single batch from the current cursor position in the data."""
batch = np.zeros(shape=(self._batch_size, vocabulary_size), dtype=np.float)
for b in range(self._batch_size):
batch[b, char2id(self._text[self._cursor[b]])] = 1.0
self._cursor[b] = (self._cursor[b] + 1) % self._text_size
return batch
#i-th batch has the i-th word from each part. number_of_parts = num_segment
def next(self):
"""Generate the next array of batches from the data. The array consists of
the last batch of the previous array, followed by num_unrollings new ones.
"""
batches = [self._last_batch] # batches initializes with one batch.
for step in range(self._num_unrollings): # add _num_unrollings(=10) batches then.
batches.append(self._next_batch())
self._last_batch = batches[-1]
return batches
def characters(probabilities):
#probabilities.shape=(64,27) or (1,27)
"""Turn a 1-hot encoding or a probability distribution over the possible
characters back into its (most likely) character representation."""
#print([id2char(c) for c in np.argmax(probabilities, 1)]) # length = 64
return [id2char(c) for c in np.argmax(probabilities, 1)] #the length of the list is 64
def batches2string(batches):
"""Convert a sequence of batches back into their (most likely) string
representation."""
#len(batches)=11
#batches[0].shape = (64,27)
s = [''] * batches[0].shape[0] #generate a list with batches[0].shape[0] many elements, all the elements are ''
for b in batches:
# b.shape = (64,27)
#print(characters(b),'\n----') # characters(b) shape is (64,27)
s = [''.join(x) for x in zip(s, characters(b))]
# len(s)=64, len(characters(b))=64
# s[i]=s[i]+characters(b)[i], recursively add character from b to each element of s.
# len(batches) = 11, so len(s[i])=11. There are 11 characters in one batch.
#zip(seq1,seq2,...)
#zip return a list of tuples, each tuple have one element from each sequence. The length of the list depends on the
#length of the shortest sequence.
return s
train_batches = BatchGenerator(train_text, batch_size, num_unrollings)
valid_batches = BatchGenerator(valid_text, 1, 1) # one part with (1+1) characters
batches = train_batches.next()
s = batches2string(batches)
print(len(s))
print(len(s[0]))
print(batches[0].shape)
print(len(batches))
print(len(batches[0]))
print(len(batches[0][0]))
print(s,'\n----')
print(batches2string(train_batches.next()),'\n----')
print(batches2string(valid_batches.next()),'\n----')
print(batches2string(valid_batches.next()),'\n----')
In [9]:
def logprob(predictions, labels):
"""Log-probability of the true labels in a predicted batch."""
predictions[predictions < 1e-10] = 1e-10
# predictions is a ndarray
# this method is numpy filter
# prediction.shape = (640,27)
# labels.shape = (640,27)
logp = np.sum(np.multiply(labels, -np.log(predictions))) / labels.shape[0]
# np.multiply is a element_wise multiplication
# element_wise_mutiply(the_row_of_predictions,the_row_of_labels)
return logp
def sample_distribution(distribution):
"""Sample one element from a distribution assumed to be an array of normalized
probabilities.
"""
#distribution is a list with 27 normalized elements
r = random.uniform(0, 1)
s = 0
for i in range(len(distribution)):
s += distribution[i]
if s >= r:
return i
return len(distribution) - 1
def sample(prediction):
"""Turn a (column) prediction into 1-hot encoded samples."""
p = np.zeros(shape=[1, vocabulary_size], dtype=np.float)
p[0, sample_distribution(prediction[0])] = 1.0
# return one-hot coded ndarray with the shape of (1,27)
return p
def random_distribution():
"""Generate a random column of probabilities."""
b = np.random.uniform(0.0, 1.0, size=[1, vocabulary_size])
#print((b/np.sum(b,1)).shape)
# return normalized random ndarray with the shape of (1,27)
return b/np.sum(b, 1)[:,None] #None?
rd = random_distribution()
print(rd)
print(sample(rd))
Simple LSTM Model.
In [10]:
num_nodes = 64
graph = tf.Graph()
with graph.as_default():
# Parameters:
# bias is added to each batch, the row of the tensor
# bias is the tensor with one row
# Input gate: input, previous output, and bias.
ix = tf.Variable(tf.truncated_normal([vocabulary_size, num_nodes], -0.1, 0.1))
im = tf.Variable(tf.truncated_normal([num_nodes, num_nodes], -0.1, 0.1))
ib = tf.Variable(tf.zeros([1, num_nodes]))
# Forget gate: input, previous output, and bias.
fx = tf.Variable(tf.truncated_normal([vocabulary_size, num_nodes], -0.1, 0.1))
fm = tf.Variable(tf.truncated_normal([num_nodes, num_nodes], -0.1, 0.1))
fb = tf.Variable(tf.zeros([1, num_nodes]))
# Memory cell: input, state and bias.
cx = tf.Variable(tf.truncated_normal([vocabulary_size, num_nodes], -0.1, 0.1))
cm = tf.Variable(tf.truncated_normal([num_nodes, num_nodes], -0.1, 0.1))
cb = tf.Variable(tf.zeros([1, num_nodes]))
# Output gate: input, previous output, and bias.
ox = tf.Variable(tf.truncated_normal([vocabulary_size, num_nodes], -0.1, 0.1))
om = tf.Variable(tf.truncated_normal([num_nodes, num_nodes], -0.1, 0.1))
ob = tf.Variable(tf.zeros([1, num_nodes]))
# Variables saving state across unrollings.
saved_output = tf.Variable(tf.zeros([batch_size, num_nodes]), trainable=False)
saved_state = tf.Variable(tf.zeros([batch_size, num_nodes]), trainable=False)
# Classifier weights and biases.
w = tf.Variable(tf.truncated_normal([num_nodes, vocabulary_size], -0.1, 0.1))
b = tf.Variable(tf.zeros([vocabulary_size])) # add the same biases to each batch
# Definition of the cell computation.
def lstm_cell(i, o, state):
"""Create a LSTM cell."""
# i is the input of the current cell, a tensor with the shape of (batch_size,27)
# o is the output of the current cell, a tensor with the shape of (batch_size, num_nodes)
# state is the input state of the current cell, or output state of the previous cell,
# state is a tensor with the shape of (batch_size, num_nodes)
input_gate = tf.sigmoid(tf.matmul(i, ix) + tf.matmul(o, im) + ib)
forget_gate = tf.sigmoid(tf.matmul(i, fx) + tf.matmul(o, fm) + fb)
update = tf.matmul(i, cx) + tf.matmul(o, cm) + cb
state = forget_gate * state + input_gate * tf.tanh(update)
output_gate = tf.sigmoid(tf.matmul(i, ox) + tf.matmul(o, om) + ob)
o = output_gate * tf.tanh(state)
return o, state
# Input data.
train_data = list()
for _ in range(num_unrollings + 1):
train_data.append(
tf.placeholder(tf.float32, shape=[batch_size,vocabulary_size]))
train_inputs = train_data[:num_unrollings]
train_labels = train_data[1:] # labels are inputs shifted by one time step.
# Unrolled LSTM loop.
outputs = list()
output = saved_output
state = saved_state
for i in train_inputs:
# i is input, a tensor with the shape of (batch_size,27)
output, state = lstm_cell(i, output, state)
outputs.append(output)
# eventually, outputs has 10 predictioned words for each batch. There are 64 batches inside.
# State saving across unrollings.
with tf.control_dependencies([saved_output.assign(output),
saved_state.assign(state)]):
# Classifier.
#print(len(outputs))
#print(outputs[0])
#print(tf.concat(0, outputs)) # concatenate 10 words together, shape = (640,27)
logits = tf.nn.xw_plus_b(tf.concat(0, outputs), w, b)
# help(tf.nn.xw_plus_b)
# so the number of total predicted words is 64*10 = 640
loss = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(
logits, tf.concat(0, train_labels)))
# Optimizer.
global_step = tf.Variable(0)
learning_rate = tf.train.exponential_decay(10.0, global_step, 5000, 0.1, staircase=True)
#help(tf.train.exponential_decay)
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
# class tf.train.GradientDescentOptimizer(...), help(tf.train.GradientDescentOptimizer)
gradients, v = zip(*optimizer.compute_gradients(loss))
# zip(*) unpacking the argument lists
# help(tf.train.GradientDescentOptimizer.compute_gradients)
# returns a list of (gradient, variable) pairs.
gradients, _ = tf.clip_by_global_norm(gradients, 1.25) # modify the gradients
optimizer = optimizer.apply_gradients(zip(gradients, v), global_step=global_step)
# Predictions.
#print(logits) #has shape of (640,27)
train_prediction = tf.nn.softmax(logits)
#print(train_prediction) #has shape of (640,27)
# Sampling and validation eval: batch 1, no unrolling.
sample_input = tf.placeholder(tf.float32, shape=[1, vocabulary_size])
saved_sample_output = tf.Variable(tf.zeros([1, num_nodes]))
saved_sample_state = tf.Variable(tf.zeros([1, num_nodes]))
reset_sample_state = tf.group(
saved_sample_output.assign(tf.zeros([1, num_nodes])),
saved_sample_state.assign(tf.zeros([1, num_nodes])))
#tf.group & group.run?
sample_output, sample_state = lstm_cell(
sample_input, saved_sample_output, saved_sample_state)
with tf.control_dependencies([saved_sample_output.assign(sample_output),
saved_sample_state.assign(sample_state)]):
sample_prediction = tf.nn.softmax(tf.nn.xw_plus_b(sample_output, w, b))
In [58]:
num_steps = 7001
summary_frequency = 100
with tf.Session(graph=graph) as session:
tf.initialize_all_variables().run()
print('Initialized')
mean_loss = 0
for step in range(num_steps):
batches = train_batches.next()
feed_dict = dict()
for i in range(num_unrollings + 1):
feed_dict[train_data[i]] = batches[i]
_, l, predictions, lr = session.run(
[optimizer, loss, train_prediction, learning_rate], feed_dict=feed_dict)
mean_loss += l
if step % summary_frequency == 0:
if step > 0:
mean_loss = mean_loss / summary_frequency
# The mean loss is an estimate of the loss over the last few batches.
print(
'Average loss at step %d: %f learning rate: %f' % (step, mean_loss, lr))
mean_loss = 0
labels = np.concatenate(list(batches)[1:])
# list(batches)[1:], the labels are the last 10 characters, while the inputs are the first 10
#np.concatenate((seq1,seq2),axis=0)
#len(list(batches)[1:])=10, there are 10 elements in the list
#these 10 elements are piped up in one array
#labels.shape is (640,27)
print('Minibatch perplexity: %.2f' % float(np.exp(logprob(predictions, labels))))
if step % (summary_frequency * 10) == 0:
# Generate some samples.
print('=' * 80)
for _ in range(5):
feed = sample(random_distribution()) # one-hot coded word
sentence = characters(feed)[0] # covert the one-hot coded word to character
reset_sample_state.run()
for _ in range(79):
prediction = sample_prediction.eval({sample_input: feed})
feed = sample(prediction)
sentence += characters(feed)[0]
print(sentence)
#print('\n')
print('=' * 80)
# Measure validation set perplexity.
reset_sample_state.run()
valid_logprob = 0
for _ in range(valid_size):
b = valid_batches.next()
predictions = sample_prediction.eval({sample_input: b[0]})
valid_logprob = valid_logprob + logprob(predictions, b[1])
print('Validation set perplexity: %.2f' % float(np.exp(
valid_logprob / valid_size)))
We want to train a LSTM over bigrams, that is pairs of consecutive characters like 'ab' instead of single characters like 'a'. Since the number of possible bigrams is large, feeding them directly to the LSTM using 1-hot encodings will lead to a very sparse representation that is very wasteful computationally.
a- Introduce an embedding lookup on the inputs, and feed the embeddings to the LSTM cell instead of the inputs themselves.
b- Write a bigram-based LSTM, modeled on the character LSTM above.
c- Introduce Dropout. For best practices on how to use Dropout in LSTMs, refer to this article.
(difficult!)
Write a sequence-to-sequence LSTM which mirrors all the words in a sentence. For example, if your input is:
the quick brown fox
the model should attempt to output:
eht kciuq nworb xof
Refer to the lecture on how to put together a sequence-to-sequence model, as well as this article for best practices.