The same weights are used at every step. That is the recurrent part, and it is why the network can handle a sequence of any length.
The inductive bias
The assumption is that the same rule applies at every position. This is the same kind of assumption a convolutional network makes about space, applied to time instead.
The state \(h_t\) has to carry everything the network needs to remember. With a plain recurrent network the gradient either shrinks or grows exponentially over many steps, so it cannot learn long dependencies. An LSTM adds gates that decide what to keep and what to forget, which fixes this in practice.
A language detector
We classify a short string as one of a few languages, character by character.
import numpy as npimport torchimport torch.nn as nntorch.manual_seed(9)rng = np.random.default_rng(9)# character frequencies that differ between the three languagesalphabet ="abcdefghijklmnopqrstuvwxyz "idx = {c: i for i, c inenumerate(alphabet)}profiles = {"en": {"t": 4, "h": 3, "e": 5, " ": 4, "a": 3, "o": 2},"de": {"c": 3, "h": 4, "e": 5, "n": 4, " ": 4, "s": 3},"fr": {"e": 6, "s": 3, "u": 3, " ": 4, "q": 2, "a": 3},}def make_string(lang, length): weights = np.ones(len(alphabet))for c, w in profiles[lang].items(): weights[idx[c]] = w weights /= weights.sum()return"".join(rng.choice(list(alphabet), size=length, p=weights))langs =list(profiles)n =1500lengths = rng.integers(20, 60, n)labels = rng.integers(0, 3, n)strings = [make_string(langs[l], int(k)) for l, k inzip(labels, lengths)]print(labels[0], strings[0][:40])
2 sueuenhbkqlksppgnuknyznfrb kpuaoehfq
Encoding
Every character becomes an integer, and an embedding layer turns it into a vector. We pad the sequences to the same length so that they fit in one tensor.
maxlen =int(lengths.max())def encode(s): v = [idx[c] for c in s]return v + [len(alphabet)] * (maxlen -len(v))X = torch.tensor([encode(s) for s in strings], dtype=torch.long)y = torch.tensor(labels, dtype=torch.long)print("input shape:", tuple(X.shape))
1
Index len(alphabet) is the padding symbol, so the embedding needs one extra row.
input shape: (1500, 59)
The model
class LangNet(nn.Module):def__init__(self, n_symbols, hidden=32, n_classes=3):super().__init__()self.embed = nn.Embedding(n_symbols, hidden)self.lstm = nn.LSTM(hidden, hidden, batch_first=True)self.out = nn.Linear(hidden, n_classes)def forward(self, x): h, _ =self.lstm(self.embed(x))returnself.out(h[:, -1, :])model = LangNet(len(alphabet) +1)print("parameters:", sum(p.numel() for p in model.parameters()))
1
We take the last state of the sequence and classify from it.
The network never saw a dictionary. It learned which character combinations are typical for which language.
What replaced recurrent networks
For most tasks with sequences, transformers have replaced recurrent networks. Instead of a state that is passed along, a transformer lets every position look at every other position directly. This removes the long dependency problem and it parallelizes much better on a GPU.
The idea of building the structure of the problem into the architecture is the same. Only the structure is different.