
前置程序
词嵌入
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import string
# 定义字典
char2indx = {s: i for i, s in enumerate(string.ascii_lowercase)}
print(char2indx)
example = 'love'
idx = []
for i in example:
idx.append(char2indx[i])
idx = torch.tensor(idx)
print(idx)
# 使用独热编码,将文本转换为二维张量
num_claz = len(char2indx.keys())
x = F.one_hot(idx, num_classes=num_claz).float()
print(x,x.shape)
dims = 5
num_claz = len(char2indx.keys())
x = F.one_hot(idx, num_classes=num_claz).float()
w = torch.randn(num_claz, dims)
对26个小写字符进行26维向量化映射,并输出示例对应的独热向量,再尝试对26维进行压缩,即使用自定义矩阵 对 通过向量化长度为n的字符串得到的n*26的矩阵 进行矩阵的空间变换。若所需压缩维度为m,则进行矩阵乘法@:x@w=n\*26@26\*m。
python
{'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9, 'k': 10, 'l': 11, 'm': 12, 'n': 13, 'o': 14, 'p': 15, 'q': 16, 'r': 17, 's': 18, 't': 19, 'u': 20, 'v': 21, 'w': 22, 'x': 23, 'y': 24, 'z': 25}
tensor([11, 14, 21, 4])
tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0.]]) torch.Size([4, 26])
tensor([[ 0.8121, 1.7843, -1.8114, -0.1955, -0.1218],
[ 0.7272, -0.1841, -1.0490, 0.5851, -0.5069],
[-1.1661, 1.7609, -0.5384, 1.6000, -0.6927],
[ 0.5573, -0.7052, 0.6820, 0.8804, -0.9916]]) torch.Size([4, 5])
这里在python环境下可以简化代码的计算流程:
python
w[idx], w[idx].shape
对于pytorch可以将用于制作独热编码长度为n的idx直接传给形状为w.shape=dim*m的变换矩阵x,自动生成独热编码后降维为目标矩阵。故可以以此编写实现上述嵌入功能的函数:
python
class Embedding:
def __init__(self, num_embeddings, embedding_dims):
self.weight = torch.randn((num_embeddings, embedding_dims), requires_grad=True)
def __call__(self, x):
self.out = self.weight[x]
return self.out
def parameters(self):
return [self.weight]
编解码
python
class CharTokenizer:
def __init__(self, data, begin_ind=0, end_ind=1):
# data: list[str]
# 得到所有的字符
chars = sorted(list(set(''.join(data))))
self.char2ind = {s: i + 2 for i, s in enumerate(chars)}
self.char2ind['<|b|>'] = begin_ind
self.char2ind['<|e|>'] = end_ind
self.ind2char = {v: k for k, v in self.char2ind.items()}
self.begin_ind = begin_ind
self.end_ind = end_ind
def encode(self, x):
# x: str
return [self.char2ind[i] for i in x]
def decode(self, x):
# x: int or list[x]
if isinstance(x, int):
return self.ind2char[x]
return [self.ind2char[i] for i in x]
#测试
tokenizer = CharTokenizer(datasets['whole_func_string'])
test_str = 'def f(x):'
re = tokenizer.encode(test_str)
print(re)
print(''.join(tokenizer.decode(re)))
单文本训练数据生成
python
def autoregressive_trans(text, tokenizer, context_length=10):
# text: str
inputs, labels = [], []
bind = tokenizer.begin_ind
eind = tokenizer.end_ind
enc = tokenizer.encode(text)
# 增加特殊字符
data = [bind] * context_length + enc + [eind]
for i in range(len(data) - context_length):
inputs.append(data[i: i + context_length])
labels.append(data[i + context_length])
return inputs, labels
#测试
inputs, labels = autoregressive_trans(test_str, tokenizer, 3)
for a, b in zip(inputs, labels):
print(f'{"".join(tokenizer.decode(a))} -----> {tokenizer.decode(b)}')
autoregressive_trans 函数用于将一段文本转换为自回归语言模型(Autoregressive Language Model)的训练样本。
函数首先利用 tokenizer.encode() 将输入文本编码为对应的 token 序列,并在序列前添加 context_length 个起始标记(begin_ind),末尾添加结束标记(end_ind),以便模型学习文本的起始和结束信息。
随后采用滑动窗口(Sliding Window)方式,以长度为 context_length 的连续 token 序列作为模型输入 inputs,将紧随其后的下一个 token 作为预测目标 labels,不断向后滑动直到遍历完整个序列,从而构造出大量"前文预测下一个词"的训练样本。
这种处理方式符合自回归模型"根据历史上下文预测下一个 token"的训练机制,使模型能够学习文本的时序依赖关系。最后的测试代码通过设置较小的上下文长度(context_length=3),并利用 tokenizer.decode() 将 token 序列还原为文本,直观展示了每个输入序列与对应预测目标之间的映射关系,例如"前三个字符 → 预测第四个字符",便于验证数据预处理过程是否正确。
基于数据集的数据生成
python
#需要处理的数据可能是多列文本,所以需要分类处理
def process(data, tokenizer):
text = data['whole_func_string']
# text: str
if isinstance(text, str):
inputs, labels = autoregressive_trans(text, tokenizer)
return {'inputs': inputs, 'labels': labels}
# text: list[str]
inputs, labels = [], []
for t in text:
i, l = autoregressive_trans(t, tokenizer)
inputs += i
labels += l
return {'inputs': inputs, 'labels': labels}
#测试
process(datasets[8], tokenizer)
process(datasets[8: 9], tokenizer)
该函数 process 用于对数据集中的文本进行统一预处理,并兼容单条样本和批量样本两种输入形式。
首先从数据中读取 whole_func_string 字段,并通过 isinstance(text, str) 判断当前输入是否为单条文本:若为字符串,则直接调用 autoregressive_trans() 将文本转换为自回归模型所需的输入序列 inputs 和训练标签 labels;
若输入为文本列表(即 batched=True 时传入的一个批次数据),则依次遍历列表中的每条文本,对每条文本执行相同的转换操作,并将所有样本的 inputs 和 labels 合并到两个列表中返回。
最终,该函数统一输出包含 inputs 和 labels 的字典,便于后续使用 Dataset.map() 对整个数据集进行批量预处理。最后两行测试代码分别验证了函数对单个样本(datasets8)和单样本批次(datasets8:9)两种输入格式均能正确完成处理,说明该函数具有良好的通用性和兼容性。
数据准备
python
# 将数据分为训练集和测试集
tokenized = datasets.train_test_split(test_size=0.1, seed=1024, shuffle=True)
f = lambda x: process(x, tokenizer)
tokenized = tokenized.map(f, batched=True, remove_columns=datasets.column_names)
tokenized.set_format(type='torch', device=device)
#测试
print(tokenized['train']['inputs'].shape, tokenized['train']['labels'].shape)
train_loader = DataLoader(tokenized['train'], batch_size=batch_size, shuffle=True)
test_loader = DataLoader(tokenized['test'], batch_size=batch_size, shuffle=True)
#测试
print(next(iter(train_loader)))
| 代码 | 作用 |
|---|---|
f = lambda x: process(x, tokenizer) |
定义一个匿名函数,将数据批次交给 process() 并传入 tokenizer。 |
tokenized.map(f, batched=True, remove_columns=...) |
对整个数据集批量执行分词/预处理,并删除原始列,生成新的特征列。 |
tokenized.set_format(type="torch", device=device) |
将数据集输出格式设为 PyTorch Tensor(可直接放到指定设备),方便后续 DataLoader 和模型训练。 |
模型定义
python
class CharMLP(nn.Module):
def __init__(self, vs):
# vs 字典大小
super().__init__()
self.emb = nn.Embedding(vs, 30)
self.hidden1 = nn.Linear(10 * 30, 200)
self.hidden2 = nn.Linear(200, 100)
self.lm = nn.Linear(100, vs)
def forward(self, x):
# x: (B, 10) 10为窗口大小
B = x.shape[0]
emb = self.emb(x) # (B, 10, 30)
h = emb.view(B, -1) # (B, 300)
h = F.relu(self.hidden1(h)) # (B, 200)
h = F.relu(self.hidden2(h)) # (B, 100)
out = self.lm(h) # (B, vs)
return out
#测试
model = CharMLP(len(tokenizer.char2ind)).to(device)
python
CharMLP(
(emb): Embedding(99, 30)
(hidden1): Linear(in_features=300, out_features=200, bias=True)
(hidden2): Linear(in_features=200, out_features=100, bias=True)
(lm): Linear(in_features=100, out_features=99, bias=True)
)
这部分和之前的差不多,不说了
评估函数定义
python
def estimate_loss(model):
re = {}
# 将模型切换至评估模式
model.eval()
re['train'] = _loss(model, train_loader)
re['test'] = _loss(model, test_loader)
# 将模型切换至训练模式
model.train()
return re
@torch.no_grad()
def _loss(model, data_loader):
"""
计算模型在不同数据集下面的评估指标
"""
loss = []
data_iter= iter(data_loader)
# 随机使用多个批量数据来预估模型效果
for k in range(eval_iters):
data = next(data_iter, None)
if data is None:
data_iter = iter(data_loader)
data = next(data_iter, None)
inputs, labels = data['inputs'], data['labels']
logits = model(inputs)
loss.append(F.cross_entropy(logits, labels).item())
return torch.tensor(loss).mean().item()
estimate_loss(model)
(1)estimate_loss 函数作用
estimate_loss 用于评估模型在训练集和测试集上的损失情况。函数首先将模型切换到评估模式(model.eval()),分别调用 _loss 计算训练集和测试集的平均损失,并将结果保存到字典中;最后再调用 model.train() 恢复训练模式,确保后续模型能够继续正常训练。
(2)@torch.no_grad() 装饰器
@torch.no_grad() 表示在评估过程中关闭梯度计算,不会构建计算图,也不会更新模型参数,从而减少显存占用并提高推理速度。这是模型验证和测试阶段的标准做法。
(3)_loss 函数作用
_loss 用于计算指定数据集上的平均损失值。函数首先创建一个空列表用于保存每个批次的损失,然后从 DataLoader 中依次读取数据。当数据遍历结束时,会重新创建迭代器继续取数据,保证能够连续计算 eval_iters 个批次的损失。
(4)损失计算过程
对于每个批次的数据,函数读取输入 inputs 和标签 labels,将输入送入模型得到预测结果 logits,随后利用交叉熵损失函数 F.cross_entropy() 计算预测结果与真实标签之间的误差,并将每个批次的损失值保存到列表中。
(5)返回评估结果
所有批次计算完成后,函数对损失列表求平均值,并返回一个标量作为该数据集的平均损失。最终调用 estimate_loss(model) 后,将返回形如 {'train': train_loss, 'test': test_loss} 的字典,用于监控模型在训练集和测试集上的性能变化,判断模型是否收敛以及是否存在过拟合现象。
模型训练
python
def train_model(model, optimizer, epochs=10):
# 记录模型在训练集上的模型损失
lossi = []
for epoch in range(epochs):
for i, data in enumerate(train_loader, 0):
inputs, labels = data['inputs'], data['labels']
optimizer.zero_grad()
logits = model(inputs)
loss = F.cross_entropy(logits, labels)
lossi.append(loss.item())
loss.backward()
optimizer.step()
# 评估模型,并输出结果
stats = estimate_loss(model)
train_loss = f'train loss {stats["train"]:.4f}'
test_loss = f'test loss {stats["test"]:.4f}'
print(f'epoch {epoch:>2}: {train_loss}, {test_loss}')
return lossi
训练老三样:损失,优化,循环
python
epoch 0: train loss 1.3786, test loss 1.5191
epoch 1: train loss 1.2663, test loss 1.4948
epoch 2: train loss 1.2127, test loss 1.4368
epoch 3: train loss 1.1642, test loss 1.3986
epoch 4: train loss 1.1281, test loss 1.3736
epoch 5: train loss 1.1588, test loss 1.3578
epoch 6: train loss 1.0906, test loss 1.4026
epoch 7: train loss 1.0988, test loss 1.3691
epoch 8: train loss 1.0670, test loss 1.3782
epoch 9: train loss 1.0861, test loss 1.3579
测试模型
python
@torch.no_grad()
def generate(model, context, tokenizer, max_new_tokens=300):
# context: (1, 10)
out = []
model.eval()
for _ in range(max_new_tokens):
logits = model(context) # (1, 99)
probs = F.softmax(logits, dim=-1) # (1, 99)
# 随机生成文本
ix = torch.multinomial(probs, num_samples=1) # (1, 1)
# 更新背景
context = torch.concat((context[:, 1:], ix), dim=-1)
out.append(ix.item())
if out[-1] == tokenizer.end_ind:
break
model.train()
return out
#测试
context = torch.zeros((1, 10), dtype=torch.long, device=device)
print(''.join(tokenizer.decode(generate(model, context, tokenizer))))
l = train_model(model, optim.Adam(model.parameters(), lr=learning_rate))
#测试训练效果
context = torch.zeros((1, 10), dtype=torch.long, device=device)
print(''.join(tokenizer.decode(generate(model, context, tokenizer))))
bash
def galuary").defastist():
it for a ry, types.Mapords((axt3rBL(= 'sc.basc.secop.copy(sermine, fields, pretator)
return [sc.tist(Func, start/sparsoc: repy. Fils all_freqect_ReplaceAPMudtil.IEj)[".']).collect()
.statter()ramefter;`'s[).sPart samples to 1
r = None by.__sewts._
(1)generate 函数作用
generate 用于利用训练好的自回归语言模型生成文本。函数以初始上下文 context 为输入,通过不断预测下一个 token,并将预测结果加入上下文,实现逐词(逐 token)生成文本,直到达到最大生成长度或遇到结束标记。
(2)关闭梯度与评估模式
函数使用 @torch.no_grad() 装饰器关闭梯度计算,并调用 model.eval() 将模型切换到评估模式。这能够减少显存占用,提高推理速度,同时避免 Dropout 等训练层影响生成结果。
(3)预测下一个 Token
在每一次循环中,将当前上下文输入模型得到预测结果 logits,再利用 F.softmax() 将其转换为概率分布 probs,表示每个 token 作为下一个字符(或单词)的生成概率。
(4)随机采样生成文本
函数使用 torch.multinomial() 根据概率分布随机采样一个 token,而不是直接选择概率最大的 token。这样生成的文本具有一定随机性和多样性,避免每次生成完全相同的结果。
(5)更新上下文窗口
生成的新 token 会追加到当前上下文末尾,同时丢弃最前面的一个 token,即通过 context = torch.concat((context:, 1:, ix), dim=-1) 构造新的固定长度上下文。这种滑动窗口机制保证模型始终使用最近的 context_length 个 token 进行预测。
(6)结束条件与结果返回
每生成一个 token,都会保存到输出列表 out 中;若生成的 token 为结束标记 tokenizer.end_ind,则提前结束生成,否则持续生成直到达到 max_new_tokens。最后恢复模型训练模式(model.train()),并返回生成的 token 序列。

缺陷

(1)无法捕捉窗口外的关联关系
多层感知器(MLP)通常采用固定长度的上下文窗口作为输入,只能利用窗口内的历史信息预测下一个词。当文本中存在跨越较长距离的语义依赖时(如代词指代、长句中的前后呼应),窗口之外的信息无法被模型获取,因此难以学习文本中的长期依赖关系。
(2)模型结构固定,无法随数据动态调整
MLP 的网络结构由输入层、隐藏层和输出层组成,输入维度在模型设计时已经固定,因此只能处理固定长度的输入序列。当输入内容发生变化时,模型无法根据不同位置之间的重要程度动态分配关注权重,也无法灵活适应不同长度或不同语义复杂度的文本,这限制了模型对复杂语言特征的表达能力。
(3)运算效率低下
随着上下文窗口长度的增加,MLP 输入层的维度也会线性增大,导致网络参数数量迅速增加,计算量和存储开销显著提高。同时,每个输入位置都需要与全连接层中的所有神经元进行计算,使模型训练和推理效率较低,难以扩展到大规模文本和长序列任务。相比之下,循环神经网络(RNN)和 Transformer 等模型能够更加高效地处理序列数据,并具有更好的扩展性。