AI 原理、模型演进与代码实践详解
本文从神经网络的基本原理出发,逐步深入探讨了神经网络模型的演进,特别是 Transformer 模型的实现原理及其在自然语言处理领域的应用。通过对神经网络的输入处理、注意力机制、残差网络、前馈网络等关键组件的详细解析,以及实际的代码实现,读者可以全面理解 AI 模型的工作机制。
初探神经网络(原理)
神经网络
讨论 ChatGPT 前,需要从神经网络开始,看最简单的'鹦鹉学舍'是怎么实现的。

上图就是一个人脑的神经元,由多个树突、轴突和细胞核构成,其中树突用来接收电信号、经细胞核加工(激活)信号、最后由轴突输出电信号,人脑大概 860 亿个神经元细胞,突触相互连接,形成拓扑结构。
每个神经元大约有 1163~11628 个突触,突触总量在 14~15 个数量级,放电频繁大约在 400~500Hz,每秒最高计算量大约 40 万亿次,换算成当前流行词汇,大脑大概等价于 100T 参数模型(140B 逊爆了),而且有别当前大模型中 ReLU 激活函数,大脑惰性计算是不用算 0 值的,效率更高。
神经网络就是借鉴了人脑神经元输入、计算、输出架构和拓扑设计,下面以一个求解数学问题的例子,看神经网络的实现原理:
当 X 为特定值时,Y 为特定值,通过训练神经网络,以求得 X 和 Y 的隐含关系,并给出 X 为特定值时,Y 的值。
为了看训练过程,我们提前知道 f(x)=x1w1+x2w2+b,其中 w1=w2=1,b=6.6260693,实际上是可以任意 f(x)
训练过程如下:
- 对输入的 X,分解成 n 个向量(举例方便,实际是直接矩阵计算,实现 batch),对每个向量的 X1 和 X2 元素,假定一个函数 f(x)=x1w1+x2w2+b 进行计算(其中 w1、w2 和 b 用随机值初始化)。
- 用假定的 f(x) 计算 X,得到结果和样本 Y 进行比照,如果有差异,调整 w1、w2 和 b 的值,重复计算。
- 直到差异收敛到某个程度后(比如小于 1),训练结束。
从训练过程看,经过 99 轮重复计算和调整 W/B 值后(训练),在 100 轮通过瞎猜求得 f(x)=x10.9991+x20.9853+6.3004,用最后一个组数据 X 计算得到的已经很接近样本数据,说明这些参数(模型)在这个场景已经对 f(x) 求得最优解了。
对 X (-6.8579 7.6980) 进行预测 Y 为 7.0334,和最初假定(w1=w2=1,b=6.6260693)参数计算得到的结果仅相差 0.2 左右,预测结束。
上述代码如下:
from torch import nn
from torch.optim import Adam
import torch
model = nn.Linear(2, 1)
optimizer = Adam(model.parameters(), lr=1e-1)
loss_fn = nn.MSELoss()
input = torch.randn(10, 2) * 10
bias = 6.6260693
target = torch.add(input.sum(dim=1, keepdim=True), bias)
print('训练样本输入', input, '\n训练样本输入 - 偏移量', bias, '\n训练样本目标值', target)
print("\n模型内部参数的初始随机值")
for name, param in model.named_parameters():
print(name, param.data)
print("\n开始训练,会发现差异值越来越小,说明模型在收敛")
for epoch in range(100):
pred = model(input)
loss = loss_fn(pred, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 9:
print(f"Epoch: {epoch} | loss: {loss:.2f}")
test = torch.randn(1, 2) * 10
target = test.sum(dim=1, keepdim=True)
pred = model(test)
print('\n训练完毕,测试模型。\n可以看到预测值和目标值近似,说明模型训练成功\n测试输入:', test, '模型预测:', pred.detach(), '目标值:', target)
print("\n可以看到经过训练后的模型值 w1、w2 接近于 1")
for name, param in model.named_parameters():
print(name, param.data)
损失函数&梯度下降
在训练过程中,会不断的通过调整 W 和 B 参数进行模型构建,进行目标拟合,得出参数过程需要解决两个问题:如何判断收敛状态,如何参数调整方向和值。
判断参数在每轮的收敛状态是通过一个叫损失函数(Loss Function)来完成的。即,判断 f(x)=wx+b* 的 w 和 b 是否最优解,只需要衡量真实值(训练样本值)_g(x)_和测量值(训练结算值)_f(x)_的均方误差。
上面代码中的 loss_fn = nn.MSELoss(),这里的 MSELoss 就是指均方误差损失函数。
参数的调整(加多少,减多少)是通过梯度下降方式实现的,也就是通过求一阶导数,来看函数单调性(一阶导数>0,函数单调递增,函数值随自变量的增大而增大)。
当函数变复杂起来,极值的查找,靠的是初始值运气,不一定每次都能找到最优解。在更复杂的场景,比如上面的二元函数拥有两个主要自变量(x1,x2),这个时候准确来说 L'是多元中 w1 的偏导。
为了防止 L'导致迭代幅度过大,会再乘以一个小数,最终公式中包含 lr(learning rate,一般设置 0.01~0.001)。
lr 的设定大小会影响效果和成本,简要说设大了会导致每次猜测幅度过大而导致的忽略了很多候选参数,跳过了最优值,这是对训练效果而言,同时对成本也会造成参数选值的震荡而无法收敛,而设置小了容易在 L(w1,w2,b) 函数曲线多凸情况下,陷入局部的极小值,而无法发现整体的极小值,同时也导致了训练时长和深度的增加。
W 和 B
上面的例子是最简单的单层(浅层)神经网络,其中神经元是神经网络中最基本的单元,最小设定就是 f(x)=xw+b*,通过拓扑设计来满足不同输入输出矩阵形状和拟合度。
这个模型对问题求可能的解、接近最优解的解以及最优解,不是函数在数学意义的上解。模型中 W 代表着权重(weight),B 代表着阈值(threshold),当一或者多个 input 经过一个神经元最终输出一个 output 过程中,通过 W 调整不同 input 做加权,最后通过 B 控制输出的偏移完成整个计算的抽象。
举个例子,城里正在举办一年一度的动漫展,你在犹豫要不要参加,考虑的因素有天气、价格、有没有同伴,规则是只要天气不好,不管价格和同伴都不去,如果天气还不错,但价格不合适,有朋友也不去。
| 因子 x | 权重 w | 这个事情决策规则 | 模型 |
|---|
| 天气 | 4 | 天气(0)&价格(0|1)&同伴(0|1)=不去 天气(1)&价格(1)&同伴(0|1)=去 天气(1)&价格(0)&同伴(0|1)=不去 | 天气4+价格2+同伴*1-6 >=0:去 <0:不去 |
| 价格 | 2 | | |
| 同伴 | 1 | | |
(权重举例 1、2、4 可以保证三因子之和可枚举所有规则,方便设阈值,不是真正权重比例,四因子是 1248)
模型:y(去不去)=x1(天气)*w1+x2(价格)*w2+x3(同伴)*w3+b
激活函数
上面例子只需要只输出 0 和 1,但是训练多层网络,不断迭代微调 W 和 B 时,只返回 0 和 1 是无法实现 0 到 1 之间的状态决策逻辑(输出太不敏感了)。
为了让输出能够平滑 0 到 1 的中间态,需要对结果进行连续性,对 y 进行改造。
这个就是 Sigmoid 激活函数,优点输出空间在 (0, 1),缺点是左右两侧导数趋 0,会出现梯度消失(下文讲),不利于训练,其他的主流的激活函数还有 ReLU f(x)= max(0, x),tanh、ELU 等函数。
激活函数主要是讲结果非线性化,放大参数对特征识别的灵敏和多样性,不同的激活函数除了非线性转换外,主要的区别是计算效率,解决梯度消失,中心值偏移这些问题。
前面的例子是 1x2 的矩阵输入,1x1 的矩阵输出,当输入是多维信息,例如一个人身高、年龄、工作、发量等等的时候,期望输出也是多维,例如感兴趣科目、愿意购买商品等等时,就需要将神经元重新规划拓扑结构,实现线性变化,形成类似下面复杂的网状结构。
当为了更好的拟合,以及引入运行时中间计算,也会调整拓扑,所以实际上整个网络会变得异常庞大(深层神经网络),例如 ChatGPT4 有 1.8 万亿的参数,这里的参数就是指训练后 W 和 B 的总和(红线部分),一共 1.8 万亿。
这些训练后(包括 fine-tuning)的参数被全部持久化后,大概代表已经吸收多少知识储备和推理潜力。
这就是最基础的神经网络实现原理。
神经网络模型演进(模型)
在此 gpt 之前,图片识别、NLP 等场景已经有成熟的 CNN、RNN 等神经网络模型,但这两种神经网络存在的两个问题限制了模型应用的突破。
卷积神经网络(Convolutional Neural Networks,CNN)
循环神经网络(Recurrent Neural Network, RNN)
CNN 和 RNN 已经具备通过神经网络实现分类、预测能力,但是存在两个典型问题:
- CNN 聚焦局部信息,丢失全局信息。
- RNN 无法并行计算(串行模式理论上确实也可以做到窗口无限大,然后从左到右把全部信息带过来,效率太差)。
在 2017 年的时候,Google 的翻译团队发布了一篇 Attention Is All You Need 论文,提出了 Transformer 模型,相比 CNN 和 RNN,Transformer 在复杂度是最低的,效率极高,他的核心就是 Attention Mechanism(2017 年前是有各种百花齐放和 CNN、RNN 结合的 Attention),相比 RNN,可以从整文视角看每个词以及这个词在上下文的意义是什么,比如'你太卷了和'把报纸卷起来'。
和前面讲的神经网络一样,transformer 也是通过训练(瞎猜)构建 f(x),实现包括文本分类(mode1)、生成下个句子预测(mode2)、翻译任务(mode3),下面以 GPT2 为例,一步步分解实现原理。
输入的处理(Embedding)
在计算之前需要对输入进行处理来解决几个问题:
- 输入可被用于数学计算
- 编码信息'足够稠密',能够承载训练过程中,学习的知识图谱
例如对'半'、'臧'、'真'、'帅'这 4 个词编码形成字典表(也可以'半'=1,'臧'=2,序列编码,one-hot 只是为了计算简单)可以解决计算问题,但基于 5 个维度编码,并且每个元素只有 0/1 是不够支持训练过程中产生的内容,eg."半"和"臧"连起可以表示一个人名,"真"是形容词等,最简单的解决方法是把维度扩充,再增加词性、关联性维度,当训练内容复杂后,会指数级增加计算量。
所以最适中的方式是控制维度,稠密元素,也就是把 [1 0 0 0] 降维成 [0.39847],也就是'低维稠密'向量化了(实际上降低 1 维是不够的,gpt2 默认时 768 维)。
当输入被向量化,可以承载的内容也就变得很多,比如说向量的空间特性,cosine 两个向量,可以从空间上判断其相似性,值接近 1 表示相似,反之依然,0 为相交或不相关。
当我们把每个字/词,都向量化后,比如'半',按照名词、动作、是否王室、阿里 4 个维度向量化后形成一个 [-0.1596, 0.3930, 0.6364, 0.2324] 词向量,同样完成'臧'字的词向量,就可以在判断'半'和'臧'之间的在表述一个事物、是否王室成员上存不存在联系。
用斯坦福的 GloVe 模型 embedding 后的英文单词编码,用一些可视化方法可以看到:
- 所有这些单词都有一条直的红色列,它们在这个维度上是相似的(名词维度)
- 'woman'和'girl'在很多地方是相似的,'man'和'boy'也是一样(性别维度)
- 'boy'和'girl'也有彼此相似的地方,但这些地方却与'woman'或'man'不同。(年龄维度)
所以当我们有了这样一份词向量表后,再去训练模型时,已经包含了词与词之前的某种联系,可以更好的达到训练目标,比如说我们训练一个模型,能够把各种夸张描述的娱乐新闻都转换成'谁干了啥'的时候,先把所有中文进行名字维度、动词维度、名词维度向量化后训练,然后再给予样本进行监督训练,将更准确。
实际上,在 tranformer 中,虽然不是一定需要这些已经训练好 (Pre-training) 的向量表,但思路是一样的。
Tranfromer 本身就可以训练这样的词向量表,已满足下一个词的预测目标,当我们有明确的训练目标,也不需要这样按照预定目标训练的词向量表,比如说我们自己训练一个类似 BERT 的模型,通过周围的词来预测完形填空试卷,____是法国的首都,通过一个模型训练词的上下文联系性之后,形成特定的词向量表。
(不仅文字,包括图片、视频、商品等等,一切皆可 Embedding,其实就是说 Embedding 用一个低维稠密的向量'表示'一个对象)
以翻译'LLM with banzang'为例,在 gpt2 中,整个 Embedding 过程如下:
- 对输入 X(LLM with banzang)进行分词:'LL','M','with','ban','z','ang';
- 查询分词在 gpt 词库中的索引列表:3069, 44, 351, 3958, 89, 648;
- 对索引列表向量化:经过一个全连接层(后面讲,可以理解经过 xw+b),形成 6768 的矩阵。
过程有三点补充说明:
- 分词有多种方式,广泛使用的子词分割(subword)方式,空间和效率更加平衡,在 gpt2 使用的是 BPE(Byte-Pair Encoding)。
为什么通过 subword 分词?
- 基于单词的分词:因为有 running 和 run,dogs 和 dog 等会形成大量的 token,因为没有覆盖全导致标记 unk,会降低拟合度。
- 基于字母的分词:没有意义,比如一个中文字符,包含太多的信息。
- 基于子词的分词:将不常见的词拆解成多个常见词,tokenization 被拆分 token 和 ization,一方面 tokenization 意义被整体保留,另一方面因为 token 都是出现频率较高的词,所以整个词汇表会缩减到很小而覆盖全。
向量化不一定非得从头训练产出,也可以使用 GloVe 这类预训练好,含有某些联系的模型数据。
6*768,768 个维度是 gpt2 默认设置,向量化后的作用在第一部已经说过,不再重复。
Embedding 过程代码如下:
embedding_layer = nn.Embedding(num_embeddings, embedding_dim)
indices = tokenizer.encode(text)
embeddings = embedding_layer(indices)
实际上更底层,索引被向量化的过程,还包含两个细节:
- 索引到向量过程,并没有按论文算法实现,而是直接随机初始了 w 参数,然后起了一层前馈层训练出 768 维度的最终值(暴力美学)
- 出了词向量外,还叠加了词位置向量(position embedding),来解决相同词在不同位置的语意不同,例如'你真狗','这是狗,不是豹子'。
论文中词位置向量细节
引入词位置的时候,还需要解决两个问题:
- 如果是用整数数值表示,推理时可能会遇到比训练时还要长的序列;而且随着序列长度的增加,位置值会越来越大。
- 如果用 [-1,+1] 之间进行表示,要解决序列长度不同时,能表示 token 在序列中的绝对位置;而且在序列长度不同时,也要在不同序列中 token 的相对位置/距离保持一致。
所以最后的实现是,对奇数维度,sin(词的 index 去除以 10000 的 2维度词向量维度(768)),偶数 cos,这样会将不管长度为多少的句子都是固定的长度的位置编码,以及位置编码都会被缩放在 [-1, 1] 的平滑范围,对模型非常理想的,最重要的是由于正弦和余弦函数的周期性(且是不同周期),对于任意固定偏移量 k,PE(pos+k) 可以表示为 PE(pos) 的线性函数。这意味着模型可以很容易地通过学习注意力权重来关注相对位置,因为相对位置的编码可以通过简单的线性变换来获得。即我知道了绝对距离,也能知道相对距离。
最后向量直接相加得出 embedding 后的值。
上面是 transform 论文内容,但实际上 gpt2 没有按照 sin 和 cos 去计算,而是直接通过一个前馈层直接训练得出的,然后和词向量直接相加,最终训练过程也不再还原位置信息,而是以这种隐含的方式将位置信息参与后续训练,提高参数拟合度。
最后把整个 embedding 过程的工程图贴一下:
注意力和多头注意力机制(Attention Mechanism&Multi-Head Attention)
Attention 不是 Transformer 提出的,在 2017 年以前的时候,各种 Attention 方式被广泛应用在 NLP 任务上,例如 Bahdanau Attention 等,单都以 RNN、CNN 结合形式出现,只不过到了 2017 年的 Attention Is All You Need 论文发表,解决了上述说的两个核心问题后,才被活跃起来。
Attention 的大概原理,鲁老师的文章,有个例子讲注意力机制是非常生动的,引用一下:
比如看到下面这张图,短时间内大脑可能只对图片中的'锦江饭店'有印象,即注意力集中在了'锦江饭店'处。短时间内,大脑可能并没有注意到锦江饭店上面有一串电话号码,下面有几个行人,后面还有'喜运来大酒家'等信息。
大脑在短时间内处理信息时,主要将图片中最吸引人注意力的部分读出来了,类似下面:
Attention 的核心实现就是论文中的公式:
其中 Q 为查询意图、K 为检索内容、V 为被查询全部信息,例如上图中:Q 为'好奇看看照片是个啥'、K 为'招牌的几个字',V 为'整个图片',通过计算 Q 和 Key 的 Attention Score,也就是最吸引人注意力的招牌,解析出招牌对应的值。
这是抽象的说法,还是以翻译'LLM with banzang'为例,分解实现细节。
"LLM with banzang"经过 embedding 后,形成以下的 6*768(seq.len, embed.dim)矩阵,分别代表着 LL、M、with、ban、z、ang 这 5 个 token 的 768 维度的向量:
然后分别乘以 3 个 768768(embed.dim, embed.dim)的 Wq、Wk、Wv 矩阵,得到的还是 3 个 6768 的矩阵,分别是 Q、K、V
然后将 Q 和 K 相乘(可以简单理解成 矩阵 X 乘以矩阵 X 的倒置),得到一个 6*6 的矩阵 Z(seq.len,seq.len),Z 代表着 [LL、M、with、ban、z、ang] 每个 token 之间的向量距离,换个说法就是把输入和输入的倒置矩阵相乘代表着 LL、MM、with、ban、zang 的 token 之间两两所有维度叠加后的'相似度',这个是 Attention 最核心部分。
两个矩阵点积表达相似度的数学原理,假设 LL、M 和 with 三个向量分别是 [1 2] [3 3] [-2 1] 映射到象限表后,相似肉眼看夹角度数,如下:
数学定义是向量 A 和 B 的顶点距离,比如说 LL 和 M 的顶点距离是
因为 cos 结果区间是 [-1, 1],所以两个矩阵的相似度也就相当于 两个矩阵的点积通过模长和 cos 给归一成 [-1, 1] 的值,等价于 LL 和 M 两个矩阵相似度,先算点积再归一,比如简化下上面的 Q、K、Z 矩阵。
回想下,上一篇讲到的词向量,LL 和 M、以及 M 和 with 之间的相似度是不一样的,经过的训练后,已经包含了某种挂链,比如下图中 cat 与 kitten 在所有维度的接近,要高于 dog 和 houses
其中缩放目的是:
- 避免数值过大,再最后归一的时候会导致梯度消失(vanishing gradients)回归效果差。
- 归一函数的输入如果太大会导致梯度非常的小,训练会很难。
其中归一函数实现很简单,就是对向量中所有元素,指数放大后,算了一个出现概率(指数用 e 的原因是 e 的 -1 次方是 0.3679,也在 (0, 1) 的区间)
最后再乘以 V 矩阵,得到一个 6*6 的矩阵 Z',也就是 token 两两关系在每个维度下,和 token 序列的关系度。
(具体的值实际上不需要理解,我们无法读懂某个参数 W 和 B 的值,也无法理解计算后的值,我们只关心这些值之间的关系,是不是预期就够)
图例如下,最后得到的矩阵 Z(6768)如果降维到 Z'(6,),第一行代表 LL 在"LLM with banzang"整句中和每个词的关联度,比如下图中 ll 和 m 紧密度高于 with,第二行是 M 如此类推,实际上如果把 61 的矩阵在 softmax 后,取某个概率阈值,可以得到"LLM with banzang"这句中,最重要的两个单词是什么(当然是训练后)。
回顾下 Attention 机制,整个过程如下:
这部分独立的流程的实现被称为 Encoder module。
实际上 transformer 论文中,一次查询可以是并行的,也就是第一维是 batch,然后第二维 seq.len,第三维是把 embed.dim 拆成了 n 份(论文中是 12 份,任意份都可以,但我怀疑 12 是一个经验值),然后第四维是 n 份后的 token 维度,还是按照上面例子从(6, 768)最终变换成(4, 12, 6, 64),再加上一个 linear 后,拼接成最终的(4, 12, 6, 768),这里的 12 就是多头(Multi-Head)概念。
多头在工程上的意义在于:
- 比起单头,多头可以在不同维度空间聚焦信息处理,也可以降低单头在聚合多维度时的不稳定。
- 拟合度更好,单头的
残差网络(Residual Network,ResNet)& 前馈网络(FeedForward Neural Network,FFNN)
前面输入的 X 从 embedding 开始,到多头 QKV,再还原 Z,经过了多层的网络,每经过一层就会增加训练的性能要求,梯度下降的特别多,会剑走偏锋,第一部里 Loss Function 中梯度下降中,和 LR 类似,会跳过最优值,或者在最优值附近震荡,反复消耗资源,有个可视化训练的小工具很有意思建议大家玩耍一下。
(在选择这个识别银河系一样的黄绿点范围的时候,我们增加了 6 个隐藏层后,traning loss 在反复横跳,训练过程非常的长)
2015 年微软亚洲研究院提出了基于 CNN 架构的 ResNet,在 transformer 中可以借鉴来解决这个'退化'问题,核心是引入了'跳跃连接'或'捷径连接'(训练场景,梯度直接反向传播最原始层),将输入除了做为 X 给每层外,还跳过这层网络直接和这层输出 Y 叠加,让后面的层可以学习到这层处理和原始输入的差异,而不是直接上层处理结果 Y,这种设计允许网络学习到恒等映射(identity mapping),即输出与输入相同,从而使得网络可以通过更简单的路径来学习到正确的映射关系。
X 和 Y 因为维度相同,这里叠加就是直接相加,但每层的输出其实是对 X 不同维度和力度的调整,需归一成正态分布后再相加,实现原理也很简单:
- 求矩阵每行所有列的均值
- 算出每行所有列的方差
- 最后用矩阵每行每列,减去这行的均值,再除以这行的标准差(ε是一个小的常数,防止除 0),然后再引入 a 和 b 训练参数,抵消这个过程的损失
经过 ADD 和 Norm 后,Transformer 的第一段处理基本结束,因为前面都是线性处理,需要再增加一个非线性层进行变化,让结果更丰富(或者让训练能够有一定的'基因突变'),这层非常的简单就是在第一部中讲的最简单的单向神经网络(f(x)=x*w+b),然后再过一次 Norm。
小结一下第一阶段的所有流程,见下图,这个流程在 tranformer 里被独立出一个模块,叫 encoder,至此 tranformer 具备识别一个句子中每个词和整句的关联性,也就是每次 token 都包含了全部 token 信息以及关联度。
训练过程
Transformer 仅一个 Encoder 模块就可以工作,可以处理信息抽取、识别、主体识别等任务,比如 BERT(Bidirectional Encoder Representations from Transformers)是只使用了 Encoder,可以从给定的文本段落中找到并提取出回答问题的文本片段,目标是识别或检索信息,而不是生成新的文本序列。
实际上,只通过 Encoder 直接连接一个全连接层去求 f(x) 也不是不可以,但不连接其他模块性能会差很多,同时也没有办法实现并行训练(Teacher Forcing 方式),所以又设计了 Decoder 模块,这两种模块的组合可以实现下面的场景:
- 文本分类(仅使用 Encoder 模块),label=f(tokens)
- 下一个 token 预测(仅使用 Decoder 模块),next_token=f(token)。比如给定一些词,做出下一个词的预测,而且是逐字向后推测
- 文本翻译(Encoder+Decoder 模块),中文=f(英文)
通过最初'LLM with banzang'翻译的例子,上面已经完成了 token 之间的关注力计算,下面从观察训练过程了解 Decoder 的构成。
整个训练过程还是求,即输入"LL、M、with、ban、z、ang"(此时的输入相比 Encoder 已经包含了 token 之间的相互关注信息),调整参数计算出结果和"大模型、与、半臧"比较,loss 收敛后结束,但实现上有两个特别的点:
第一个是在训练时词序引入了,翻译是以'大'、'大模型'、'大模型与'、'大模型与半臧'这样的顺序进行输出的,这样 token 之间的注意力加上词序的训练,在翻译(预测)场景会更贴合上下文场景,变得准确。
但是在技术上,引入词序会导致训练任务必须得到前一个任务的输出结果做输入进行串行,无法并行执行,所以 transformer 引入 mask 来解决这个问题,具体是把本来串行的任务,通过直接将完整的预期结果按词序做了 mask 覆盖,分成多个可以并行执行的任务。
mask 的主要实现就是对样本输入做了右上三角形的掩码(覆盖了一个非常小的数,可以让 softmax 归一后趋 0),在做 Decoder 的 QKV 的时候忽略了。
任务并行后,同时任务也变成了'Teacher Forcing'方式,可以让 loss 收敛更快。
Teacher Forcing
RNN 模型逐字推测训练过程,产生大量预测分支计算,比如'大'->[模型 (90%)、小 (4%)、量 (1%)…],可能在某个迭代会推测出下个词是'小',倒置推测从大模型走向大小变量的分支,越走越远,loss 收敛不了。
Teacher Forcing 概念是仍正常推测训练,但下个 token 不采纳推测实际结果,按照训练样本直接指定正确值,进行下个 token 预测。
在 transformer 中,完整 token 序列参与训练,但屏蔽每个 token 后面信息,形成可并行执行任务,每个 token 序列推测过程无视其他任务推测结果,采用训练样本值进行前序 context 推测下个 token。
另外一个特别点是 Decoder 含有两层注意力,第一层是和 Encoder 一样的实现(自注意力),接受来自训练文本的真实输出,形成 token 的相互关注信息,第二层注意力 QKV 中 Q 来自上一个 Decoder,KV 并非由上一层 Decoder 结果计算来的,而是来自 Encoder 的 KV 结果(非同源,非自注意力),这样的设计是将来自 Encoder 的训练文本输入和训练真实输出(按词序 mask)的相互关注都整合在一起预测被 mask 的值。
论文中 Encoder 和 Decoder 是可以堆叠的,即输入通过堆叠 n 层 Encoder(论文中使用了 2/4/6/8 层,只有 6 层效果最好,但实际在只有一行训练样本场景下,n_layers=1 是效果最好的)处理后在传递到 n 层 Deocder 继续处理,同时每层 Deocder 的 KV 都来自最后一层的 Encoder 的输出。
整个训练过程如下:
- 训练文本(输入/输出)['LLM with banzang', '大模型和半臧'] 的输入 ['LLM with banzang'] 进行分词和 embedding
- Encoder 计算训练文本输入 X 的 Self-AttentionScore,形成输入 X 的 token 间关注信息带入 Decoder
- 训练文本(输入/输出)['LLM with banzang', '大模型和半臧'] 的输出 ['大模型和半臧'] 进行分词和 embedding
- 第一层 Decoder 对训练文本输出 X'进行右上三角形 MASK 遮盖操作后进行 Self-AttentionScore,形成输出 X'从左往右词序的 token 间关注信息(即只能了解过去信息,无法提前知道未来词序,因为要预测),事实上形成了训练输出长度为 m 的并行任务
- 第二层 Decoder 将第一层 Deocder 输出的结果做为输入 Q,使用 Encoder 的 KV 参数,拼接训练输入和输出再次做 AttentionScore
- 预测下一个结果和训练输出对比后,反向传播调整各层参数,直到 Loss 收敛到预期结果。
小结一下流程:
预测过程
相比训练过程,切换到预测模式后,原本训练的并行输入由最后一个 Decoder 输出取代,然后开始串行循环,直至标记结束。就和 RNN 一样,逐一预测到底,例如:
- LL -> LLM
- LLM -> LLM with
- LLM with -> LLM with ban
- LLM with ban -> LLM with banz
- LLM with banz -> LLM with banzang
实际上预测下一个 token 是按照 softmax 后概率挑选的,在串行循环时,选取不同概率的词会形成不同的预测分支,例如:
预测结果因概率挑选以及前馈层的非线性激活函数,将整个预测变的更加丰富,有点像基因突变,可能未来会产生艺术创造的价值,当前基于 transfomer 的各个模型都有会有多个预测输出分支待采用。
输出的处理
当输入 X 被 Encoder 和 Decoder2 处理完后,形成 token_sizeembedding_size 大小的矩阵,每行代表着一个 token,每列是这个 token 在不同维度的某个 magic number,最后再经过一个全连接层(Linear Layer,),对输入矩阵做线形变化成 token_size*logits_size,同时引入训练参数做拟合,再经过一个 softmax 归一,拟合每个 token 在字典表中最高概率的 index,拿 index 还原最终 token。
至此 Transformer 整个工作流程全部结束,图例回顾:
工程代码实现及开源模型使用(工程)
案例通过 Transformer 模型的实现
上面贯穿的翻译'LLM with banzang'翻译案例,完整的代码实现如下(参考了 https://adaning.github.io/posts/63679.html#toc-heading-16):
import torch
from torch import nn
from torch import optim
from torch.utils import data as Data
import numpy as np
d_model = 6
max_len = 1024
d_ff = 12
d_k = d_v = 3
n_layers = 1
n_heads = 8
p_drop = 0.1
def get_attn_pad_mask(seq_q, seq_k):
batch, len_q = seq_q.size()
batch, len_k = seq_k.size()
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)
return pad_attn_mask.expand(batch, len_q, len_k)
def get_attn_subsequent_mask(seq):
attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
subsequent_mask = np.triu(np.ones(attn_shape), k=1)
subsequent_mask = torch.from_numpy(subsequent_mask)
return subsequent_mask
(nn.Module):
():
(PositionalEncoding, ).__init__()
.dropout = nn.Dropout(p=p_drop)
positional_encoding = torch.zeros(max_len, d_model)
position = torch.arange(, max_len).().unsqueeze()
div_term = torch.exp(torch.arange(, d_model, ).() *
(-torch.log(torch.Tensor([])) / d_model))
positional_encoding[:, ::] = torch.sin(position * div_term)
positional_encoding[:, ::] = torch.cos(position * div_term)
positional_encoding = positional_encoding.unsqueeze().transpose(, )
.register_buffer(, positional_encoding)
():
x = x + .pe[:x.size(), ...]
.dropout(x)
(nn.Module):
():
(FeedForwardNetwork, ).__init__()
.ff1 = nn.Linear(d_model, d_ff)
.ff2 = nn.Linear(d_ff, d_model)
.relu = nn.ReLU()
.dropout = nn.Dropout(p=p_drop)
.layer_norm = nn.LayerNorm(d_model)
():
x = .ff1(x)
x = .relu(x)
x = .ff2(x)
.layer_norm(x)
(nn.Module):
():
(MultiHeadAttention, ).__init__()
.n_heads = n_heads
.W_Q = nn.Linear(d_model, d_k * n_heads, bias=)
.W_K = nn.Linear(d_model, d_k * n_heads, bias=)
.W_V = nn.Linear(d_model, d_v * n_heads, bias=)
.fc = nn.Linear(d_v * n_heads, d_model, bias=)
.layer_norm = nn.LayerNorm(d_model)
():
residual, batch = input_Q, input_Q.size()
Q = .W_Q(input_Q).view(batch, -, n_heads, d_k).transpose(, )
K = .W_K(input_K).view(batch, -, n_heads, d_k).transpose(, )
V = .W_V(input_V).view(batch, -, n_heads, d_v).transpose(, )
attn_mask = attn_mask.unsqueeze().repeat(, n_heads, , )
prob, attn = ScaledDotProductAttention()(Q, K, V, attn_mask)
prob = prob.transpose(, ).contiguous()
prob = prob.view(batch, -, n_heads * d_v).contiguous()
output = .fc(prob)
.layer_norm(residual + output), attn
(nn.Module):
():
(ScaledDotProductAttention, ).__init__()
():
scores = torch.matmul(Q, K.transpose(-, -)) / np.sqrt(d_k)
scores.masked_fill_(attn_mask, -)
attn = nn.Softmax(dim=-)(scores)
prob = torch.matmul(attn, V)
prob, attn
(nn.Module):
():
(EncoderLayer, ).__init__()
.encoder_self_attn = MultiHeadAttention()
.ffn = FeedForwardNetwork()
():
encoder_output, attn = .encoder_self_attn(encoder_input, encoder_input, encoder_input, encoder_pad_mask)
encoder_output = .ffn(encoder_output)
encoder_output, attn
(nn.Module):
():
(Encoder, ).__init__()
.source_embedding = nn.Embedding(source_vocab_size, d_model)
.positional_embedding = PositionalEncoding(d_model)
.layers = nn.ModuleList([EncoderLayer() layer (n_layers)])
():
encoder_output = .source_embedding(encoder_input)
encoder_output = .positional_embedding(encoder_output.transpose(, )).transpose(, )
encoder_self_attn_mask = get_attn_pad_mask(encoder_input, encoder_input)
encoder_self_attns = ()
layer .layers:
encoder_output, encoder_self_attn = layer(encoder_output, encoder_self_attn_mask)
encoder_self_attns.append(encoder_self_attn)
encoder_output, encoder_self_attns
(nn.Module):
():
(DecoderLayer, ).__init__()
.decoder_self_attn = MultiHeadAttention()
.encoder_decoder_attn = MultiHeadAttention()
.ffn = FeedForwardNetwork()
():
decoder_output, decoder_self_attn = .decoder_self_attn(decoder_input, decoder_input, decoder_input, decoder_self_mask)
decoder_output, decoder_encoder_attn = .encoder_decoder_attn(decoder_output, encoder_output, encoder_output, decoder_encoder_mask)
decoder_output = .ffn(decoder_output)
decoder_output, decoder_self_attn, decoder_encoder_attn
(nn.Module):
():
(Decoder, ).__init__()
.target_embedding = nn.Embedding(target_vocab_size, d_model)
.positional_embedding = PositionalEncoding(d_model)
.layers = nn.ModuleList([DecoderLayer() layer (n_layers)])
():
decoder_output = .target_embedding(decoder_input)
decoder_output = .positional_embedding(decoder_output.transpose(, )).transpose(, )
decoder_self_attn_mask = get_attn_pad_mask(decoder_input, decoder_input)
decoder_subsequent_mask = get_attn_subsequent_mask(decoder_input)
decoder_encoder_attn_mask = get_attn_pad_mask(decoder_input, encoder_input)
decoder_self_mask = torch.gt(decoder_self_attn_mask + decoder_subsequent_mask, )
decoder_self_attns, decoder_encoder_attns = [], []
layer .layers:
decoder_output, decoder_self_attn, decoder_encoder_attn = layer(decoder_output, encoder_output, decoder_self_mask, decoder_encoder_attn_mask)
decoder_self_attns.append(decoder_self_attn)
decoder_encoder_attns.append(decoder_encoder_attn)
decoder_output, decoder_self_attns, decoder_encoder_attns
(nn.Module):
():
(Transformer, ).__init__()
.encoder = Encoder()
.decoder = Decoder()
.fc = nn.Linear(d_model, target_vocab_size, bias=)
():
encoder_output, encoder_attns = .encoder(encoder_input)
decoder_output, decoder_self_attns, decoder_encoder_attns = .decoder(decoder_input, encoder_input, encoder_output)
decoder_logits = .fc(decoder_output)
decoder_logits.view(-, decoder_logits.size(-))
:
():
(Tokenizer, ).__init__()
.sentences = sentences
():
.source_vocab
():
.target_vocab
():
source_inputs = .join([sentences[i][] i ((sentences))]).replace(, ).split()
source_inputs.insert(, )
.source_vocab = {k: v v, k (source_inputs)}
target_inputs = .join([sentences[i][] i ((sentences))]).replace(, ).replace(, ).split()
target_inputs.insert(, )
target_inputs.insert(, )
.target_vocab = {k: v v, k (target_inputs)}
encoder_inputs, decoder_inputs, decoder_outputs = [], [], []
i ((sentences)):
encoder_input = [.source_vocab[word] word sentences[i][].split()]
decoder_input = [.target_vocab[word] word sentences[i][].split()]
decoder_output = [.target_vocab[word] word sentences[i][].split()]
encoder_inputs.append(encoder_input)
decoder_inputs.append(decoder_input)
decoder_outputs.append(decoder_output)
torch.LongTensor(encoder_inputs), torch.LongTensor(decoder_inputs), torch.LongTensor(decoder_outputs)
():
split_word.join([key key .source_vocab][ids[i].item()] i ((ids)))
():
split_word.join([key key .target_vocab][ids[i].item()] i ((ids)))
device = torch.device( torch.cuda.is_available() )
epochs =
lr =
model = Transformer().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=lr)
sentences = [
[, , ]
]
tokenizer = Tokenizer(sentences)
encoder_inputs, decoder_inputs, decoder_outputs = tokenizer.convert_token_to_ids()
dataset = Seq2SeqDataset(encoder_inputs, decoder_inputs, decoder_outputs)
data_loader = Data.DataLoader(dataset, , )
source_vocab_size = (tokenizer.get_source_vocab())
target_vocab_size = (tokenizer.get_target_vocab())
epoch (epochs):
encoder_input, decoder_input, decoder_output data_loader:
encoder_input = encoder_input.to(device)
decoder_input = decoder_input.to(device)
decoder_output = decoder_output.to(device)
output = model(encoder_input, decoder_input)
loss = criterion(output, decoder_output.view(-))
(, % (epoch + ), , .(loss), , tokenizer.convert_ids_to_target_sentences(output.(dim=, keepdim=)[].data), , tokenizer.convert_ids_to_target_sentences(decoder_output.view(-).data), )
optimizer.zero_grad()
loss.backward()
optimizer.step()
output_len = (decoder_outputs.squeeze())
encoder_input, decoder_input, decoder_output data_loader:
encoder_input = encoder_input.to(device)
decoder_input = torch.zeros(, output_len).type_as(encoder_input.data)
next_symbol =
(, tokenizer.convert_ids_to_source_sentences(encoder_input.data.squeeze()))
i (output_len):
decoder_input[][i] = next_symbol
output = model(encoder_input, decoder_input)
prob = output.(dim=, keepdim=)[]
next_symbol = prob.data[i].item()
(, tokenizer.convert_ids_to_target_sentences(prob.data[:i+], ))
next_symbol == :
训练过程(在 400 轮的时候,已经收敛):
预测过程:
具体代码实现有兴趣学习或者在解疑过程可以仔细参考,所有抽象过程全部在上述文字描述中,重点行做了中文注释,需要补充说明的有两点内容:
- 代码中 Encoder 和 Deocder 有 Layer 的实现,意思是 Encoder 或者 Decoder 可以堆叠执行,Encoder 堆叠后的输出统一提供给每个 Decoder(论文中使用了 2/4/6/8 层,只有 6 层效果最好,但实际在只有一行训练样本场景下,n_layers=1 是效果最好的)
for layer in self.layers:
encoder_output, encoder_self_attn = layer(encoder_output, encoder_self_attn_mask)
encoder_self_attns.append(encoder_self_attn)
- 包括多头 n_heads = 8 的实现,也不是对 input 拆分成 8 份分别计算,而是和 Layer 一样直接合并在一个矩阵中做一次计算(并行实现)
通过开源模型处理实际案例
大淘宝技术项目需求管理中,有一环是维护每个需求的子技术 PM,但这个字段并不是必填,容易被漏填,案例目标:
- 通过训练已经存在的'子技术 PM'需求列表(1000+),求出剩余(1000+)需求的'子技术 PM'字段
不必再从头写一份训练代码,得益于"HUB"潮流,可以通过 https://huggingface.co/获取大量的开源模型及训练数据(甚至可以快速体验效果),比如我们选择一个可以处理中文的文本分类模型:
Hugging Face Transformers 是一个开源 Python 库,其提供了数以千计的预训练 transformer 模型,可广泛用于自然语言处理 (NLP) 、计算机视觉、音频等各种任务。它通过对底层 ML 框架 (如 PyTorch、TensorFlow 和 JAX) 进行抽象,简化了 transformer 模型的实现,从而大大降低了 transformer 模型训练或部署的复杂性。
选择的是'哈工大讯飞联合实验室(HFL)'的 MiniRBT,hfl/minirbt-h288
目前预训练模型存在参数量大,推理时间长,部署难度大的问题,为了减少模型参数及存储空间,加快推理速度,我们推出了实用性强、适用面广的中文小型预训练模型 MiniRBT,我们采用了如下技术:
- 全词掩码技术:全词掩码技术(Whole Word Masking)是预训练阶段的训练样本生成策略。简单来说,原有基于 WordPiece 的分词方式会把一个完整的词切分成若干个子词,在生成训练样本时,这些被分开的子词会随机被 mask(替换成 [MASK];保持原词汇;随机替换成另外一个词)。而在 WWM 中,如果一个完整的词的部分 WordPiece 子词被 mask,则同属该词的其他部分也会被 mask。更详细的说明及样例请参考:Chinese-BERT-wwm,本工作中我们使用哈工大 LTP 作为分词工具。
- 两段式蒸馏:相较于教师模型直接蒸馏到学生模型的传统方法,我们采用中间模型辅助教师模型到学生模型蒸馏的两段式蒸馏方法,即教师模型先蒸馏到助教模型(Teacher Assistant),学生模型通过对助教模型蒸馏得到,以此提升学生模型在下游任务的表现。并在下文中贴出了下游任务上两段式蒸馏与一段式蒸馏的实验对比,结果表明两段式蒸馏能取得相比一段式蒸馏更优的效果。
- 构建窄而深的学生模型。相较于宽而浅的网络结构,如 TinyBERT 结构(4 层,隐层维数 312),我们构建了窄而深的网络结构作为学生模型 MiniRBT(6 层,隐层维数 256 和 288),实验表明窄而深的结构下游任务表现更优异。
MiniRBT 目前有两个分支模型,分别为 MiniRBT-H256 和 MiniRBT-H288,表示隐层维数 256 和 288,均为 6 层 Transformer 结构,由两段式蒸馏得到。同时为了方便实验效果对比,我们也提供了 TinyBERT 结构的 RBT4-H312 模型下载。
在 CoLab 上使用 huggingface 非常简单,如下:
import torch
from transformers import AdamW, AutoTokenizer, AutoModelForSequenceClassification
from torch.utils.data import Dataset, DataLoader
from tqdm.auto import tqdm
checkpoint = "hfl/minirbt-h288"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=3)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
class TrainDataset(Dataset):
def __init__(self, sentences):
self.sentences = sentences
def __len__(self):
return len(self.sentences)
def __getitem__(self, idx):
return sentences['train'][idx][0], sentences['train'][idx][1], sentences['train'][idx][2]
class ValidationDataset(Dataset):
def __init__(self, sentences):
self.sentences = sentences
def __len__(self):
return (.sentences)
():
sentences[][idx][], sentences[][idx][], sentences[][idx][]
():
sentence ,labels = [],[]
item batch:
sentence.append([item[], item[]])
labels.append(item[])
inputs = tokenizer(sentence, padding=, truncation=, return_tensors=)
inputs[] = torch.tensor(labels)
inputs
sentences = {
: [
[, , ],
[, , ],
[, , ],
[, , ]
],
: [
[, , ],
[, , ]
]
}
evaluate
transformers TrainingArguments
transformers Trainer
numpy np
():
metric = evaluate.load(, )
logits, labels = eval_preds
predictions = np.argmax(logits, axis=-)
metric.compute(predictions=predictions, references=labels)
training_args = TrainingArguments(, evaluation_strategy=)
trainer = Trainer(
model,
training_args,
train_dataset = TrainDataset(sentences),
eval_dataset = ValidationDataset(sentences),
data_collator = data_collator,
tokenizer = tokenizer,
compute_metrics=compute_metrics
)
trainer.train()
train_dataset = TrainDataset(sentences)
train_dataloader = DataLoader(train_dataset, shuffle=, batch_size=, collate_fn=data_collator)
num_epochs =
progress_bar = tqdm((num_epochs))
model.train()
metric = evaluate.load(, )
optimizer = AdamW(model.parameters(), lr=)
epoch (num_epochs):
batch train_dataloader:
optimizer.zero_grad()
output = model(**batch)
loss, logits = output[:]
loss.backward()
optimizer.step()
predictions = torch.argmax(logits, dim=-)
metric.add_batch(predictions=predictions, references=batch[])
()
progress_bar.update()
metric.compute()
model.()
sentences = [, ]
logits = model(**tokenizer(sentences[], sentences[], return_tensors=)).logits
pred = torch.argmax(logits,dim=-)
(pred)
代码整体分五个部分:
- 模型引入:
AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=3)(num_labels 在案例中,等于'子技术 PM'的去重数)
checkpoint = "hfl/minirbt-h288"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=3)
训练样本准备:分成两部分 Train 和 Validation 两个训练集,每个训练集分成三个维度,前两个是文本的两个普通维度,最后一个是 lables 表明了文本的分类,在案例中就是'子技术 PM'值
模型评估:使用 Transformer 的高级工具 Trainer,看模型对数据的准确性指标(evaluate 需要安装,预设了指标)
因为数据安全的原因,没有办法把案例的需求数据进行上传,所以需要在本地执行,Colab 提供代码在本机执行的能力,前提需要安装 Jupyter,具体步骤参考 https://research.google.com/colaboratory/local-runtimes.html
连接本地执行后,运行时会出现一些问题:
- 依赖的包通过 pip,按错误提示在本机上挨个安装一遍,比如说 pip install torch
通过 hugginface 在线引入的模型,可能会遇到网络连接问题,遇到的话,下载模型到本地 https://huggingface.co/docs/transformers/installation#offline-mode
Trainer 包可能出现依赖版本问题,指标观察部分可以注释掉,包括 trainer.train()、metric.compute()
本地执行的代码:
import torch
from transformers import AdamW, AutoTokenizer, AutoModelForSequenceClassification
from torch.utils.data import Dataset, DataLoader
from tqdm.auto import tqdm
from datasets import load_dataset
device = 'mps' if torch.backends.mps.is_available() else 'cpu'
class UniqueLabelsDataset(Dataset):
def __init__(self, data, labels):
self.data = data
self.labels = labels
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.labels[idx], pre_process_input(self.data[idx].values())
def data_collator(batch):
sentence, labels = [],[]
for l, item in batch:
sentence.append(item)
labels.append(l.item())
inputs = tokenizer(sentence, padding=True, truncation=True, return_tensors="pt")
inputs['labels'] = torch.tensor(labels)
return inputs
def pre_process_input(inputs):
return .join([(value).replace(, ) value inputs])
train_dataset = load_dataset(, data_files = , delimiter=)[]
labels_vocab, unique_labels = torch.unique(torch.tensor(train_dataset[]), return_inverse=)
num_labels = (labels_vocab)
train_dataset.remove_columns([])
train_dataloader = DataLoader(UniqueLabelsDataset(train_dataset, unique_labels), shuffle=, batch_size=, collate_fn=data_collator)
checkpoint =
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=num_labels)
model.to(device)
transformers get_scheduler
num_epochs =
progress_bar = tqdm((num_epochs), dynamic_ncols=)
num_training_steps = num_epochs * (train_dataloader)
lr_scheduler = get_scheduler(
,
optimizer=optimizer,
num_warmup_steps=,
num_training_steps=num_training_steps,
)
model.train()
optimizer = AdamW(model.parameters(), lr=)
epoch (num_epochs):
batch train_dataloader:
batch = {k: v.to(device) k, v batch.items()}
output = model(**batch)
loss, logits = output[:]
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.set_postfix(loss=loss.item())
progress_bar.update()
model.save_pretrained()
tokenizer.save_pretrained()
sentences = [, , , , , , , ]
= tokenizer(pre_process_input(sentences), padding=, truncation=, return_tensors=)
.to(device)
logits = model(**).logits
pred = torch.argmax(logits,dim=-)
(labels_vocab[pred.item()])
和 colab 上的代码有一些不同,补充说明几点,剩余大家看代码理解:
- device = 'mps' if torch.backends.mps.is_available() else 'cpu',是启用 M3 的 GPU 加速
- 训练的数据结构中,'子需求技术 PM'字段是 labels,即'分类值',训练目标
{
'标题': Value(dtype='string', id=None),
'需求指派人': Value(dtype='string', id=None),
'***': Value(dtype='int64', id=None),
'***': Value(dtype='string', id=None),
'***': Value(dtype='string', id=None),
'子需求 pd': Value(dtype='string', id=None),
'子需求技术 pm': Value(dtype='int64', id=None),
'***': Value(dtype='string', id=None)
}
- lr_scheduler,是预热 LR 值,保持动态的 LR(1e-1 到 5e-5 范围),加速收敛
大概训练 100 轮后,loss 收敛到预期目标:
最后拿了一条没有填写技术子 PM 的数据做输入后,给出了预期团队,但非预测同学的结果(还算可以,训练样本太少)
结语
本文从神经网络的基本原理出发,逐步深入探讨了神经网络模型的演进,特别是 Transformer 模型的实现原理及其在自然语言处理领域的应用。通过对神经网络的输入处理、注意力机制、残差网络、前馈网络等关键组件的详细解析,以及实际的代码实现,读者可以全面理解 AI 模型的工作机制。此外,文章还介绍了如何利用开源模型进行实际任务的开发,为读者提供了从理论到实践的完整指南。希望本文能够帮助读者更好地掌握 AI 技术,推动 AI 领域的发展。