Name Generation with LSTM

We will train a character-level LSTM to generate Arabic names. Using a dataset of Arabic names, we will train the model to generate new names that resemble the training data.

Show Code
#import 
import random
import torch
import torch.nn as nn

import pandas as pd
import matplotlib.pyplot as plt
Show Code
# read the csv file containing Arabic names
df = pd.read_csv("./data/Arabic_names.csv")
names = df["Name"].tolist()
names[:5]  # show the first 5 names
['ابتسام', 'ابتهاج', 'ابتهال', 'اجتهاد', 'ازدهار']
Show Code
# mark the end of a name with a special character (e.g. ".") to help the model learn when to stop


END = "."                                  # marks the end of a name
names = [n.strip() + END for n in names]   # "محمد" → "محمد."

# index 0 reserved for padding
chars = ["<pad>"] + sorted(set("".join(names)))
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for c, i in stoi.items()}
pad_idx = stoi["<pad>"]
vocab_size = len(chars)

print(f"Unique characters: {chars}")
print(f"Vocabulary size: {vocab_size}")
Unique characters: ['<pad>', ' ', '.', 'ء', 'آ', 'أ', 'ؤ', 'إ', 'ئ', 'ا', 'ب', 'ة', 'ت', 'ث', 'ج', 'ح', 'خ', 'د', 'ذ', 'ر', 'ز', 'س', 'ش', 'ص', 'ض', 'ط', 'ظ', 'ع', 'غ', 'ف', 'ق', 'ك', 'ل', 'م', 'ن', 'ه', 'و', 'ى', 'ي']
Vocabulary size: 39
Show Code
def get_batch(batch_size=32):
    batch = random.sample(names, batch_size)          # pick random names 32 at a time
    seqs = [[stoi[c] for c in name] for name in batch] # convert chars to indices
    maxlen = max(len(s) for s in seqs) # find the longest name in the batch

    # pre-fill with padding, so any slot we don't write stays <pad>.
    X = torch.full((batch_size, maxlen - 1), pad_idx) # fill every slot with the pad index 0
    Y = torch.full((batch_size, maxlen - 1), pad_idx)

    for i, s in enumerate(seqs):
        s = torch.tensor(s)
        X[i, :len(s) - 1] = s[:-1]     # takes the values s[:-1] and writes them into the first len(s)-1 slots of X[i]
        Y[i, :len(s) - 1] = s[1:]      # "in row i, write the name-without-first-char (shifted), rest padded"
    return X, Y

print("Example batch (X, Y):")
X, Y = get_batch(4)
print(X)
print(Y)
Example batch (X, Y):
tensor([[36, 21,  9, 33,  0,  0,  0,  0,  0],
        [27, 10, 17,  9, 32, 36,  9, 15, 17],
        [27, 10, 17,  9, 32, 33, 34, 27, 33],
        [33, 20, 34,  0,  0,  0,  0,  0,  0]])
tensor([[21,  9, 33,  2,  0,  0,  0,  0,  0],
        [10, 17,  9, 32, 36,  9, 15, 17,  2],
        [10, 17,  9, 32, 33, 34, 27, 33,  2],
        [20, 34,  2,  0,  0,  0,  0,  0,  0]])
Show Code
class NameGenerator(nn.Module):
    def __init__(self, vocab_size, embedding_dim=16, hidden_dim=64, pad_idx=0):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
        self.drop = nn.Dropout(0.3)
        self.fc = nn.Linear(hidden_dim, vocab_size)

    def forward(self, x, hidden=None):
        x = self.embedding(x)                    # (batch, seq_len) → (batch, seq_len, embed)
        output, hidden = self.lstm(x, hidden)    # (batch, seq_len, hidden)
        logits = self.fc(output)                 # (batch, seq_len, vocab)
        return logits, hidden
Show Code
batch_size=32
model = NameGenerator(vocab_size, pad_idx=pad_idx)

optimizer = torch.optim.Adam(model.parameters(), lr=3e-3)
loss_fn = nn.CrossEntropyLoss(ignore_index=pad_idx)

losses = []

model.train()
for step in range(2000):
    X, Y = get_batch(batch_size)
    
    logits, _ = model(X)
    loss = loss_fn(logits.view(-1, vocab_size), Y.view(-1))
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    losses.append(loss.item())
    if step % 10 == 0:
        print(f"Step {step}, Loss: {loss.item():.4f}")

    
Step 0, Loss: 3.7123
Step 10, Loss: 3.3857
Step 20, Loss: 2.8803
Step 30, Loss: 2.8244
Step 40, Loss: 2.7437
Step 50, Loss: 2.7162
Step 60, Loss: 2.5429
Step 70, Loss: 2.5481
Step 80, Loss: 2.4537
Step 90, Loss: 2.4701
Step 100, Loss: 2.3826
Step 110, Loss: 2.3310
Step 120, Loss: 2.5720
Step 130, Loss: 2.2398
Step 140, Loss: 2.3545
Step 150, Loss: 2.1896
Step 160, Loss: 2.2061
Step 170, Loss: 2.0550
Step 180, Loss: 2.0724
Step 190, Loss: 2.1330
Step 200, Loss: 2.1347
Step 210, Loss: 2.2341
Step 220, Loss: 2.1966
Step 230, Loss: 2.1150
Step 240, Loss: 2.0838
Step 250, Loss: 2.1643
Step 260, Loss: 2.0793
Step 270, Loss: 2.1172
Step 280, Loss: 2.0315
Step 290, Loss: 2.0927
Step 300, Loss: 1.9873
Step 310, Loss: 2.0878
Step 320, Loss: 1.9793
Step 330, Loss: 1.9454
Step 340, Loss: 2.0474
Step 350, Loss: 1.9074
Step 360, Loss: 2.2570
Step 370, Loss: 1.9570
Step 380, Loss: 1.8986
Step 390, Loss: 1.8623
Step 400, Loss: 2.0273
Step 410, Loss: 1.9277
Step 420, Loss: 1.9720
Step 430, Loss: 1.9351
Step 440, Loss: 1.8524
Step 450, Loss: 1.8674
Step 460, Loss: 1.8174
Step 470, Loss: 2.0780
Step 480, Loss: 1.9160
Step 490, Loss: 1.7706
Step 500, Loss: 2.0075
Step 510, Loss: 1.9726
Step 520, Loss: 1.8953
Step 530, Loss: 1.9025
Step 540, Loss: 1.8525
Step 550, Loss: 1.7931
Step 560, Loss: 1.7889
Step 570, Loss: 1.7882
Step 580, Loss: 1.9293
Step 590, Loss: 1.8817
Step 600, Loss: 1.8805
Step 610, Loss: 1.6472
Step 620, Loss: 1.7630
Step 630, Loss: 1.9575
Step 640, Loss: 1.7358
Step 650, Loss: 1.7999
Step 660, Loss: 1.6662
Step 670, Loss: 1.8949
Step 680, Loss: 1.8099
Step 690, Loss: 1.8274
Step 700, Loss: 1.6679
Step 710, Loss: 1.8380
Step 720, Loss: 1.8513
Step 730, Loss: 1.7640
Step 740, Loss: 1.7828
Step 750, Loss: 1.8286
Step 760, Loss: 1.7204
Step 770, Loss: 1.7343
Step 780, Loss: 1.7405
Step 790, Loss: 1.7840
Step 800, Loss: 1.5746
Step 810, Loss: 1.8369
Step 820, Loss: 1.5820
Step 830, Loss: 1.7803
Step 840, Loss: 1.6693
Step 850, Loss: 1.6607
Step 860, Loss: 1.5404
Step 870, Loss: 1.7420
Step 880, Loss: 1.6304
Step 890, Loss: 1.5814
Step 900, Loss: 1.6428
Step 910, Loss: 1.7463
Step 920, Loss: 1.6731
Step 930, Loss: 1.6752
Step 940, Loss: 1.6630
Step 950, Loss: 1.6431
Step 960, Loss: 1.6774
Step 970, Loss: 1.6674
Step 980, Loss: 1.4655
Step 990, Loss: 1.4735
Step 1000, Loss: 1.5689
Step 1010, Loss: 1.6467
Step 1020, Loss: 1.7233
Step 1030, Loss: 1.5932
Step 1040, Loss: 1.7460
Step 1050, Loss: 1.5042
Step 1060, Loss: 1.5063
Step 1070, Loss: 1.6953
Step 1080, Loss: 1.5196
Step 1090, Loss: 1.5652
Step 1100, Loss: 1.7751
Step 1110, Loss: 1.5762
Step 1120, Loss: 1.5920
Step 1130, Loss: 1.5387
Step 1140, Loss: 1.4948
Step 1150, Loss: 1.4893
Step 1160, Loss: 1.4954
Step 1170, Loss: 1.5451
Step 1180, Loss: 1.4964
Step 1190, Loss: 1.5426
Step 1200, Loss: 1.3994
Step 1210, Loss: 1.5579
Step 1220, Loss: 1.3510
Step 1230, Loss: 1.4612
Step 1240, Loss: 1.4722
Step 1250, Loss: 1.4938
Step 1260, Loss: 1.4157
Step 1270, Loss: 1.4470
Step 1280, Loss: 1.3908
Step 1290, Loss: 1.5252
Step 1300, Loss: 1.5881
Step 1310, Loss: 1.5137
Step 1320, Loss: 1.4267
Step 1330, Loss: 1.4385
Step 1340, Loss: 1.4196
Step 1350, Loss: 1.4857
Step 1360, Loss: 1.4806
Step 1370, Loss: 1.5740
Step 1380, Loss: 1.4062
Step 1390, Loss: 1.6283
Step 1400, Loss: 1.4927
Step 1410, Loss: 1.3525
Step 1420, Loss: 1.5622
Step 1430, Loss: 1.3932
Step 1440, Loss: 1.3734
Step 1450, Loss: 1.7393
Step 1460, Loss: 1.5932
Step 1470, Loss: 1.5161
Step 1480, Loss: 1.3962
Step 1490, Loss: 1.3382
Step 1500, Loss: 1.4751
Step 1510, Loss: 1.3464
Step 1520, Loss: 1.4493
Step 1530, Loss: 1.4821
Step 1540, Loss: 1.4604
Step 1550, Loss: 1.3568
Step 1560, Loss: 1.3004
Step 1570, Loss: 1.4466
Step 1580, Loss: 1.4418
Step 1590, Loss: 1.3195
Step 1600, Loss: 1.4305
Step 1610, Loss: 1.3140
Step 1620, Loss: 1.3425
Step 1630, Loss: 1.5338
Step 1640, Loss: 1.3494
Step 1650, Loss: 1.3700
Step 1660, Loss: 1.3954
Step 1670, Loss: 1.3456
Step 1680, Loss: 1.2161
Step 1690, Loss: 1.3156
Step 1700, Loss: 1.4048
Step 1710, Loss: 1.2463
Step 1720, Loss: 1.3602
Step 1730, Loss: 1.2403
Step 1740, Loss: 1.2894
Step 1750, Loss: 1.4075
Step 1760, Loss: 1.3777
Step 1770, Loss: 1.2939
Step 1780, Loss: 1.3723
Step 1790, Loss: 1.3277
Step 1800, Loss: 1.2410
Step 1810, Loss: 1.2697
Step 1820, Loss: 1.2559
Step 1830, Loss: 1.4382
Step 1840, Loss: 1.3431
Step 1850, Loss: 1.2973
Step 1860, Loss: 1.3711
Step 1870, Loss: 1.3636
Step 1880, Loss: 1.3608
Step 1890, Loss: 1.2869
Step 1900, Loss: 1.2485
Step 1910, Loss: 1.3436
Step 1920, Loss: 1.3802
Step 1930, Loss: 1.3581
Step 1940, Loss: 1.1560
Step 1950, Loss: 1.2749
Step 1960, Loss: 1.3660
Step 1970, Loss: 1.3919
Step 1980, Loss: 1.4134
Step 1990, Loss: 1.2329
Show Code
# --- plot ---
plt.figure(figsize=(8, 4))
plt.plot(losses)
plt.xlabel("training step")
plt.ylabel("loss")
plt.title("Name generator — training loss")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Show Code
import random


@torch.no_grad()
def generate_name(temperature=0.8, max_len=20):
    model.eval()

    # the model never learned to predict the FIRST letter (it was always given),
    # so we seed with a random real first letter, then let the model continue.
    # names = list WITH END appended
    first = random.choice([n[0] for n in names])
    out = [first]

    x = torch.tensor([[stoi[first]]])                # (1, 1) batch-first
    hidden = None

    for _ in range(max_len):
        logits, hidden = model(x, hidden)
        probs = torch.softmax(logits[0, -1] / temperature, dim=-1)
        probs[pad_idx] = 0                            # never sample padding

        next_idx = torch.multinomial(probs, 1).item()
        if itos[next_idx] == END:                    # stop at "."
            break
        out.append(itos[next_idx])
        x = torch.tensor([[next_idx]])               # feed it back

    model.train()
    return "".join(out)


# --- generate 10 names ---
for _ in range(10):
    print(generate_name(temperature=1.5))
بتير
ديما
علاوية
عقل
دالسان
غزت
عويز
بطيحة
نوت
تانيا
Show Code
train_set = set(n.strip(END) for n in names)

novel_names = []
gen = [generate_name(temperature=0.7) for _ in range(50)]
novel = sum(1 for g in gen if g not in train_set)
novel_names = [g for g in gen if g not in train_set]
print(f"{novel}/50 names are NEW")
print("Novel names:")
for name in novel_names:
    print(name)
14/50 names are NEW
Novel names:
أليب
عبدالحميم
نجي
بكرية
غزان
أوهب
نجوة
هانم
مجية
عريفة
مكلف
رقا
مراس
هازم

Comments