LSTM First Application
Here we follow the same application from the RNN first one, predicting the next character in the sequence 'helloahmed'. However, this time we will implement it using an LSTM architecture instead of a simple RNN.
Python Implementation
import numpy as np
import matplotlib.pyplot as plt
class LSTM:
def __init__(self, hidden_size, vocab_size):
# ...
pass
def _sigmoid(self, z):
# ...
pass
def _tanh(self, z):
# ...
pass
def _softmax(self, z):
# ...
pass
def forward(self, inputs, h_prev, c_prev):
# ...
pass
def compute_cost(self, y_preds, targets):
# ...
pass
def backpropagation(self, targets, cache):
# ...
pass
def update_parameters(self, learning_rate=0.01):
# ...
pass
def sample(self, seed_idx, h_prev, c_prev, length=20):
# ...
pass
# 1. prepare data
data = "helloahmed"
chars = list(set(data))
vocab_size = len(chars)
char_to_idx = { ch:i for i,ch in enumerate(chars)}
idx_to_char = { i:ch for i,ch in enumerate(chars)}
print(f"Data: {data}")
print(f"Vocabulary: {chars}")
print(f"Vocab Size: {vocab_size}")
# 2. create model
hidden_size = 25
epochs = 3000
lstm = LSTM(hidden_size, vocab_size)
# 3. training loop
print("Training LSTM...")
costs = []
inputs = [char_to_idx[ch] for ch in data[:-1]] # all chars except last
targets = [char_to_idx[ch] for ch in data[1:]] # all chars except first
for epoch in range(epochs):
# We reset the memory at the start of each epoch
h_prev = np.zeros((hidden_size, 1))
c_prev = np.zeros((hidden_size, 1))
# Forward pass
y_preds, h_final, c_final, cache = lstm.forward(inputs, h_prev, c_prev)
cost = lstm.compute_cost(y_preds, targets)
lstm.backpropagation(targets, cache)
lstm.update_parameters()
if epoch % 200 == 0:
print(f"Epoch {epoch}, Cost: {cost}")
costs.append(cost) # Append cost for plotting
print("Training complete.")
plt.plot(np.squeeze(costs))
plt.ylabel('Cost')
plt.xlabel('Epochs (per 200)')
plt.title(f"Learning rate = {0.01}")
plt.show()
# Test the model (sampling)
print("\nSampling from the model:")
# Get the index for our seed character 'h'
seed_char_idx = char_to_idx['h']
h_sample = np.zeros((hidden_size, 1))
c_sample = np.zeros((hidden_size, 1))
generated_indices = lstm.sample(seed_char_idx, h_sample, c_sample, length=10)
generated_text = 'h' + ''.join(idx_to_char[idx] for idx in generated_indices)
print(f"Generated text: '{generated_text}'")
Output
Training LSTM...
Epoch 0, Cost: 1.9459308553119037
Epoch 200, Cost: 1.888694596930537
Epoch 400, Cost: 1.885747141311822
Epoch 600, Cost: 1.869165401158513
Epoch 800, Cost: 1.7772037343293763
Epoch 1000, Cost: 1.4138688219022517
Epoch 1200, Cost: 0.7320503793476106
Epoch 1400, Cost: 0.3883764333872252
Epoch 1600, Cost: 0.21691620735344425
Epoch 1800, Cost: 0.12986328517492585
Epoch 2000, Cost: 0.08541260344701034
Epoch 2200, Cost: 0.06066953628544073
Epoch 2400, Cost: 0.04568582684125432
Epoch 2600, Cost: 0.035955796413271686
Epoch 2800, Cost: 0.029270449962368588
Training complete.
Sampling from the model:
Generated text: 'helloahmedl'

RNN vs. LSTM
The same application using a simple RNN architecture resulted in the generated text 'hmedlloahme', which is less accurate than the LSTM's output 'helloahmedl'. This demonstrates the LSTM's superior ability to capture long-term dependencies in sequences.
Notebook
You can find an interactive Jupyter Notebook version of this implementation here.