维度对齐训练:
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))

1770

被折叠的 条评论
为什么被折叠?



