AI基本结构10-mlp实现简单nlp

前置程序

词嵌入

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 等模型能够更加高效地处理序列数据,并具有更好的扩展性。

相关推荐
计算机魔术师1 小时前
Karpathy:用语音与LLM长谈可提升理解效率
人工智能·ai编程
甲维斯1 小时前
我要开始吹牛逼了!Kimi K3 “宇宙无敌”!
前端·人工智能
周末程序猿1 小时前
图解 120 个大语言模型(LLM)核心概念(61-90)
人工智能
科技圈快迅2 小时前
游戏投影仪和普通投影仪区别是什么?2026游戏投影仪测评
人工智能
陆枫Larry2 小时前
CPU 和 GPU 的核心区别与适用场景
人工智能
Aa99883342 小时前
AI视觉检测设备厂家的技术选型框架——密封件和磁材的缺陷分类与光学成像原理
人工智能
吴佳浩2 小时前
今天我们讲讲大模型的“核心”技术:蒸馏(Model Distillation)
人工智能·llm·agent
阿里云大数据AI技术2 小时前
阿里云 ES AI 引擎版:面向 Agent 场景,为亿级租户、千亿规模向量设计的搜索引擎
人工智能·elasticsearch·agent
郭小铭2 小时前
[开源] 做了一个本地优先的多 Agent 市场研究桌面:Evidence Loom
人工智能