链载Ai

标题: 从模型原理到代码实践,深入浅出上手Transformer,叩开大模型世界的大门 [打印本页]

作者: 链载Ai    时间: 昨天 18:00
标题: 从模型原理到代码实践,深入浅出上手Transformer,叩开大模型世界的大门


序言

作为非算法同学,最近被Cursor、DeepSeek搞的有点焦虑,同时也非常好奇这里的原理,所以花了大量业余时间自学了Transformer并做了完整的工程实践。希望自己心得和理解可以帮到大家~

如有错漏,欢迎指出~

本文都会以用Transformer做中英翻译的具体实例进行阐述。


从宏观逻辑看Transformer

让我们先从宏观角度解释一下这个架构。

首先 Transformer也是一个神经网络,神经网络的本质是模拟人脑神经元的思考过程,数学上是一种拟合,当然,人脑内部的信号处理是否连续或者可拟合我们不得而知,但Transformer在我的机器上实实在在地思考并输出了正确的答案。

Transformer 主要是设计用来做翻译的,分两大块,如上图,左边的编码器和右边的解码器。

编码器负责提取原文的特征, 解码器负责提取当前已有译文序列的特征,并结合原文特征(编码器解码器的连线部分),给出下一个词的预测。

GPT基本可以认为就是Transformer的解码器部分。

接下来我们分几个部分逐步讲解并附上代码实践,很长 得慢慢看....


输入和输出

我认为大部分文章都没有把输入和输出讲得很细,其实理解了输入输出,你就基本可以理解Transformer的大半了。

模型的输入

以中英翻译为例,Transformer的输入分两部分

  1. 源文序列(中文) 即图1左侧部分,输入到编码器。 举例: 我爱00700
  2. 目标译文(英文)即图1右侧部分,输入到解码器。 一开始只有一个

模型的输出

很显然,对于上述输入,我们期待的输出是 I love 00700

模型并不能一下子输出完整的句子,他是一个词一个词( /token/ )吐出来的,并且每一个词都需要作为下一词的输入,这也是为什么大模型都是打字机交互的原因。 具体例子:

第一个循环
编码器输入我爱00700
解码器输入<bos>
输出I

第二个循环
编码器输入我爱00700
解码器输入<bos>I
输出love

第三个循环
编码器输入我爱00700
解码器输入<bos>Ilove
输出00700

第四个循环
编码器输入我爱00700
解码器输入<bos>Ilove00700
输出<eos>
//输出了结束符,翻译完成

请注意上述是为了简化理解的一个陈述,实际上模型真正的输出并不是一个词,而是整个 /词表/ 内任意一个词可能的概率,也就是图2所示的probabilities。

具体说,就是一个数组[0.2, 0.7, 0.1], 序号为0的词的概率是0.2,序号为1的词的概率是0.7,序号为2的词的概率是0.1。 假设这是第二轮循环,词表是[I, love, 00700] 取最大概率0.7的词,然后得到第二轮输出是love, 和第一轮输出拼起来就是 I love。

词表 & token

划线的两个词 /token/ /词表/ 你可能仍有疑惑。这正是我们真正弄清楚整个输入输出的关键。

显然,计算机只能理解二进制数据,我们说输入"我爱00700"的时候,实际上输入的是处理好的二进制数据。

如何把句子转化成模型可以理解的二进制数据呢? 不妨先想想我们怎么学英语的, 没错,背单词啊! 我们需要先认识词,然后理解句子,模型也是一样的。

我们背的英文单词表,和这里的模型 /词表/ 其实是一个意思,当然,形式略有不同。 词表里的每一个词,就是我们说的 /token/ ,请注意 token 不一定是一个英语单词。上述例子的英文词表可能是: [I, lo, ve, 00, 7] 。 显然,token不是按照空格分的。 原因是算力难以覆盖,英文单词有几十万个(不权威),老黄听了都摇头?。 而更低维度的分词,可以压缩词表的数量,[I, lo, ve, 00, 7]是我瞎写的,理解意思就行,让DS给我们举一个更好的例子:

假设语料库包含以下高频单词:

直接按空格分词会生成独立词表:

空格分词词表大小:6
[play,player,playing,plays,replay,replaying]
#bpe分词算法最终词表(目标大小=4)
#Ġ表示这个token只会出现在开始,想一想还原句子的时候你需要知道两个token是拼起来还是插入空格
[Ġplay,re,ing,er]

对比空格分词, /bpe分词算法/ (想了解的自行ds,限于篇幅不赘述)可以有效压缩词表大小,在大量语料的情况下会更明显,40GB数据的GPT2也仅5w词表大小

嵌入(embedding)

现在,我们有了词表,很容易就可以得到二进制的序列了

还是老例子:

我 爱 00700 被编码为 [1, 2, 3] (为了方便,这里假设分词就这样。)

我们可以直接输入到网络训练了吗? 答案是否定的,不过我们也终于来到了有意思的地方。 在在训练之前需要做一个 /嵌入/

来一个youtobe的动图看下 /嵌入/ 大概是啥。

再来一个DS的灵魂解释:

		嵌入(Embedding)的核心思想是 将复杂、高维的数据(如文字、图像)转化为低维、连续的数值向量,同时保留其内在的语义或关系 。这种转化让计算机能像人类一样“理解”数据的含义,并用数学方式计算相似性、分类或生成。

额... 有点抽象,似乎讲了些什么,又似乎什么都没讲......

没关系,让我来举一个形象的例子 观察下面的句子:

红色
当红明星
生意好红火啊

注意"红"这个字,发现了吗,在不同语境(或者说维度)它有不同的意思,如果我们直接输入token编码,这些信息是缺失的,模型就不太可能理解句子的意思。

那么,一个字或者说一个token到底有多少维度的语义呢?不知道啊?,但是没关系,猜一个, 512。 没错,就是这么朴实无华!

于是我们就有了

即E是一个32000x512的二维矩阵 (代码里就是数组)

E[1,1] E[1,2] ...... E[1,512] 的值表示词表中序号为1的词在维度1、2、512的语义, 由此同样一个“红”的token,就可以有多种语义,这样模型就可以理解词和句子了。

当我们输入 我 爱 00700 被编码为 [1, 2, 3] , 实际上我们应该输入:

请注意请注意,所谓一个字有不同维度的语义,是我现编的比喻,如果它帮到你理解这个东西那最最好,如果觉得比喻得不太对,大可一笑了之。

继续

E[1,1] E[1,2] ...... E[1,512] 我们可以把它看作是以原点为起点的 512维的向量 ,一般就叫做词的嵌入向量,也就是平时可能听到的向量化,没什么高大上的对吧。 向量化之后可以做什么呢?看图

(以三维举例,512维可以发挥想象力自行脑补)

为了让模型理解词和词的关系,我们用数学语言去描述这种关系,就是向量的内积:

就算忘记了向量内积怎么算也没关系,我们只需要理解,夹角越小,向量内积越大, 两个词的关联越紧密就行了。 实际上大部分复杂公式的计算都是封装好了的。 这里的内积,在自注意力的时候我们也会用到。

到了这里就可以解释一下 的元素值是怎么来的了,我们定义了每一个词有512个维度,那么每一个维度的值是什么呢?

答案是模型自己学习出来的,E是模型的参数的一部分,一个词每个维度的值,初始化是一个随机值,模型训练的时候会被更新。

怎么学出来的? 试想一下: 语料里面有无数的词,但是他们是有一定的关联的,比如 I love 这两个词经常同时出现,那么他们的内积就应该较大,反之也一样。实际上有点像聚类,怎么理解都行,模型学得差不多的时候,token的分布或者说嵌入向量的关系,一定是有规律的。

让我们来用数学语言总结一下嵌入:

就是我们的查表或者说嵌入操作:

嵌入查找操作上述例子 就是 ,L是句子长度也就是3

为什么这里也会叫查表? 实际上对索引为1的token做嵌入,就是在嵌入矩阵里到找第一行然后拿出来,就是 E[1,1] E[1,2] ...... E[1,512] 。这个过程不就是查表么。

Batch 处理

细心的你可能发现了问题,这里多了一个维度B,输入的结果 ,是三维的。这里解释一下,我们的例子只有一句话,但是在模型的训练中,其实是一次性输入多句话并行计算的,batch_size 的值就是你一次性输入多少句子来训练,一般来说,这取决于你的显存大小,自行尝试调整就可以了。

假设batch 为2,一次输入两句:

两句话的embedding矩阵合并,得到一个 2x3x512的三维矩阵 就是我们给编码器的输入了。 此时 B=2 L=3 D=512

padding掩码矩阵

还没完,让我们来一点刁钻的问题。 上述例子L=3,两个句子长度都一样=3。 如果句子长度不一样呢?batch矩阵岂不是拼不出来? 当然不会,实际写代码的时候,L一般都是取一个较大值,比如语料中最长句子的值。L=100。 不够长的句子怎么办呢? 补padding,注意不是0,因为神经网络的某些中间计算如softmax输入0也是有值的。 一般是用一个特殊的token作为padding。

在自注意力计算的时候(下文会讲)会根据padding的位置,生成mask 矩阵,计算时候padding替换为一个极小值比如-1e9,就可以不影响计算了。

PositionalEncoding

现在这个带padding的矩阵可以输入模型开始训练了吗? 还是不行..... 我们还缺少一个重要的信息,位置。 举一个例子:

[[0.3,0.5,0.1,0.4]
[猫,吃,鱼]=>Transformer=>[0.1,-0.6,-0.2,0.3],
[0.3,0.5,0.3,-0.1]]

[[0.3,0.5,0.1,0.4]
[鱼,吃,猫]=>Transformer=>[0.1,-0.6,-0.2,0.3],
[0.3,0.5,0.3,-0.1]]

很明显,虽然只是交换了一个字的顺序,但其实是两个完全不同意义的句子, 而 Transformer输出的分布却是不变的,说明网络没有办法识别位置信息。

为了解决这个问题,Transformer的论文里的方案是添加位置编码,也就是PositionalEncoding。

其实没有那么神秘,我们把 Transformer 看成函数 , 现在的问题是:

我们可以加上一个位置编码去规避这个问题

来一个简单粗暴易理解的 P(i) = i

猫0
吃1
鱼2

然后呢?就是简单的直接加上去

没错,这样其实我们的输入就包含了位置信息了,只要信息在那里,模型总能学明白

当然,这里只是为了方便说明位置编码本身,所以用了最简单的方案,你说它能不能跑,那肯定也是能跑的,就是会有不少问题,实际上论文里实现是正弦torch.sin(position * div_term)和余弦函数torch.cos(position * div_term)来生成位置编码, 解释起来就篇幅太长,大家可以自己探索。

位置编码不改变矩阵的维度,只是改变了数值, 我们给编码器的最终输入总结如下:

但是对于解码器的输入,还需要额外处理,继续往下。

并行计算 & Teacher Forcing

回顾下一开始的例子

第一个循环
编码器输入我爱00700
解码器输入<bos>
输出I

第二个循环
编码器输入我爱00700
解码器输入<bos>I
输出love

第三个循环
编码器输入我爱00700
解码器输入<bos>Ilove
输出00700

第四个循环
编码器输入我爱00700
解码器输入<bos>Ilove00700
输出<eos>
//输出了结束符,翻译完成

我们给编码器的输入始终是整个句子序列的embedding。而给解码器的输入,则是当前已经预测出来的 n-1个词的序列的embedding。

并行计算

当你真的上手去写代码的时候,你就会发现, 在训练阶段,代码里我们给解码器直接输入完整的句子

别急,实际上我们输入不是全部的ground truth,是 n-1序列的ground truth。怎么理解呢?真正的ground truth 是 I love 00700 eos 。 我们的输入是

实际上解码的输入(姑且叫trg_input)在做计算的时候还需要做一个causal mask,叫做因果掩码,字面意思就是为了防止计算的时候知道未来的信息。

>>>trg_input#假设我们的input长这样1234代表<bos>Ilove00700
tensor([[1,2,3,4],
[1,2,3,4],
[1,2,3,4],
[1,2,3,4]])
#因果掩码矩阵是这样的,对角线上移一位的上三角矩阵,上三角的元素是极小值
>>>mask=torch.triu(torch.ones(4,4),diagonal=1)
>>>mask=mask.float().masked_fill(mask==1,float(-1e9))
>>>mask
tensor([[0.,-inf,-inf,-inf],
[0.,0.,-inf,-inf],
[0.,0.,0.,-inf],
[0.,0.,0.,0.]])
>>>

#执行causalmask
>>>trg_input+mask
tensor([[1.,-inf,-inf,-inf],
[1.,2.,-inf,-inf],
[1.,2.,3.,-inf],
[1.,2.,3.,4.]])

看看causal mask结果的第一行,[1., -inf, -inf, -inf] 其实就是相当于第一轮循环的输入 bos, 因为无限小在计算的时候可以忽略。 后续的2、3、4行刚好也就是 对应的2、3、4轮循环。 get到了吗, 直接作为一个矩阵输入,在模型里并行计算,4个循环优化成了一个!能够并行计算,这正是transformer一个重要特性。

Teacher Forcing

想一想为什么我们可以实现并行计算? 因为训练的时候,我们提前知道了正确答案,在生产环境推理过程中,我们不可能知道第一个token应该输出I,第二个token应该输出love ...., 所以推理的时候,只能顺序执行循环。

再多想一点,在训练的时候,模型还并不成熟,第一个字符输出的未必是I,假设输出了 He 呢? 然后第二轮循环我们的输入就是 bos He, 这样继续循环下去只会越来越错,非常不利于学习的收敛。 所以,我们第二轮循环不输入He, 而是用正确答案 I 来继续下一个token预测,实验表明这样学习的速度会更快。 这种训练方法,我们就称之为Teacher Forcing。

关于并行计算和Teacher Forcing感觉大部分文章都是一笔带过,而我感觉这里很重要,包括理解架构本身或者是去理解为什么transformer带来了技术的爆发,并行计算是一个很重要的因素。

输入输出的代码实现

作为一个严谨的工程师,最后我贴一下我自己的实现,并做简单讲解:

#关于语料我的数据集来自wmt17v13zh-en大约100m30+w行长句子
#从csv读出来,第一列是中文第0列是英文
train_data=read_tsv_file(opt.train_tsv,1,0,opt.delimiter)
valid_data=read_tsv_file(opt.valid_tsv,1,0,opt.delimiter)ifopt.valid_tsvelse[]
test_data=read_tsv_file(opt.test_tsv,1,0,opt.delimiter)ifopt.test_tsvelse[]

#分词器初始化我这里是bpebytelevel32000词表大小,具体实现太长就不贴了
src_tokenizer_path=os.path.join(opt.data_dir,"tokenizer_zh.json")
src_tokenizer=train_tokenizer([srcforsrc,_intrain_data],opt.vocab_size,src_tokenizer_path)

trg_tokenizer_path=os.path.join(opt.data_dir,"tokenizer_en.json")
trg_tokenizer=train_tokenizer([trgfor_,trgintrain_data],opt.vocab_size,trg_tokenizer_path)

#有了分词器之后,对训练数据句子分词打包成pkl格式,方便模型训练的时候读取
processed_train=process_data(train_data,src_tokenizer,trg_tokenizer,opt.max_len)

#train阶段
#从pkl加载数据略

#训练输入
fori,batchinenumerate(dataloader):
#准备数据每次循环处理一个batch的数据
src=batch['src'].to(device)#取出语料原文。我爱00700(举例,实际上batch是多句的数组,但是矩阵运算的时候都是一起并行算的)
trg=batch['trg'].to(device)#取出语料目标译文。bosIlove00700eos
trg_input=trg[:,:-1]#去掉最后一个token,bosIlove00700n-1的TeacherForcing序列
trg_output=trg[:,1:]#去掉第一个token,Ilove00700eos真实的groudtruth,用于计算损失

#前向传播清空梯度
optimizer.zero_grad()

#输入模型计算一次
#teacherforcing:trg_input实际上就是groundtruth(正确的值)
#train模式下,output是整个句子的预测值
output=model(src,trg_input)
......


#嵌入
"""初始化嵌入矩阵"""
#使用正态分布(高斯分布)初始化张量
#均值为0,标准差为0.02
#在数学上,512维的随机向量,可以认为是互相正交的(就是任意两个token之间一点关系都没有)
nn.init.normal_(self.weight,mean=0,std=0.02)

#基本的嵌入查找
#weight是模型参数,自动参与学习
embeddings=self.weight[x]


#mask
"""创建源序列和目标序列的掩码"""
src_padding_mask=(src==self.pad_idx)#srcpadding掩码
trg_padding_mask=(trg==self.pad_idx)#trgpadding掩码

#因果掩码
seq_len=trg.size(1)
trg_mask=torch.triu(
torch.ones((seq_len,seq_len),device=src.device),diagonal=1
).bool()

returnsrc_padding_mask,trg_padding_mask,trg_mask

#应用mask我这里的mask是bool矩阵,true表示需要mask
#如果mask是true,scores对应位置的元素就填充为极小值-1e9
ifmaskisnotNone:
scores=scores.masked_fill(mask==True,-1e9)

输入输出终于写完 看到这里就真的不容易 感谢~!


自注意力机制

到这里我们已经完成了输入的所有前置处理,训练数据开始进入到网络训练。 而理解Transformer网络的关键就在于自注意力机制,如下图所示的三个模块,他们是整个网络的核心。

注意力机制

注意力其实非常好理解, 如下图: 万绿丛中一点红,你首先看到了红,这就是注意力。

在训练网络中,注意力的作用是什么呢?

老例子:“ 我 爱 00700 ” 翻译为 “ I love () ” () 为当前正在预测的词。

如上图, 前面我们介绍过Transformer预测下一个词的时候,会结合整个上下文,即“我 爱 00700 I love”,假如没有注意力机制,那么所有词对预测结果的贡献是一样的! 假设语料的言情文偏多,那输出很可能是 I love you, 很明显不合理, 此时我们希望网络把注意力集中在“00700”这个词上,其他词应该忽略掉。

那么在数学上注意力是什么呢? 非常简单,就是权重系数 写出来就是:

??? 解释了这么久 就这? 没错是的?‍♀️ 取个高大上的名字而已

自注意力机制

自注意力毕竟多了一个字,肯定还是有点不一样的,不一样在哪呢,他乘的不是权重矩阵,它乘它自己!

重点看一下这个

如果你对矩阵乘法熟,你马上就会意识到,这不就是句子的词和词之间的内积吗?

回忆一下嵌入的内容,我们说过词向量的内积反应的是词的关系远近。

本质是,Transformer通过句子本身的词和词之间的关系计算注意力权重! 所以我们叫它 - 自注意力 !还算自洽吧~

当然,直接相乘没有参数可以训练啊,来个线性变换吧 ,于是

不好记,得起个名字, 第一个式子是原始请求 就叫Query吧,简写为Q

感觉不用纠结这个命名,反正论文没提命名的道理 自行想象吧

第二个式子是被用来计算关系的,就叫Key吧,简写为K

于是:

按照论文所提,实验发现,QK点积可能会过大,数据方差太大,导致梯度不太稳定,所以需要把分布均匀一下,于是:

我们需要的是权重,所以需要转化为 0.0~1.0 这样的值,这正是 /softmax/ 函数的能力 于是:

代入 , y改写为Attention,更高大上

另外为了增加网路的复杂度,我们不直接乘x,老办法,线性变换一下, 记为V

最终

最终我们得到了和论文一模一样的公式~

多头注意力机制

先看一个图,这是一个自注意力计算的示例:

问题是: 我 - 我 爱-爱 00700-00700 的分数最高, 自己最关注自己,听起来是合理的,但论文作者应该是觉得这不利于收敛。 所以提出了多头机制

其实这里论文并没有太多解释,我理解是一种实验性的经验

那么什么是多头? 其实也很简单,就是分块计算注意力,好比原来是一个头看整个矩阵, 现在是变身哪吒三头六臂,一个头看一小块,然后信息合并。 画图说明:

这种分头其实本质上计算量是一样的,只是注意力分块了,直接看公式就好了。


论文中 i=8, 8个头, dk =dv = d_model/8 = 64

因果掩码-多头自注意力机制

其实输入输出的时候就讲了,就是解码器做多头计算的时候 使用teacher forcing,需要加上因果掩码mask。 下面代码说明。

交叉-多头自注意力机制

解码器如何结合编码器的上下文就在这里了,如果是交叉-多头自注意力机制,那么 Q = x, K=V=编码器的输出,直接看代码最直观。

#1.线性变换这里的query=key=value就是x
#如果是解码器的交叉-多头自注意力机制,那么query=x,key=value=编码器的输出
Q=self.q_linear(query)#这里的linear就是线性变换wx+b,下同
K=self.k_linear(key)
V=self.v_linear(value)

#2.分割成多头[batch_size,seq_len,d_model]->[batch_size,seq_len,num_heads,d_k]
#实际上torch的张量(矩阵)是用一纬数组存的,所谓分割多头、转置,最终就是改一下数组元素的顺序
Q=Q.view(batch_size,-1,self.num_heads,self.d_k).transpose(1,2)
K=K.view(batch_size,-1,self.num_heads,self.d_k).transpose(1,2)
V=V.view(batch_size,-1,self.num_heads,self.d_k).transpose(1,2)

#3.计算注意力
#K.transpose(-2,-1)就是k的转置
scores=torch.matmul(Q,K.transpose(-2,-1))/math.sqrt(self.d_k)

#4.应用mask因果掩码或者padding掩码
ifmaskisnotNone:
scores=scores.masked_fill(mask==True,-1e9)

#5.softmax获取注意力权重
attn=F.softmax(scores,dim=-1)
#dropout是随机丢弃一些值(写0),为了增加随机性,训练时才会开启,避免过拟合
attn=self.dropout(attn)

#6.注意力加权求和就是乘v
out=torch.matmul(attn,V)

#7.重新拼起来多头把维度转置回来
out=out.transpose(1,2).contiguous().view(batch_size,-1,self.d_model)

#8.最后又加了一个线性变换wx+b,不要为问我为什么......实验性经验
out=self.out_linear(out)

returnout

前向传播

到这里就很简单了,核心的部分都讲完了。 还剩下几个小块简单讲一下。

Add & Norm

Add 叫残差,超级简单,直接上代码:

#self_attn就是算注意力,tgt_mask是因果掩码。
#本来应该是x=self.dropout1(self.self_attn(x2,x2,x2,tgt_mask))
#残差就是多加了x,x=x+self.dropout1(self.self_attn(x2,x2,x2,tgt_mask))
#简单解释就是加这个x,防止梯度下降太快到后面没了。详细就不展开了,可自行gpt
x=x+self.dropout1(self.self_attn(x2,x2,x2,tgt_mask))

Norm 是归一化,简单解释一下,归一化是让数据分布更均衡,一般是消除一些离谱值、不同数据量纲的影响。 比如分析年龄和资产对相亲成功率的影响,年龄只有0-100, 资产的范围就很大,计算的时候就不好弄,需要把资产也缩放到一个合理的数值范围 比如 0-100 万。

归一化有很多种,这里用的是 /Layernorm/ ,这个公式还挺复杂,有兴趣可以自己研究,这里我是偷懒的:

#直接使用pytorch的自带公式
self.norm1=nn.LayerNorm(d_model)
#调包就完事
x2=self.norm1(x)

FeedForward

前馈全连接层(feed-forward linear layer) ,好多文章试图解释这个意义,其实没啥意义,就是普通的两层网络, 一般来说就是一层神经网络就是 线性变换+激活函数。

线性变换,无非就是升维和降维。举例:升维是类似于你看图片的时候,双指放大,然后滑来滑去找到你需要的,是特征放大。降维么,就是截图保存放大部分,然后输出,是特征提取。

一升一降就是FeedForward啦,直接看代码:

classFeedForward(nn.Module):
def__init__(self,d_model:int,d_ff:int,dropout:float=0.1):
super().__init__()
#从dmodel升维到d_ff=2048论文的值
self.linear1=nn.Linear(d_model,d_ff)

#降回来提取特征
self.linear2=nn.Linear(d_ff,d_model)
self.dropout=nn.Dropout(dropout)

defforward(self,x:torch.Tensor)->torch.Tensor:
#怎么找到关注特征的那不就是激活函数F.relu,很明显,神经元被激活的地方就是感兴趣的特征啊。
returnself.linear2(self.dropout(F.relu(self.linear1(x))))

解码器

到这里就没啥了,每个块我们都理解了,后续就是实现逻辑了。 直接上代码,太长了好像没必要,文末尾放git地址把。

编码器

同上。


反向传播

前向传播计算出一次训练的结果,反向传播就是根据结果的好or坏,更新参数,循环往复,最终得到一个满意的模型。

自己实现 实在是有点累,调包只需要一句:

#前向传播
output=model(src,trg_input)

......

#计算损失
loss=criterion(output_flat,trg_output_flat)

#反向传播
loss.backward()

pytorch框架会记录你整个模型前向传播的图,然后根据损失,使用 /链式法则 / & /梯度下降算法/ 帮你回溯计算更新每一个参数,太舒服了~

/链式法则 / & /梯度下降算法/ 讲的好的文章很多,实在写不动了,况且文章也够长了.... 大家自行gpt吧~






欢迎光临 链载Ai (https://www.lianzai.com/) Powered by Discuz! X3.5