Files
2026-07-13 13:39:55 +08:00

196 lines
6.6 KiB
Python

# coding:utf-8
import autograd.numpy as np
from autograd import elementwise_grad
from mla.neuralnet.activations import sigmoid
from mla.neuralnet.initializations import get_initializer
from mla.neuralnet.layers import Layer, get_activation, ParamMixin
from mla.neuralnet.parameters import Parameters
"""
References:
Understanding LSTM Networks http://colah.github.io/posts/2015-08-Understanding-LSTMs/
A Critical Review of Recurrent Neural Networks for Sequence Learning http://arxiv.org/pdf/1506.00019v4.pdf
"""
class LSTM(Layer, ParamMixin):
def __init__(
self,
hidden_dim,
activation="tanh",
inner_init="orthogonal",
parameters=None,
return_sequences=True,
):
self.return_sequences = return_sequences
self.hidden_dim = hidden_dim
self.inner_init = get_initializer(inner_init)
self.activation = get_activation(activation)
self.activation_d = elementwise_grad(self.activation)
self.sigmoid_d = elementwise_grad(sigmoid)
if parameters is None:
self._params = Parameters()
else:
self._params = parameters
self.last_input = None
self.states = None
self.outputs = None
self.gates = None
self.hprev = None
self.input_dim = None
self.W = None
self.U = None
def setup(self, x_shape):
"""
Naming convention:
i : input gate
f : forget gate
c : cell
o : output gate
Parameters
----------
x_shape : np.array(batch size, time steps, input shape)
"""
self.input_dim = x_shape[2]
# Input -> Hidden
W_params = ["W_i", "W_f", "W_o", "W_c"]
# Hidden -> Hidden
U_params = ["U_i", "U_f", "U_o", "U_c"]
# Bias terms
b_params = ["b_i", "b_f", "b_o", "b_c"]
# Initialize params
for param in W_params:
self._params[param] = self._params.init((self.input_dim, self.hidden_dim))
for param in U_params:
self._params[param] = self.inner_init((self.hidden_dim, self.hidden_dim))
for param in b_params:
self._params[param] = np.full((self.hidden_dim,), self._params.initial_bias)
# Combine weights for simplicity
self.W = [self._params[param] for param in W_params]
self.U = [self._params[param] for param in U_params]
# Init gradient arrays for all weights
self._params.init_grad()
self.hprev = np.zeros((x_shape[0], self.hidden_dim))
self.oprev = np.zeros((x_shape[0], self.hidden_dim))
def forward_pass(self, X):
n_samples, n_timesteps, input_shape = X.shape
p = self._params
self.last_input = X
self.states = np.zeros((n_samples, n_timesteps + 1, self.hidden_dim))
self.outputs = np.zeros((n_samples, n_timesteps + 1, self.hidden_dim))
self.gates = {
k: np.zeros((n_samples, n_timesteps, self.hidden_dim))
for k in ["i", "f", "o", "c"]
}
self.states[:, -1, :] = self.hprev
self.outputs[:, -1, :] = self.oprev
for i in range(n_timesteps):
t_gates = np.dot(X[:, i, :], self.W) + np.dot(
self.outputs[:, i - 1, :], self.U
)
# Input
self.gates["i"][:, i, :] = sigmoid(t_gates[:, 0, :] + p["b_i"])
# Forget
self.gates["f"][:, i, :] = sigmoid(t_gates[:, 1, :] + p["b_f"])
# Output
self.gates["o"][:, i, :] = sigmoid(t_gates[:, 2, :] + p["b_o"])
# Cell
self.gates["c"][:, i, :] = self.activation(t_gates[:, 3, :] + p["b_c"])
# (previous state * forget) + input + cell
self.states[:, i, :] = (
self.states[:, i - 1, :] * self.gates["f"][:, i, :]
+ self.gates["i"][:, i, :] * self.gates["c"][:, i, :]
)
self.outputs[:, i, :] = self.gates["o"][:, i, :] * self.activation(
self.states[:, i, :]
)
self.hprev = self.states[:, n_timesteps - 1, :].copy()
self.oprev = self.outputs[:, n_timesteps - 1, :].copy()
if self.return_sequences:
return self.outputs[:, 0:-1, :]
else:
return self.outputs[:, -2, :]
def backward_pass(self, delta):
if len(delta.shape) == 2:
delta = delta[:, np.newaxis, :]
n_samples, n_timesteps, input_shape = delta.shape
# Temporal gradient arrays
grad = {k: np.zeros_like(self._params[k]) for k in self._params.keys()}
dh_next = np.zeros((n_samples, input_shape))
output = np.zeros((n_samples, n_timesteps, self.input_dim))
# Backpropagation through time
for i in reversed(range(n_timesteps)):
dhi = (
delta[:, i, :]
* self.gates["o"][:, i, :]
* self.activation_d(self.states[:, i, :])
+ dh_next
)
og = delta[:, i, :] * self.activation(self.states[:, i, :])
de_o = og * self.sigmoid_d(self.gates["o"][:, i, :])
grad["W_o"] += np.dot(self.last_input[:, i, :].T, de_o)
grad["U_o"] += np.dot(self.outputs[:, i - 1, :].T, de_o)
grad["b_o"] += de_o.sum(axis=0)
de_f = (dhi * self.states[:, i - 1, :]) * self.sigmoid_d(
self.gates["f"][:, i, :]
)
grad["W_f"] += np.dot(self.last_input[:, i, :].T, de_f)
grad["U_f"] += np.dot(self.outputs[:, i - 1, :].T, de_f)
grad["b_f"] += de_f.sum(axis=0)
de_i = (dhi * self.gates["c"][:, i, :]) * self.sigmoid_d(
self.gates["i"][:, i, :]
)
grad["W_i"] += np.dot(self.last_input[:, i, :].T, de_i)
grad["U_i"] += np.dot(self.outputs[:, i - 1, :].T, de_i)
grad["b_i"] += de_i.sum(axis=0)
de_c = (dhi * self.gates["i"][:, i, :]) * self.activation_d(
self.gates["c"][:, i, :]
)
grad["W_c"] += np.dot(self.last_input[:, i, :].T, de_c)
grad["U_c"] += np.dot(self.outputs[:, i - 1, :].T, de_c)
grad["b_c"] += de_c.sum(axis=0)
dh_next = dhi * self.gates["f"][:, i, :]
# TODO: propagate error to the next layer
# Change actual gradient arrays
for k in grad.keys():
self._params.update_grad(k, grad[k])
return output
def shape(self, x_shape):
if self.return_sequences:
return x_shape[0], x_shape[1], self.hidden_dim
else:
return x_shape[0], self.hidden_dim