模型原理详解:字符级 LSTM 姓名生成器

发布于:2025-05-13 ⋅ 阅读:(7) ⋅ 点赞:(0)

模型原理详解:字符级 LSTM 姓名生成器

本模型实现的是一个字符级别的语言建模任务,目标是通过学习历史姓名的构成方式,让模型能够自动补全一个部分给出的名字。


🧩 任务目标

通过学习真实姓名的数据,模型将学会:

给定前缀字符序列 x = [c₁, c₂, …, cₙ],预测下一个字符 cₙ₊₁。

例如:输入 "jas",模型应预测出 "o",并继续预测下一个字符,直到生成一个完整的名字。


写在前面:

# ✅ 1. 导入依赖
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
from tqdm import tqdm
import matplotlib.pyplot as plt
import os

1️⃣ 数据预处理阶段

✅ 1.1 加载与清洗

  • 从 CSV 文件中提取所有姓名,统一转为小写字母,去除重复。
  • 构建字符集(a–z),为每个字符分配索引(char_to_idx)。
# ... existing code ...

# 读取CSV文件
file_path = './all_names_combined.csv'  # 指定数据文件路径
df = pd.read_csv(file_path)  # 使用pandas读取CSV文件
print(df.head())  # 打印数据前5行,用于检查数据格式

# 处理姓名列,统一转为小写
names = df['Name'].unique()  # 获取姓名列的唯一值,去除重复
# 对每个姓名进行处理:
# 1. strip()去除首尾空格
# 2. lower()转换为小写
# 3. 只保留字符串类型的姓名
names = [name.strip().lower() for name in names if isinstance(name, str)]

# 构建字符集
all_text = ''.join(names)  # 将所有姓名连接成一个长字符串
chars = sorted(set(all_text))  # 获取所有不重复的字符并排序
# 创建字符到索引的映射字典
char_to_idx = {ch: idx for idx, ch in enumerate(chars)}
# 创建索引到字符的映射字典
idx_to_char = {idx: ch for ch, idx in char_to_idx.items()}
print(chars, len(chars))  # 打印字符集及其大小

# ... existing code ...

✅ 1.2 构造训练样本


# 该段代码用于构建训练数据,将每个名字分解成多个序列样本
# 对于每个名字,我们取前n个字符作为输入序列,第n+1个字符作为预测目标
# 例如:对于名字"john",会生成以下训练样本:
# 输入序列 -> 目标字符
# "j" -> "o"
# "jo" -> "h"
# "joh" -> "n"

# 设置最大序列长度为10
max_seq_len = 10
sequences, next_chars = [], []

# 遍历所有名字,生成训练序列
for name in names:
    for i in range(1, len(name)):
        seq = name[:i]  # 取名字的前i个字符作为输入序列
        next_char = name[i]  # 取第i+1个字符作为预测目标
        if len(seq) <= max_seq_len:  # 只保留长度不超过max_seq_len的序列
            sequences.append(seq)
            next_chars.append(next_char)

# 创建输入特征矩阵X和标签向量y
X = np.zeros((len(sequences), max_seq_len), dtype=np.int32)  # 初始化输入矩阵,用0填充
y = np.zeros(len(sequences), dtype=np.int32)  # 初始化标签向量

# 将字符序列转换为数字索引
for i, seq in enumerate(sequences):
    # 将序列填充到max_seq_len长度,短序列从右侧开始填充
    for t, char in enumerate(seq):
        X[i, max_seq_len - len(seq) + t] = char_to_idx[char]
    # 将目标字符转换为对应的索引
    y[i] = char_to_idx[next_chars[i]]

print(X.shape, y.shape)  # 打印数据形状,用于检查

每个名字生

成多个训练对:

例如 "jason" 会产生:

  • "j""a"
  • "ja""s"
  • "jas""o"
  • "jaso""n"

将这些输入序列转为固定长度(max_seq_len=10)的张量,用 0 补齐左侧。


2️⃣ 模型结构(LSTM)

输入字符索引序列(最多10个)

Embedding 层:将字符索引映射为向量

LSTM 层:学习字符序列中的模式

全连接层 + Softmax:输出下一个字符的概率分布

模型结构如下:

class NamePredictor(nn.Module):
    def __init__(...):
        self.embedding = nn.Embedding(...)
        self.lstm = nn.LSTM(...)
        self.fc = nn.Linear(...)

3️⃣ 模型训练流程

class NamePredictor(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, dropout=0.2):
        super(NamePredictor, self).__init__()
        # 字符嵌入层:将每个字符转换为embedding_dim维的向量表示
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        # LSTM层:处理序列信息,hidden_dim为隐藏状态维度
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
        # Dropout层:防止过拟合,默认丢弃率为0.2
        self.dropout = nn.Dropout(dropout)
        # 全连接层:将LSTM的输出映射到词汇表大小的概率分布
        self.fc = nn.Linear(hidden_dim, vocab_size)
    
    def forward(self, x):
        # 1. 字符嵌入:将输入序列转换为向量表示
        embedded = self.embedding(x)
        # 2. LSTM处理:获取序列的隐藏状态
        _, (h_n, _) = self.lstm(embedded)
        # 3. 应用dropout并去除多余的维度
        h_n = self.dropout(h_n.squeeze(0))
        # 4. 通过全连接层得到最终输出
        output = self.fc(h_n)
        return output

# 创建模型实例
vocab_size = len(char_to_idx)  # 词汇表大小(字符集大小)
model = NamePredictor(vocab_size, embedding_dim=32, hidden_dim=64)  # 创建模型实例
print(model)  # 打印模型结构

输入 batch 的字符序列

模型预测下一个字符的概率分布

使用真实字符与预测值计算 交叉熵损失(CrossEntropyLoss)

使用 Adam 优化器反向传播更新参数

每轮训练记录平均损失,并保存当前最优模型:

if avg_loss < best_loss:
    torch.save(model.state_dict(), ...)


device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 检查是否有GPU可用,如果有则使用GPU,否则使用CPU
model = model.to(device)  # 将模型移动到指定设备上

# 将数据转换为PyTorch张量并创建数据加载器
X_tensor = torch.tensor(X, dtype=torch.long)  # 将输入数据转换为PyTorch长整型张量
y_tensor = torch.tensor(y, dtype=torch.long)  # 将标签数据转换为PyTorch长整型张量
dataset = TensorDataset(X_tensor, y_tensor)  # 创建数据集对象
dataloader = DataLoader(dataset, batch_size=512, shuffle=True)  # 创建数据加载器,设置批量大小为512,并随机打乱数据

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()  # 使用交叉熵损失函数,适用于多分类问题
optimizer = optim.Adam(model.parameters(), lr=0.05)  # 使用Adam优化器,设置学习率为0.005

# 初始化训练相关变量
best_loss = float('inf')  # 初始化最佳损失值为无穷大
save_path = 'best_name_model.pt'  # 设置模型保存路径
epoch_losses = []  # 创建列表用于记录每个epoch的平均损失

# 开始训练循环
num_epochs = 20  # 设置训练轮数为50
for epoch in range(1, num_epochs + 1):
    model.train()  # 将模型设置为训练模式
    epoch_loss = 0  # 初始化当前epoch的总损失
    total_samples = 0  # 初始化当前epoch的总样本数
    pbar = tqdm(dataloader, desc=f"Epoch {epoch}/{num_epochs}")  # 创建进度条,显示当前训练进度
    
    # 遍历每个批次
    for batch_x, batch_y in pbar:
        # 将数据移到指定设备
        batch_x, batch_y = batch_x.to(device), batch_y.to(device)  # 将批次数据移动到指定设备(GPU/CPU)
        optimizer.zero_grad()  # 清空之前的梯度
        outputs = model(batch_x)  # 前向传播,获取模型预测结果
        loss = criterion(outputs, batch_y)  # 计算损失值
        loss.backward()  # 反向传播,计算梯度
        optimizer.step()  # 更新模型参数
        
        # 累计损失和样本数
        batch_size = batch_x.size(0)  # 获取当前批次的大小
        epoch_loss += loss.item() * batch_size  # 累加批次损失
        total_samples += batch_size  # 累加样本数
        pbar.set_postfix({"BatchLoss": f"{loss.item():.4f}"})  # 在进度条中显示当前批次的损失值
    
    # 计算并记录平均损失
    avg_loss = epoch_loss / total_samples  # 计算当前epoch的平均损失
    epoch_losses.append(avg_loss)  # 将平均损失添加到记录列表中

    # 如果当前epoch的损失更低,保存模型
    if avg_loss < best_loss:
        best_loss = avg_loss  # 更新最佳损失值
        torch.save(model.state_dict(), save_path)  # 保存模型参数
        print(f"✅ Model saved. New best loss: {best_loss:.6f}")  # 打印保存信息

    print(f"Epoch {epoch}, Avg Loss: {avg_loss:.6f}")  # 打印当前epoch的平均损失

在这里插入图片描述
在这里插入图片描述

4️⃣ 姓名生成机制

函数 predict_name(prefix) 使用如下逻辑生成名字:

输入前缀如 “mi”,转为字符索引序列

构造输入张量,调用模型预测下一个字符

取最大概率字符(argmax)作为输出

将新字符添加到输入中,循环执行直到满足停止条件

例如:

mi ➜ mic ➜ mich ➜ miche ➜ michel ➜ michael

🧠 总结

阶段 内容 作用
数据准备 名字拆分为序列 ➜ 标签对 构造监督学习样本
模型结构 LSTM 结构输出分类结果 理解字符顺序关系
模型训练 最小化交叉熵损失 学习字符到字符的预测能力
名字生成 按预测字符依次构建完整名字 模拟自然名字补全的推理过程

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到