手把手教你实现makemore

维度对齐训练:

import torch
import torch.nn.functional as F

# ===================== 固定全局配置 =====================
vocab_size = 27    # 独热向量维度固定等于词汇表大小+1特殊字符
embedding_dim = 2  # 嵌入维度(每个字符被映射为 2 维连续向量)
batch_size = 32    # 批量大小(1 批共 32 个输入样本)
seq_len = 3        # 序列长度(每个样本由 3 个字符索引组成)
hidden_out_dim = 100  # W1输出的隐藏层维度
target_idx = 5     # 等价性验证的目标字符索引

# ===================== 嵌入矩阵初始化+生成输入+嵌入查找 =====================
C = torch.randn(vocab_size, embedding_dim) # 嵌入矩阵 C 需要被初始化成正态分布。形状 (27,2)
X = torch.randint(low=0, high=vocab_size, size=(batch_size, seq_len)) # 输入索引张量 X 是字符索引数据,(这里由于没有输入样本所以我直接用 torch.randint 生成随机值来模拟,实际上是固定的),无 “初始化” 概念。形状 (32,3)
# 执行批量嵌入查找,shape: (32, 3, 2)
# 方式1:直接查表C[X](PyTorch实操高效版,无独热向量)
embedded_out = C[X] 
# 方式2:独热向量 + 矩阵乘法(教学原理版,数学等价)
one_hot_vec = F.one_hot(torch.tensor(target_idx), num_classes=vocab_size).float()
# 独热向量与嵌入矩阵做矩阵乘法,实现查表
one_hot_emb = one_hot_vec @ C

# 打印各对象形状
print(f"嵌入矩阵C形状: {C.shape}")                # 预期:torch.Size([27, 2])
print(f"输入索引X形状: {X.shape}")                # 预期:torch.Size([32, 3])
print(f"嵌入查找输出形状: {embedded_out.shape}")   # 预期:torch.Size([32, 3, 2])

# ===================== 嵌入向量展平+与W1矩阵乘法得到隐藏层 =====================
# 1. 嵌入向量展平:三维(32,3,2)→二维(32,6),保留batch_size,拼接其余维度
emb_flat = embedded_out.view(batch_size, seq_len * embedding_dim)
# 2. 正态分布初始化隐藏层权重W1(输入6维,输出100维)
W1 = torch.randn(seq_len * embedding_dim, hidden_out_dim)
# 3. 矩阵乘法得到隐藏层输出(维度匹配:(32,6)@(6,100)=(32,100))
hidden_out = emb_flat @ W1
# 打印关键形状
print(f"嵌入向量展平后形状: {emb_flat.shape}")  # 预期:torch.Size([32, 6])
print(f"隐藏层权重W1形状: {W1.shape}")          # 预期:torch.Size([6, 100])
print(f"隐藏层输出张量形状: {hidden_out.shape}")  # 预期:torch.Size([32, 100])

Makemore MLP 完整可运行代码:

import torch
import torch.nn.functional as F

# 1. 准备模拟数据集
# 模拟 Makemore 数据集:32个样本,每个样本3个字符索引(0-26)
X = torch.randint(0, 27, (32, 3))  # 输入:(32, 3)
Y = torch.randint(0, 27, (32,))    # 目标:(32,)
dataset = (X.shape, Y.shape)

# 2. 初始化模型参数
# 固定随机种子,保证结果可复现
g = torch.Generator().manual_seed(2147483647)

# 嵌入矩阵:27个字符 × 2维嵌入
C = torch.randn((27, 2), generator=g)      # 形状:(27, 2)
# 隐藏层权重:输入6维(3字符×2嵌入)→ 输出100维
W1 = torch.randn((6, 100), generator=g)    # 形状:(6, 100)
b1 = torch.randn(100, generator=g)         # 隐藏层偏置:100维
# 输出层权重:输入100维 → 输出27维(字符表大小)
W2 = torch.randn((100, 27), generator=g)   # 形状:(100, 27)
b2 = torch.randn(27, generator=g)          # 输出层偏置:27维
parameters = [C, W1, b1, W2, b2]           # 统一管理所有可学习参数

# 统计总参数量
print(f"总参数量: {sum(p.nelement() for p in parameters)}")  # 输出:3481

# 3. 前向传播
# -----------------------------------------------------------------------------
emb = C[X]                                 # 嵌入查找:(32, 3) → (32, 3, 2)
h = torch.tanh(emb.view(-1, 6) @ W1 + b1)  # 展平+隐藏层:(32, 3, 2) → (32, 6) → (32, 100)
logits = h @ W2 + b2                       # 输出层:(32, 100) → (32, 27)
# 计算交叉熵损失
# 方法一:手动计算交叉熵
# logits_max = logits.max(1, keepdim=True).values  # 数值稳定:减去logits每行最大值,避免exp溢出
# counts = logits_max.exp() / logits.sub(logits_max).exp().sum(1, keepdim=True)  # softmax求概率
# manual_loss = -counts[torch.arange(32), Y].log().mean()  # 取目标标签对应概率→取对数→取负→求均值
# 方法二:使用 PyTorch 内置的交叉熵损失函数计算模型预测与真实标签 Y 的误差。
loss = F.cross_entropy(logits, Y)         

print(f"初始损失: {loss.item():.4f}")

# 4. 反向传播 + 参数更新(完整训练循环)
optimizer = torch.optim.AdamW(parameters, lr=0.01)  # 使用 AdamW 优化器

# 训练 1000 步
for step in range(1000):
    # 前向传播(每步重复计算)
    emb = C[X]
    h = torch.tanh(emb.view(-1, 6) @ W1 + b1)
    logits = h @ W2 + b2
    loss = F.cross_entropy(logits, Y)

    # 反向传播
    optimizer.zero_grad()  # 清零梯度
    loss.backward()        # 计算梯度
    optimizer.step()       # 更新参数

    # 每 100 步打印一次损失
    if step % 100 == 0:
        print(f"步骤 {step} | 损失: {loss.item():.4f}")

print(f"最终损失: {loss.item():.4f}")

小批次和确定调整学习率,用训练集、测试集、验证集防止过拟合:

import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt

# -------------------------- 1. 配置与初始化 --------------------------
chars = ['.'] + [chr(ord('a') + i) for i in range(26)]
itos = {i: c for i, c in enumerate(chars)}
stoi = {c: i for i, c in enumerate(chars)}
vocab_size = len(chars)
block_size = 3
n_embd = 2
hidden_dim = 100

# -------------------------- 2. 生成模拟数据集 --------------------------
def generate_dataset(num_samples=200000):
    X = torch.randint(0, vocab_size, (num_samples, block_size))
    Y = torch.randint(0, vocab_size, (num_samples,))
    n = num_samples
    Xtr, Ytr = X[:int(0.8*n)], Y[:int(0.8*n)]
    Xdev, Ydev = X[int(0.8*n):int(0.9*n)], Y[int(0.8*n):int(0.9*n)]
    Xte, Yte = X[int(0.9*n):], Y[int(0.9*n):]
    return Xtr, Ytr, Xdev, Ydev, Xte, Yte

# -------------------------- 3. 初始化模型参数 --------------------------
g = torch.Generator().manual_seed(2147483647)
C = torch.randn((vocab_size, n_embd), generator=g)
W1 = torch.randn((block_size * n_embd, hidden_dim), generator=g)
b1 = torch.randn(hidden_dim, generator=g)
W2 = torch.randn((hidden_dim, vocab_size), generator=g)
b2 = torch.randn(vocab_size, generator=g)
parameters = [C, W1, b1, W2, b2]
for p in parameters:
    p.requires_grad = True

# -------------------------- 4. 训练模型 + 监控与可视化 --------------------------
Xtr, Ytr, Xdev, Ydev, Xte, Yte = generate_dataset()
batch_size = 32
total_steps = 20000
train_losses = []
dev_losses = []

for step in range(total_steps):
    # 小批量采样
    ix = torch.randint(0, Xtr.shape[0], (batch_size,))
    Xb, Yb = Xtr[ix], Ytr[ix]
    
    # 前向传播
    emb = C[Xb]
    h = torch.tanh(emb.view(-1, block_size * n_embd) @ W1 + b1)
    logits = h @ W2 + b2
    loss = F.cross_entropy(logits, Yb)
    
    # 反向传播
    for p in parameters:
        p.grad = None
    loss.backward()
    
    # 参数更新(动态学习率)
    lr = 0.1 if step < 10000 else 0.01
    for p in parameters:
        p.data += -lr * p.grad
    
    # 记录损失(每100步)
    if step % 100 == 0:
        train_losses.append(loss.log10().item())
        # 验证集损失(无梯度)
        with torch.no_grad():
            emb_dev = C[Xdev[:batch_size]]
            h_dev = torch.tanh(emb_dev.view(-1, block_size * n_embd) @ W1 + b1)
            logits_dev = h_dev @ W2 + b2
            dev_loss = F.cross_entropy(logits_dev, Ydev[:batch_size])
            dev_losses.append(dev_loss.log10().item())
    
    # 打印进度
    if step % 2000 == 0:
        print(f"Step {step:5d} | Train Loss: {loss.item():.4f} | Dev Loss: {dev_loss.item():.4f} | LR: {lr}")

# 绘制损失曲线
plt.figure(figsize=(10, 5))
plt.plot(train_losses, label='Train Loss (log10)', alpha=0.7)
plt.plot(dev_losses, label='Validation Loss (log10)', alpha=0.7)
plt.xlabel('Steps (×100)')
plt.ylabel('Loss (log10)')
plt.title('Training vs Validation Loss')
plt.legend()
plt.grid(alpha=0.3)
plt.show()

print("训练完成!")

# -------------------------- 5. 采样生成 --------------------------
def generate_names(num_names=20):
    g_gen = torch.Generator().manual_seed(2147483647 + 10)
    for _ in range(num_names):
        out = []
        context = [0] * block_size
        while True:
            emb = C[torch.tensor([context])]
            h = torch.tanh(emb.view(1, -1) @ W1 + b1)
            logits = h @ W2 + b2
            probs = F.softmax(logits, dim=1)
            ix = torch.multinomial(probs, num_samples=1, generator=g_gen).item()
            context = context[1:] + [ix]
            out.append(ix)
            if ix == 0:
                break
        name = ''.join([itos[i] for i in out[:-1]])
        print(name)

print("\n生成的名字:")
generate_names()

系数调整标准差的正态分布初始化和 Batch Norm(训练用批次均值 / 方差并更新滑动统计量、测试用训练累计的滑动均值 / 方差)

import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt

# ===================== 1. 基础参数定义 =====================
vocab_size = 27        # 字符类别数(0-26)
n_embed = 10           # 字符嵌入维度
n_hidden = 200         # 隐藏层神经元数
batch_size = 32
max_steps = 20000      # 训练步数(可调整)
context_length = 8     # 上下文长度
seed = 2147483647      # 固定随机种子

# ===================== 2. 生成模拟数据集 =====================
g = torch.Generator().manual_seed(seed)
Xtr = torch.randint(0, vocab_size, (10000, context_length), generator=g)  # 训练集
Ytr = torch.randint(0, vocab_size, (10000,), generator=g)
Xte = torch.randint(0, vocab_size, (1000, context_length), generator=g)   # 测试集
Yte = torch.randint(0, vocab_size, (1000,), generator=g)

# ===================== 3. 模型参数初始化 =====================
g = torch.Generator().manual_seed(seed)
C = torch.randn((vocab_size, n_embed), generator=g)  # 嵌入矩阵

# 线性层W1(带系数调整标准差)
W1 = torch.randn((context_length * n_embed, n_hidden), generator=g) * (5/3) / ((n_embed * context_length) ** 0.5) * 0.2
b1 = torch.randn(n_hidden, generator=g) * 0.01  # 偏置(后续Batch Norm会抵消,可保留)
# 线性层W2(带系数调整标准差)
W2 = torch.randn((n_hidden, vocab_size), generator=g) * 0.1
b2 = torch.randn(vocab_size, generator=g) * 0.01

# Batch Norm参数
bn_gain = torch.ones((1, n_hidden))    # BN放缩系数
bn_bias = torch.zeros((1, n_hidden))   # BN偏移系数
bn_mean_running = torch.zeros((1, n_hidden))  # 滑动均值
bn_std_running = torch.ones((1, n_hidden))    # 滑动方差

# 参数列表
parameters = [C, W1, b1, W2, b2, bn_gain, bn_bias]
print(f"总参数数量: {sum(p.numel() for p in parameters):,}")
for p in parameters:
    p.requires_grad = True

# ===================== 4. 前向传播函数(含tanh乘5/3) =====================
def forward_pass(x, is_train=True):
    # 嵌入层
    emb = C[x]  # (batch_size, context_length, n_embed)
    embcat = emb.view(emb.shape[0], -1)  # (batch_size, context_length*n_embed)
    
    # 线性层1(预激活值)
    hpreact = embcat @ W1 + b1  # (batch_size, n_hidden)
    
    # Batch Norm层
    if is_train:
        bn_mean = hpreact.mean(0, keepdim=True)
        bn_std = hpreact.std(0, keepdim=True) + 1e-5
        hpreact = bn_gain * (hpreact - bn_mean) / bn_std + bn_bias
        # 更新滑动统计量
        with torch.no_grad():
            global bn_mean_running, bn_std_running
            bn_mean_running = 0.999 * bn_mean_running + 0.001 * bn_mean
            bn_std_running = 0.999 * bn_std_running + 0.001 * bn_std
    else:
        hpreact = bn_gain * (hpreact - bn_mean_running) / (bn_std_running + 1e-5) + bn_bias
    
    # 非线性激活(tanh 乘5/3)
    h = torch.tanh(hpreact) * (5/3)
    
    # 输出层
    logits = h @ W2 + b2
    return logits

# ===================== 5. 训练循环 =====================
lossi = []
print("\n开始训练...")
for i in range(max_steps):
    # 构造小批次
    ix = torch.randint(0, Xtr.shape[0], (batch_size,), generator=g)
    Xb, Yb = Xtr[ix], Ytr[ix]
    
    # 前向传播
    logits = forward_pass(Xb, is_train=True)
    loss = F.cross_entropy(logits, Yb)
    lossi.append(loss.log10().item())
    
    # 反向传播+更新
    for p in parameters:
        p.grad = None
    loss.backward()
    lr = 0.1 if i < max_steps//2 else 0.01
    for p in parameters:
        p.data += -lr * p.grad
    
    # 日志
    if i % 1000 == 0:
        acc = (logits.argmax(1) == Yb).float().mean().item()
        print(f"步骤 {i:6d}/{max_steps}, 损失: {loss.item():.4f}, 训练准确率: {acc:.3f}")

# ===================== 6. 测试阶段 =====================
print("\n开始测试...")
with torch.no_grad():
    logits_te = forward_pass(Xte, is_train=False)
    loss_te = F.cross_entropy(logits_te, Yte)
    acc_te = (logits_te.argmax(1) == Yte).float().mean().item()
print(f"测试损失: {loss_te.item():.4f}, 测试准确率: {acc_te:.3f}")

# ===================== 7. 损失曲线可视化 =====================
plt.figure(figsize=(10, 4))
plt.plot(torch.tensor(lossi).view(-1, 100).mean(1))
plt.title("训练损失曲线(log10)")
plt.xlabel("步骤(×100)")
plt.ylabel("损失(log10)")
plt.grid(True)
plt.show()

封装成模块化的Pytorch格式的神经网络:

# 导入依赖库
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt

# 1. 数据集定义
# 步骤1:定义字符集(26个小写字母 + 1个空格,共27类,对应vocab_size=27)
chars = [chr(ord('a') + i) for i in range(26)] + [' ']
vocab_size = len(chars)  # vocab_size=27

# 步骤2:字符→索引映射(用于将字符转成数字)
char_to_idx = {c: i for i, c in enumerate(chars)}
idx_to_char = {i: c for i, c in enumerate(chars)}

# 步骤3:生成模拟训练数据(字符序列预测任务:输入8个字符,预测下1个字符)
context_length = 8  # 上下文长度(和网络输入维度匹配)
n_samples = 100000  # 训练样本数(足够多保证训练稳定)
g = torch.Generator().manual_seed(2147483647)  # 固定种子,复现结果

# 生成随机字符序列(先生成所有样本的字符索引,再拆分输入和目标)
# 总字符数 = 样本数 × (上下文长度 + 1)(每个样本需要8个输入+1个目标)
total_chars = n_samples * (context_length + 1)
random_chars_idx = torch.randint(0, vocab_size, (total_chars,), generator=g)

# 拆分Xtr(输入)和Ytr(目标)
# Xtr: (n_samples, context_length) → 每个样本8个字符
# Ytr: (n_samples,) → 每个样本对应1个目标字符(输入序列的下一个字符)
Xtr = random_chars_idx[:-n_samples].view(n_samples, context_length)
Ytr = random_chars_idx[context_length:].view(n_samples)

# 验证数据形状(确保正确)
print(f"Xtr形状: {Xtr.shape} (样本数, 上下文长度)")
print(f"Ytr形状: {Ytr.shape} (样本数)")
print(f"示例输入(索引): {Xtr[0]} → 对应字符: {''.join([idx_to_char[i.item()] for i in Xtr[0]])}")
print(f"示例目标(索引): {Ytr[0]} → 对应字符: {idx_to_char[Ytr[0].item()]}")

# 2. 定义网络模块类
class Linear:
    """线性层:模拟PyTorch的nn.Linear,实现 y = x @ weight + bias"""
    def __init__(self, fan_in, fan_out, bias=True):
        # fan_in: 输入特征维度;fan_out: 输出特征维度;bias: 是否使用偏置
        # 权重初始化:正态分布 + 除以sqrt(fan_in),让输出标准差接近1
        self.weight = torch.randn((fan_in, fan_out), generator=g) / fan_in**0.5
        # 偏置初始化:小幅度正态分布(0.01),避免初始偏移过大
        self.bias = torch.randn(fan_out, generator=g) * 0.01 if bias else None

    def __call__(self, x):
        # 前向传播:矩阵乘法 + 偏置(如果有)
        self.out = x @ self.weight  # 核心计算:(batch, fan_in) @ (fan_in, fan_out) → (batch, fan_out)
        if self.bias is not None:
            self.out += self.bias  # 加偏置(广播机制:(batch, fan_out) + (fan_out,))
        return self.out

    def parameters(self):
        # 返回该层可学习参数(供后续收集)
        return [self.weight] + ([] if self.bias is None else [self.bias])


class BatchNorm1d:
    """1维批量归一化:模拟PyTorch的nn.BatchNorm1d,稳定训练"""
    def __init__(self, dim, eps=1e-5, momentum=0.1):
        # dim: 输入特征维度;eps: 防止除0的小值;momentum: 滑动平均动量
        self.eps = eps
        self.momentum = momentum
        self.training = True  # 标记训练/测试模式(默认训练)
        
        # 可学习参数:gamma(放缩系数)、beta(偏移系数)
        self.gamma = torch.ones(dim)  # 初始放缩系数1(不改变归一化结果)
        self.beta = torch.zeros(dim)  # 初始偏移系数0(不改变归一化结果)
        
        # 滑动统计量:训练中累计全局均值/方差,测试时使用
        self.running_mean = torch.zeros(dim)  # 初始均值0
        self.running_var = torch.ones(dim)    # 初始方差1

    def __call__(self, x):
        # 前向传播:根据训练/测试模式选择不同的均值/方差
        if self.training:
            # 训练模式:用当前批次的均值/方差(更贴近实时数据分布)
            xmean = x.mean(0, keepdim=True)  # 按特征维度算均值:(1, dim)
            xvar = x.var(0, keepdim=True, unbiased=True)  # 按特征维度算方差(无偏估计)
        else:
            # 测试模式:用训练累计的滑动统计量(避免测试批次依赖)
            xmean = self.running_mean
            xvar = self.running_var
        
        # 核心归一化:(x - 均值) / sqrt(方差 + eps) → 归一化到均值0、方差1
        xhat = (x - xmean) / torch.sqrt(xvar + self.eps)
        # 可学习的放缩+偏移:恢复特征表达能力(避免强制归一化导致的信息丢失)
        self.out = self.gamma * xhat + self.beta
        
        # 训练模式下更新滑动统计量(不参与梯度计算,避免影响训练)
        if self.training:
            with torch.no_grad():
                # 滑动平均公式:新统计量 = (1-动量)×旧统计量 + 动量×当前批次统计量
                self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * xmean
                self.running_var = (1 - self.momentum) * self.running_var + self.momentum * xvar
        return self.out

    def parameters(self):
        # 返回该层可学习参数(gamma和beta)
        return [self.gamma, self.beta]


class Tanh:
    """Tanh激活函数:模拟PyTorch的nn.Tanh,引入非线性"""
    def __call__(self, x):
        # 前向传播:tanh(x),输出范围[-1, 1]
        self.out = torch.tanh(x)
        return self.out

    def parameters(self):
        # 激活函数无参数,返回空列表
        return []

# 3. 初始化网络参数与构建网络
n_embed = 10  # 字符嵌入维度(每个字符转成10维向量)
n_hidden = 100  # 隐藏层神经元数
g = torch.Generator().manual_seed(2147483647)  # 固定种子,复现结果

# 嵌入矩阵:C[vocab_size, n_embed] → 每个字符对应一个10维向量
C = torch.randn((vocab_size, n_embed), generator=g)

# 构建网络(模拟PyTorch的nn.Sequential,按顺序堆叠层)
# 输入维度:context_length × n_embed = 8×10=80(8个字符,每个10维)
layers = [
    Linear(n_embed * context_length, n_hidden), Tanh(),  # 输入层→隐藏层1
    Linear(n_hidden, n_hidden), Tanh(),                  # 隐藏层1→隐藏层2
    Linear(n_hidden, n_hidden), Tanh(),                  # 隐藏层2→隐藏层3
    Linear(n_hidden, n_hidden), Tanh(),                  # 隐藏层3→隐藏层4
    Linear(n_hidden, n_hidden), Tanh(),                  # 隐藏层4→隐藏层5
    Linear(n_hidden, vocab_size),                        # 隐藏层5→输出层(27类字符)
]

# 权重初始化修正(老师的关键技巧,提升训练稳定性)
with torch.no_grad():
    layers[-1].weight *= 0.1  # 最后一层权重缩小10倍:避免输出logits过大,损失爆炸
    # 其他Linear层权重乘5/3:Tanh默认输出标准差≈0.6,乘5/3后≈1,契合初始化目标
    for layer in layers[:-1]:
        if isinstance(layer, Linear):
            layer.weight *= 5/3

# 收集所有可学习参数(嵌入矩阵C + 所有层的参数)
parameters = [C] + [p for layer in layers for p in layer.parameters()]
print(f"\n总可学习参数数量: {sum(p.numel() for p in parameters):,}")  # 输出参数总数,方便监控
for p in parameters:
    p.requires_grad = True  # 开启梯度计算(允许反向传播更新)

# 4. 训练循环
max_steps = 200000  # 总训练步数(足够多保证收敛)
batch_size = 32     # 批次大小(平衡训练速度和稳定性)
lossi = []          # 记录损失值,用于后续可视化

print("\n开始训练...")
for i in range(max_steps):
    # 步骤1:采样小批次数据(随机选择batch_size个样本)
    ix = torch.randint(0, Xtr.shape[0], (batch_size,), generator=g)
    Xb, Yb = Xtr[ix], Ytr[ix]  # Xb: (32,8), Yb: (32,)

    # 步骤2:前向传播(从嵌入层到输出层)
    emb = C[Xb]  # 字符嵌入:(32,8) → (32,8,10)(每个字符转10维向量)
    x = emb.view(emb.shape[0], -1)  # 拼接输入:(32,8,10) → (32,80)(展平成80维向量)
    for layer in layers:
        x = layer(x)  # 逐层前向传播:(32,80) → ... → (32,27)
    loss = F.cross_entropy(x, Yb)  # 计算交叉熵损失(分类任务标准损失)

    # 步骤3:反向传播(梯度计算)
    for p in parameters:
        p.grad = None  # 梯度清零:避免上一轮梯度累积
    loss.backward()  # 自动计算所有参数的梯度

    # 步骤4:参数更新(SGD优化器,学习率衰减)
    lr = 0.1 if i < 100000 else 0.01  # 前10万步用0.1(快速收敛),后10万步用0.01(精细调整)
    for p in parameters:
        p.data += -lr * p.grad  # SGD更新公式:参数 = 参数 - 学习率×梯度

    # 步骤5:记录损失与打印日志
    if i % 10000 == 0:  # 每1万步打印一次,监控训练进度
        print(f"步骤 {i:6d}/{max_steps}, 损失值: {loss.item():.4f}")
    lossi.append(loss.log10().item())  # 记录log10(损失),让可视化更清晰

# 5. 可视化工具(激活值分布+梯度分布)
# 可视化1:Tanh层激活值分布(检查是否饱和)
plt.figure(figsize=(20, 4))
legends = []
for i, layer in enumerate(layers[:-1]):  # 排除输出层,只看Tanh层
    if isinstance(layer, Tanh):
        # 提取Tanh层的输出(detach()脱离计算图,避免影响梯度)
        t = layer.out.detach()
        # 打印关键统计信息:均值、标准差、饱和比例(|t|>0.97视为饱和,饱和会导致梯度消失)
        print(f"\nTanh层 {i}: 均值={t.mean():.2f}, 标准差={t.std():.2f}, 饱和比例={(t.abs() > 0.97).float().mean()*100:.2f}%")
        # 绘制直方图(密度分布)
        hy, hx = torch.histogram(t, density=True)
        plt.plot(hx[:-1].detach(), hy.detach())
        legends.append(f"Tanh层 {i}")
plt.legend(legends)
plt.title("各Tanh层激活值分布(越接近正态分布越好,饱和比例越低越好)")
plt.xlabel("激活值")
plt.ylabel("密度")
plt.show()

# 可视化2:权重梯度分布(检查训练是否稳定)
plt.figure(figsize=(20, 4))
legends = []
for i, p in enumerate(parameters):
    if p.ndim == 2:  # 只看2维权重(嵌入矩阵C和Linear层权重,偏置是1维,忽略)
        # 提取梯度(detach()脱离计算图)
        t = p.grad.detach().view(-1)
        # 打印关键统计信息:均值(接近0最好)、标准差(合理范围0.001~0.1)
        print(f"\n权重 {tuple(p.shape)}: 梯度均值={t.mean():.4f}, 梯度标准差={t.std():.4f}")
        # 绘制直方图(密度分布)
        hy, hx = torch.histogram(t, density=True)
        plt.plot(hx[:-1].detach(), hy.detach())
        legends.append(f"权重 {tuple(p.shape)}")
plt.legend(legends)
plt.title("各权重梯度分布(均值接近0、分布集中为稳定)")
plt.xlabel("梯度值")
plt.ylabel("密度")
plt.show()

# 可视化3:训练损失曲线(检查是否收敛)
plt.figure(figsize=(10, 4))
# 每100步取平均,让曲线更平滑(方便观察趋势)
plt.plot(torch.tensor(lossi).view(-1, 100).mean(1))
plt.title("训练损失曲线(log10尺度)")
plt.xlabel("步骤(×100)")
plt.ylabel("log10(损失值)")
plt.grid(True)
plt.show()

封装修改成Wavenet,并读入真实数据集验证:

import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt

# --------------------------
# 1. 数据准备
# --------------------------
# 读取训练数据(名字列表)
words = open('names.txt', 'r').read().splitlines()
# 提取所有出现的字符,并排序
chars = sorted(list(set(''.join(words))))
# 创建字符到索引的映射,从1开始,'.'作为特殊字符对应索引0
stoi = {s:i+1 for i,s in enumerate(chars)}
stoi['.'] = 0
# 创建索引到字符的反向映射
itos = {i:s for s,i in stoi.items()}
# 词汇表大小
vocab_size = len(itos)

# 定义上下文窗口大小(用前8个字符预测下一个)
block_size = 8
def build_dataset(words):  
    X, Y = [], []
    for w in words:
        # 初始化上下文,前面用block_size个0(对应字符'.')
        context = [0] * block_size
        for ch in w + '.':  # 每个名字后面加上终止符'.'
            ix = stoi[ch]
            X.append(context)
            Y.append(ix)
            # 滑动窗口:去掉最前面一个字符,加入当前字符
            context = context[1:] + [ix]
    # 转换为PyTorch张量
    X = torch.tensor(X)
    Y = torch.tensor(Y)
    return X, Y

import random
random.seed(42)  # 设置随机种子保证可复现
random.shuffle(words)  # 打乱名字列表
n1 = int(0.8*len(words))  # 80%作为训练集
n2 = int(0.9*len(words))  # 10%作为验证集,10%作为测试集
Xtr, Ytr = build_dataset(words[:n1])
Xdev, Ydev = build_dataset(words[n1:n2])
Xte, Yte = build_dataset(words[n2:])

# --------------------------
# 2. 定义模型组件
# --------------------------
class Linear:
    """全连接层"""
    def __init__(self, fan_in, fan_out, bias=True):
        # 初始化权重,使用Xavier初始化
        self.weight = torch.randn((fan_in, fan_out)) / fan_in**0.5
        self.bias = torch.zeros(fan_out) if bias else None
    def __call__(self, x):
        self.out = x @ self.weight
        if self.bias is not None:
            self.out += self.bias
        return self.out
    def parameters(self):
        return [self.weight] + ([] if self.bias is None else [self.bias])

class BatchNorm1d:
    """1D批量归一化层"""
    def __init__(self, dim, eps=1e-5, momentum=0.1):
        self.eps = eps  # 防止除以0
        self.momentum = momentum  # 移动平均的动量
        self.training = True  # 标记是否在训练模式
        self.gamma = torch.ones(dim)  # 缩放参数
        self.beta = torch.zeros(dim)  # 偏移参数
        # 用于推理时的移动平均均值和方差
        self.running_mean = torch.zeros(dim)
        self.running_var = torch.ones(dim)
    def __call__(self, x):
        if self.training:
            # 训练时计算当前batch的均值和方差
            xmean = x.mean(0, keepdim=True)
            xvar = x.var(0, keepdim=True)
        else:
            # 推理时使用移动平均得到的均值和方差
            xmean = self.running_mean
            xvar = self.running_var
        # 归一化
        xhat = (x - xmean) / torch.sqrt(xvar + self.eps)
        self.out = self.gamma * xhat + self.beta  # 缩放和偏移
        if self.training:
            # 更新移动平均
            with torch.no_grad():
                self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * xmean
                self.running_var = (1 - self.momentum) * self.running_var + self.momentum * xvar
        return self.out
    def parameters(self):
        return [self.gamma, self.beta]

class Tanh:
    """Tanh激活函数层"""
    def __call__(self, x):
        self.out = torch.tanh(x)
        return self.out
    def parameters(self):
        return []

class Embedding:
    """词嵌入层"""
    def __init__(self, num_embeddings, embedding_dim):
        self.weight = torch.randn((num_embeddings, embedding_dim))
    def __call__(self, IX):
        # 根据索引IX取出对应的嵌入向量
        self.out = self.weight[IX]
        return self.out
    def parameters(self):
        return [self.weight]

class FlattenConsecutive:
    """将连续的n个时间步的特征拼接到一起,是WaveNet的核心"""
    def __init__(self, n):
        self.n = n  # 每次合并n个时间步
    def __call__(self, x):
        B, T, C = x.shape  # 输入形状:[Batch, Time, Channel]
        # 重新排列形状:将T个时间步分成T//n组,每组n个时间步的C个通道拼接在一起
        x = x.view(B, T//self.n, C*self.n)
        # 如果合并后时间维度为1,就把它去掉
        if x.shape[1] == 1:
            x = x.squeeze(1)
        self.out = x
        return self.out
    def parameters(self):
        return []

class Sequential:
    """容器,按顺序堆叠各个层"""
    def __init__(self, layers):
        self.layers = layers
    def __call__(self, x):
        for layer in self.layers:
            x = layer(x)
        self.out = x
        return self.out
    def parameters(self):
        # 收集所有层的参数
        return [p for layer in self.layers for p in layer.parameters()]

# --------------------------
# 3. 构建WaveNet模型
# --------------------------
n_embd = 24  # 字符嵌入的维度
n_hidden = 128  # 隐藏层的神经元数量
model = Sequential([
    Embedding(vocab_size, n_embd),  # 字符嵌入层
    # 第1个残差块:合并2个时间步 -> 全连接 -> BN -> Tanh
    FlattenConsecutive(2), Linear(n_embd * 2, n_hidden, bias=False), BatchNorm1d(n_hidden), Tanh(),
    # 第2个残差块:合并2个时间步 -> 全连接 -> BN -> Tanh
    FlattenConsecutive(2), Linear(n_hidden * 2, n_hidden, bias=False), BatchNorm1d(n_hidden), Tanh(),
    # 第3个残差块:合并2个时间步 -> 全连接 -> BN -> Tanh
    FlattenConsecutive(2), Linear(n_hidden * 2, n_hidden, bias=False), BatchNorm1d(n_hidden), Tanh(),
    Linear(n_hidden, vocab_size),  # 输出层,预测下一个字符的概率
])

# --------------------------
# 4. 参数初始化
# --------------------------
with torch.no_grad():
    # 最后一层权重乘以0.1,让初始输出更平缓,避免一开始就过于自信
    model.layers[-1].weight *= 0.1
# 收集所有可训练参数
parameters = model.parameters()
print(f"总参数量: {sum(p.nelement() for p in parameters)}")
for p in parameters:
    p.requires_grad = True

# --------------------------
# 5. 训练模型
# --------------------------
max_steps = 200000  # 训练总步数
batch_size = 32  # 批大小
lossi = []  # 记录损失

for i in range(max_steps):
    # 构建minibatch:随机选择batch_size个样本
    ix = torch.randint(0, Xtr.shape[0], (batch_size,))
    Xb, Yb = Xtr[ix], Ytr[ix]
    
    # 前向传播,计算logits
    logits = model(Xb)
    # 计算交叉熵损失
    loss = F.cross_entropy(logits, Yb)
    
    # 反向传播
    for p in parameters:
        p.grad = None  # 清空梯度
    loss.backward()  # 计算梯度
    
    # 更新参数:学习率前15万步用0.1,之后用0.01
    lr = 0.1 if i < 150000 else 0.01
    for p in parameters:
        p.data += -lr * p.grad
    
    # 跟踪损失,每1万步打印一次
    if i % 10000 == 0:
        print(f'{i:7d}/{max_steps:7d}: {loss.item():.4f}')
    lossi.append(loss.log10().item())

# --------------------------
# 6. 评估模型
# --------------------------
# 将所有BatchNorm层设置为评估模式
model.layers[1].training = False
model.layers[3].training = False
model.layers[5].training = False
model.layers[7].training = False

@torch.no_grad()  # 评估时不需要计算梯度
def split_loss(split):
    x, y = {'train': (Xtr, Ytr), 'val': (Xdev, Ydev), 'test': (Xte, Yte)}[split]
    logits = model(x)
    loss = F.cross_entropy(logits, y)
    print(f"{split} loss: {loss.item():.4f}")

# 计算训练集、验证集的损失
split_loss('train')
split_loss('val')

# --------------------------
# 7. 生成新名字
# --------------------------
for _ in range(20):  # 生成20个名字
    out = []
    context = [0] * block_size  # 初始上下文
    while True:
        # 预测下一个字符
        logits = model(torch.tensor([context]))
        probs = F.softmax(logits, dim=1)  # 转换为概率
        ix = torch.multinomial(probs, num_samples=1).item()  # 按概率采样
        # 更新上下文
        context = context[1:] + [ix]
        out.append(ix)
        if ix == 0:  # 遇到终止符'.'就停止
            break
    # 将索引转换为字符并打印
    print(''.join(itos[i] for i in out))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值