【从零开始大模型开发与微调:基于PyTorch与ChatGLM】(一学就会的深度学习基础算法详解)

作者:承渊政道日期:2026/6/30

🔥承渊政道:个人主页

❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》

✨逆境不吐心中苦,顺境不忘来时路!✨

🎬 博主简介:

神经网络看起来由大量矩阵、激活函数和参数组成,但训练过程可以浓缩成四句话:先用当前参数完成一次预测;再用损失函数衡量预测有多差;接着通过链式法则把误差逐层传回;最后沿着能让损失下降的方向微调参数.前两步是前向传播,后两步构成反向传播与优化.深度学习是目前以及可以预见的将来最为重要也是最有发展前景的一个学科,而深度学习的基础是神经网络,神经网络本质上是一种无须事先确定输入输出之间的映射关系的数学方程,仅通过自身的训练学习某种规则,在给定输入值时得到最接近期望输出值的结果.作为一种智能信息处理系统,人工神经网络实现其功能的核心是反向传播(BackPropagation,BP)神经网络.反向传播神经网络是一种按误差反向传播(简称误差反传)训练的多层前馈网络,它的基本思想是梯度下降法,利用梯度搜索技术,以期使网络的实际输出值和期望输出值的误差均方差最小.本文将从BP神经网络开始讲起,全面介绍其概念、原理及其背后的数学原理.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!



目录

  • 1.反向传播神经网络的前身历史
  • 2.先把术语说清:BP不是一种神秘网络,而是一种求梯度的方法
    • 2.1从感知机到多层网络:真正的障碍是非线性
    • 2.2历史脉络中的几个常见误区
  • 3.反向传播神经网络两个基础算法详解
    • 3.1最小二乘法详解
    • 3.2梯度下降算法
    • 3.3最小二乘法的梯度下降算法及其Python实现
  • 4.反馈神经网络反向传播算法介绍
    • 4.1深度学习基础
    • 4.2链式求导法则
    • 4.3反馈神经网络的原理与公式推导
    • 4.4反馈神经网络原理的激活函数
    • 4.5反馈神经网络原理的Python实现
  • 5.NumPy手写反向传播:让网络学会 XOR
  • 6.PyTorch自动求导:loss.backward()做了什么
    • 6.1动态计算图里到底保存了什么
    • 6.2用有限差分检查手写梯度
  • 7.训练不收敛时,按这张清单排查
    • 7.1先检查数据与目标
    • 7.2再检查前向与损失
    • 7.3然后检查梯度
    • 7.4最后检查优化器超参数
    • 7.5五个高频问题
  • 8.从三层网络到大语言模型:规模变了,学习引擎没变
    • 8.1大模型中的损失
    • 8.2大模型训练增加了哪些工程机制

1.反向传播神经网络的前身历史

在介绍反向传播神经网络之前,人工神经网络是必须提到的内容.人工神经网络(Artificial Neural
Network, ANN)的发展经历了大约半个世纪,从20世纪40年代初到80年代,神经网络的研究经历了低潮和高潮几起几落的发展过程.

1930年,B.Widrow和M.Hoff提出了自适应线性元件网络(ADAptive LINear NEuron, ADALINE),
这是一种连续取值的线性加权求和阈值网络.后来,在此基础上发展了非线性多层自适应网络.WidrowHoff的技术被称为最小均方误差(Least MeanSquare,LMS)学习规则.从此,神经网络的发展进入了第一个高潮期.

的确,在有限的范围内,感知机有较好的功能,并且收敛定理得到证明.单层感知机能够通过学习
把线性可分的模式分开,但对像XOR(异或)这样简单的非线性问题却无法求解,这一点让人们大失所望,甚至开始怀疑神经网络的价值和潜力.

1939年,麻省理工学院著名的人工智能专家M.Minsky和S.Papert出版了颇有影响力的Perceptron一书,从数学上剖析了简单神经网络的功能和局限性,并且指出多层感知机还不能找到有效的计算方法.由于M.Minsky在学术界的地位和影响力,其悲观的结论被大多数人不做进一步分析而接受,加之当时以逻辑推理为研究基础的人工智能和数字计算机的辉煌成就,大大降低了人们对神经网络研究的热情.

其后,人工神经网络的研究进入了低潮.尽管如此,神经网络的研究并未完全停顿下来,仍有不少
学者在极其艰难的条件下致力于这一研究.

1943年,心理学家W·McCulloch和数理逻辑学家W·Pitts在分析、总结神经元的基本特性的基础上提出了神经元的数学模型(McCulloch-Pitts模型,简称MP模型),标志着神经网络研究的开始.受当时研究条件的限制,很多工作不能模拟,在一定程度上影响了MP模型的发展.尽管如此,MP模型对后来的各种神经元模型及网络模型都有很大的启发作用,在此后的1949年,D.O.Hebb从心理学的角度提出了至今仍对神经网络理论有着重要影响的Hebb法则.

1945年,冯·诺依曼领导的设计小组试制成功存储程序式电子计算机,标志着电子计算机时代的开始,如下图所示.1948年,他在研究工作中比较了人脑结构与存储程序式计算机的根本区别,提出了以简单神经元构成的再生自动机网络结构.但是,由于指令存储式计算机技术的发展非常迅速,迫使他放弃了神经网络研究的新途径,继续投身于指令存储式计算机技术的研究,并在此领域作出了巨大贡献.虽然,冯·诺依曼的名字是与普通计算机联系在一起的,但他也是人工神经网络研究的先驱之一.

1958年,F·Rosenblatt设计制作了感知机,这是一种多层的神经网络.这项工作首次把人工神经网
络的研究从理论探讨付诸工程实践.感知机由简单的阈值性神经元组成,初步具备了诸如学习、并行处理、分布存储等神经网络的一些基本特征,从而确立了从系统角度进行人工神经网络研究的基础.

1972年,T.Kohonen和J.Anderson不约而同地提出具有联想记忆功能的新神经网络.1973年,S.Grossberg与G.A.Carpenter提出了自适应共振理论(Adaptive Resonance Theory,ART),并在以后的若干年内发展了ART1、ART2、ART3这3个神经网络模型,从而为神经网络研究的发展奠定了理论基础.

进入20世纪80年代,特别是80年代末期,对神经网络的研究从复兴很快转入了新的热潮.这主要是因为:一方面,经过十几年的迅速发展,以逻辑符号处理为主的人工智能理论和冯·诺依曼计算机在处理诸如视觉、听觉、形象思维、联想记忆等智能信息问题上受到了挫折.另一方面,并行分布处理的神经网络本身的研究成果使人们看到了新的希望.

1982年,美国加州工学院的物理学家J.Hoppfield提出了HNN(Hoppfield Neural Network)模型,并首次引入了网络能量函数概念,使网络稳定性研究有了明确的判据,其电子电路实现为神经计算机的研究奠定了基础,同时也开拓了神经网络用于联想记忆和优化计算的新途径.

1983年,K.Fukushima等提出了神经认知机网络理论;1985年,D.H.Ackley、G.E.Hinton和T.J.Sejnowski将模拟退火概念移植到Boltzmann机模型的学习中,以保证网络能收敛到全局最小值.1983年,D.Rumelhart和J.McCelland等提出了PDP(Parallel Distributed Processing)理论,致力于认知微观结构的探索,同时发展了多层网络的BP算法,使BP网络成为目前应用最广的网络.

反向传播(见下图)一词的使用出现在1985年后,它的广泛使用是在1983年D.Rumelhart和J.McCelland所著的Parallel Distributed Processing这本书出版以后.1987年,T.Kohonen提出了自组织映射(SelfOrganizing Map, SOM).1987年,美国电气和电子工程师学会(Institute for Electrical and ElectronicEngineer, IEEE)在圣地亚哥(SanDiego)召开了规模盛大的神经网络国际学术会议,国际神经网络学会(International Neural NetworksSociety, INNS)也随之诞生.

1988年,国际神经网络学会的正式杂志Neural Networks创刊;从1988年开始,国际神经网络学会和IEEE每年联合召开一次国际学术年会.1990年,IEEE神经网络会刊问世,各种期刊的神经网络特刊层出不穷,神经网络的理论研究和实际应用进入了一个蓬勃发展的时期.

BP神经网络(见下图)的代表者是D.Rumelhart和J.McCelland,这是一种按误差逆传播算法训练的多层前馈网络,是目前应用最广泛的神经网络模型之一.其基本组成结构为输入层、中间层以及输出层.

  • 输入层:各个神经元负责接收来自外界的输入信息,并传递给中间层的各个神经元.
  • 中间层:中间层是内部信息处理层,负责信息变换,根据信息变换能力的需求,中间层可以设计
    为单隐藏层或者多隐藏层结构.
  • 输出层:传递到输出层各个神经元的信息,经过进一步处理后,完成一次学习的正向传播处理过程,由输出层向外界输出信息处理结果.

而BP算法(反向传播算法)的学习过程是由信息的正向传播和误差的反向传播两个过程组成.首先是输入数据经过模型的中间层计算后由输出层输出预测结果.

当实际输出与期望输出不符时,进入误差的反向传播阶段.误差通过输出层,按误差梯度下降的方
式修正各层的权值,向隐藏层、输入层逐层反传.周而复始的信息正向传播和误差反向传播过程是各层权值不断调整的过程,也是神经网络学习训练的过程,该过程一直进行到网络输出的误差减少到可以接受的程度,或者预先设定的学习次数为止.

目前,神经网络的研究方向和应用很多,反映了多学科交叉技术领域的特点.其主要的研究工作集
中在以下几个方面:

  • 生物原型研究:从生理学、心理学、解剖学、脑科学、病理学等生物科学方面研究神经细胞、神经网络、神经系统的生物原型结构及其功能机理.
  • 建立理论模型:根据生物原型的研究,建立神经元、神经网络的理论模型,其中包括概念模型、知识模型、物理化学模型、数学模型等.
  • 网络模型与算法研究:在理论模型研究的基础上构建具体的神经网络模型,以实现计算机模拟或硬件的仿真,还包括网络学习算法的研究.这方面的工作也称为技术模型研究.
  • 人工神经网络应用系统:在网络模型与算法研究的基础上,利用人工神经网络组成实际的应用系统.例如,完成某种信号处理或模式识别的功能,构建专家系统,制造机器人,等等.

纵观当代新兴科学技术的发展历史,人类在征服宇宙空间、基本粒子、生命起源等科学技术领域的进程中历经了崎岖不平的道路.我们也会看到,探索人脑功能和神经网络的研究将伴随着重重困难的克服而日新月异.


2.先把术语说清:BP不是一种神秘网络,而是一种求梯度的方法

BP 是 Backpropagation 的缩写,中文通常译为"反向传播".它不是某种固定的网络拓扑,而是一套高效计算参数梯度的算法.只要模型由可微运算组成,我们就能把它视为一个计算图,再利用链式法则从输出端向输入端计算梯度.

传统教材常把"使用反向传播训练的多层前馈网络"简称为"BP 神经网络".这种说法在教学语境中很常见,但容易让初学者误以为 BP 与卷积网络、循环网络、Transformer 是并列的网络类型.更准确的关系是:

  • 多层感知机、卷积神经网络、Transformer 描述的是模型结构;
  • 反向传播描述的是梯度计算过程;
  • SGD、Momentum、Adam、AdamW 描述的是拿到梯度后如何更新参数.

可以把训练系统想象成一支施工队:模型结构是建筑图纸,反向传播是测量每一处偏差的测绘系统,优化器才是根据测量结果调整材料和位置的施工策略.三者相互配合,但不能混为一谈.

校正 1:不要把“反馈神经网络”与“反向传播”混用。
“反馈网络”通常指存在环路或状态反馈的网络,例如一类循环神经网络;“反向传播”则指梯度沿计算图反向计算。前馈网络也能使用反向传播,循环网络则常使用“随时间反向传播”(BPTT)。两者不是同一个概念。


2.1从感知机到多层网络:真正的障碍是非线性

一个最简单的人工神经元可以写成:

z = w ⊤ x + b , a = ϕ ( z ) z = \mathbf{w}^\top \mathbf{x} + b, \qquad a = \phi(z) z=w⊤x+b,a=ϕ(z)

其中:

  • x \mathbf{x}x 是输入向量;
  • w \mathbf{w}w 是权重,表示每个输入的重要程度;
  • b bb 是偏置,用于平移决策边界;
  • ϕ \phiϕ 是激活函数;
  • a aa 是神经元输出.

如果不使用非线性激活函数,那么无论叠多少层线性变换,最终仍只是一次线性变换:

W 2 ( W 1 x + b 1 ) + b 2 = ( W 2 W 1 ) x + ( W 2 b 1 + b 2 ) W_2(W_1x+b_1)+b_2=(W_2W_1)x+(W_2b_1+b_2) W2​(W1​x+b1​)+b2​=(W2​W1​)x+(W2​b1​+b2​)

这说明"堆层数"本身并不会自动获得更强表达能力.真正让多层网络能够描述弯曲决策边界的,是层与层之间的非线性激活.

XOR 是最经典的例子:输入相同输出0,输入不同输出1.

x 1 x_1x1​x 2 x_2x2​XOR
000
011
101
110

在二维平面上,两个输出为1的点位于对角线上,不可能用一条直线与两个输出为 0 的点完全分开.单层线性分类器因此无能为力.加入隐藏层和非线性函数后,网络可以先构造多个线性边界,再把它们组合成非线性区域.


2.2历史脉络中的几个常见误区

神经网络史并不是"某个人在某一年突然发明一切",而是神经元建模、学习规则、自动微分和计算能力逐步汇合的结果.理解这段历史时,需要注意几个常见错位:

校正 2:三个容易被写错的时间点。

  1. Widrow-Hoff 的 LMS/Delta 学习规则通常追溯到 1960 年前后的 ADALINE 工作,而不是 1930 年。
  2. Minsky 与 Papert 的《Perceptrons》初版出版于 1969 年,而不是 1939 年。它系统分析了感知机能力边界,但“因此单独导致神经网络寒冬”是一种过度简化。
  3. Rumelhart、Hinton 与 Williams 1986 年的论文让误差反向传播在多层网络训练中广为人知,但链式求导、反向模式自动微分及更早的相关思路已有前置工作。因此更稳妥的说法是“推广和系统化”,不是“凭空发明”。

这段历史真正重要的启示是:多层网络并不缺表达能力,长期缺少的是可扩展的参数学习方法. 反向传播把"每个参数应该改多少"从指数级重复计算,变成可以复用中间结果的高效过程.


3.反向传播神经网络两个基础算法详解

在正式介绍BP神经网络之前,首先介绍两个非常重要的算法,即随机梯度下降算法和最小二乘法.最小二乘法是统计分析中一种最常用的逼近计算算法,其交替计算结果使得最终结果尽可能地逼近真实结果.而随机梯度下降算法充分利用了深度学习的运算特性,具有高效性和迭代性,通过不停地判断和选择当前目标下的最优路径,使得能够在最短路径下达到最优的结果,从而提高大数据的计算效率.


3.1最小二乘法详解

最小二乘法(LS算法)是一种数学优化技术,也是机器学习的常用算法.它通过最小化误差的平方
和寻找数据的最佳函数匹配,可以简便地求得未知的数据,并使得这些求得的数据与实际数据之间误差的平方和最小.最小二乘法还可用于曲线拟合.其他一些优化问题也可通过最小化能量或最大化熵用最小二乘法来表达.由于最小二乘法不是本文的重点内容,因此这里我们只通过图示演示一下最小二乘法的原理,如下图所示.

从图可以看到,若干个点依次分布在向量空间中,如果希望找出一条直线和这些点达到最佳匹配
,那么最简单的方法是希望这些点到直线的值最小,即下面的最小二乘法实现公式最小.

这里直接引用真实值与计算值之间的差的平方和,具体而言,这种差值有一个专门的名称——残差.基于此,表达残差的方式有以下3种:
∞-范数:残差绝对值的最大值

L1-范数:绝对残差和

L2-范数:残差平方和

可以看到,所谓的最小二乘法,就是L2-范数的一个具体应用.通俗地说,就是看模型计算出的结果
与真实值之间的相似性.
因此,最小二乘法可定义如下:
对于给定的数据(xi,yi)(i=1,…,m),在取定的假设空间H中,求解f(x)∈H,使得残差δ=Σ(f(xi)-yi)2的L
2-范数最小.看到这里,可能有人会提出疑问,这里的f(x)该如何表示呢?实际上,函数f(x)是一条多项式函数曲线:

假设我们用直线预测数据:

y ^ i = w x i + b \hat{y}_i=wx_i+b y^​i​=wxi​+b

第 i ii 个样本的残差为:

r i = y ^ i − y i r_i=\hat{y}_i-y_i ri​=y^​i​−yi​

如果直接把残差相加,正负误差可能相互抵消.因此最小二乘法使用残差平方和:

J ( w , b ) = 1 2 m ∑ i = 1 m ( y ^ i − y i ) 2 J(w,b)=\frac{1}{2m}\sum_{i=1}^{m}(\hat{y}_i-y_i)^2 J(w,b)=2m1​i=1∑m​(y^​i​−yi​)2

m mm 是样本数,系数 1 / 2 1/21/2 只是为了求导后抵消平方产生的2.训练目标就是寻找一组 w , b w,bw,b,使 J JJ 尽可能小.

校正 3:平方和与 L2 范数不是完全同一个量。
残差向量 r \mathbf{r}r 的 L2 范数是 ∥ r ∥ 2 = ∑ i r i 2 \|\mathbf{r}\|_2=\sqrt{\sum_i r_i^2}∥r∥2​=∑i​ri2​ ​,残差平方和是 ∥ r ∥ 2 2 \|\mathbf{r}\|_2^2∥r∥22​。机器学习中经常口语化地说“L2 损失”,但推导时最好区分“范数”和“范数平方”。

最小二乘适合回归,但分类问题通常采用更匹配概率模型的损失.例如二分类常用二元交叉熵:

L = − 1 m ∑ i = 1 m [ y i log ⁡ y ^ i + ( 1 − y i ) log ⁡ ( 1 − y ^ i ) ] \mathcal{L}=-\frac{1}{m}\sum_{i=1}^{m} \left[y_i\log \hat{y}_i+(1-y_i)\log(1-\hat{y}_i)\right] L=−m1​i=1∑m​[yi​logy^​i​+(1−yi​)log(1−y^​i​)]

多分类和语言模型则常使用多类交叉熵.现代大模型的"预测下一个 token",本质上就是在庞大词表上做多分类,并最小化真实 token 的负对数似然.

由上面的公式我们知道,所谓的最小二乘法,就是找到一组权重w,使得δ=Σ(f(xi)-yi)2最小.问题
又来了,如何能使得最小二乘法的值最小?

对于求出最小二乘法的结果,可以使用数学上的微积分处理方法,这是一个求极值的问题,只需要
对权值依次求偏导数,最后令偏导数为0,即可求出极值点.

具体实现最小二乘法的代码如下(为了简化起见,使用一元一次方程组进行演示拟合).

1import numpy as np
2from matplotlib import pyplot as plt
3
4A = np.array([[5], [4]])
5C = np.array([[4], [6]])
6
7B = A.T @ C
8AA = np.linalg.inv(A.T @ A)
9l = AA @ B
10P = A @ l
11
12t = np.linspace(-2, 2, 10).reshape(1, 10)
13xx = A @ t
14
15# 转成一维数组,方便 matplotlib 画点和标注
16Af = A.ravel()
17Cf = C.ravel()
18Pf = P.ravel()
19
20fig = plt.figure()
21ax = fig.add_subplot(111)
22
23ax.plot(xx[0, :], xx[1, :])
24ax.plot(Af[0], Af[1], 'ko')
25ax.plot([Cf[0], Pf[0]], [Cf[1], Pf[1]], 'r-o')
26ax.plot([0, Cf[0]], [0, Cf[1]], 'm-o')
27
28ax.axvline(x=0, color='black')
29ax.axhline(y=0, color='black')
30
31margin = 0.1
32ax.text(Af[0] + margin, Af[1] + margin, "A", fontsize=20)
33ax.text(Cf[0] + margin, Cf[1] + margin, "C", fontsize=20)
34ax.text(Pf[0] + margin, Pf[1] + margin, "P", fontsize=20)
35ax.text(0 + margin, 0 + margin, "O", fontsize=20)
36ax.text(0 + margin, 4 + margin, "y", fontsize=20)
37ax.text(4 + margin, 0 + margin, "x", fontsize=20)
38
39plt.xticks(np.arange(-10, 11, 1))
40plt.yticks(np.arange(-10, 11, 1))
41ax.axis("equal")
42ax.grid(True)
43
44plt.show()
45

最终结果如下图所示.


3.2梯度下降算法

在介绍随机梯度下降算法之前,给大家讲一个道士下山的故事.请先看一下下面这张图.

这是一个模拟随机梯度下降算法的演示图.为了便于理解,我们将其比喻成道士想要出去游玩的一座山.设想道士有一天和道友一起到一座不太熟悉的山上去玩,在兴趣盎然中很快登上了山顶.但是天有不测,下起了雨.如果这时需要道士及其道友用最快的速度下山,那么怎么办呢?

如果想以最快的速度下山,那么最快的办法就是顺着坡度最陡峭的地方走下去.但是由于不熟悉路,道士在下山的过程中,每走过一段路程就需要停下来观望,从而选择最陡峭的下山路.这样一路走下来的话,可以在最短时间内走到底.

从图上可以近似地表示为:① → ② → ③ → ④ → ⑤ → ⑥ → ⑦

每个数字代表每次停顿的地点,这样只需要在每个停顿的地点选择最陡峭的下山路即可.

这就是道士下山的故事,随机梯度下降算法和这个类似.如果想要使用最迅捷的下山方法,那么最
简单的办法就是在下降一个梯度的阶层后,寻找一个当前获得的最大坡度继续下降.这就是随机梯度算法的原理.

从上面的例子可以看到,随机梯度下降算法就是不停地寻找某个节点中下降幅度最大的那个趋势进行迭代计算,直到将数据收缩到符合要求的范围为止.通过数学公式表达的方式计算的话,公式如下:

在讲解最小二乘法的时候,我们通过最小二乘法说明了直接求解最优化变量的方法,也介绍了
求解的前提条件是要求计算值与实际值的偏差的平方最小.但是在随机梯度下降算法中,对于系数需要不停地求解出当前位置下最优化的数据.使用数学方式表达的话,就是不停地对系数θ求偏导数,公式如下:

公式中θ会向着梯度下降最快的方向减小,从而推断出θ的最优解.因此,随机梯度下降算法最终被归结为:通过迭代计算特征值,从而求出最合适的值.求解θ的公式如下:

公式中α是下降系数.用较为通俗的话表示,就是用来计算每次下降的幅度大小.系数越大,每次
计算中的差值就越大;系数越小,差值就越小,但是计算时间也相对延长.随机梯度下降算法的迭代过程如下图所示.

从图中可以看到,实现随机梯度下降算法的关键是拟合算法的实现.而本例拟合算法的实现较为
简单,通过不停地修正数据值,从而达到数据的最优值.

随机梯度下降算法在神经网络特别是机器学习中应用较广,但是由于其天生的缺陷,噪声较大,使
得其在计算过程中并不是都向着整体最优解的方向优化,往往只能得到局部最优解.因此,为了克服这些困难,最好的办法就是增大数据量,在不停地使用数据进行迭代处理的时候,能够确保整体的方向是全局最优解,或者最优结果在全局最优解附近.

1x = [(2, 0, 3), (1, 0, 3), (1, 1, 3), (1, 4, 2), (1, 2, 4)]
2y = [5, 6, 8, 10, 11]
3
4epsilon = 0.002
5alpha = 0.02
6diff = [0, 0]
7max_iter = 1000
8error0 = 0
9error1 = 0
10cnt = 0
11m = len(x)
12
13theta0 = 0
14theta1 = 0
15theta2 = 0
16
17while True:
18    cnt += 1
19
20    for i in range(m):
21        diff[0] = (theta0 * x[i][0] + theta1 * x[i][1] + theta2 * x[i][2]) - y[i]
22        theta0 -= alpha * diff[0] * x[i][0]
23        theta1 -= alpha * diff[0] * x[i][1]
24        theta2 -= alpha * diff[0] * x[i][2]
25
26    error1 = 0
27
28    for lp in range(len(x)):
29        error1 += (y[lp] - (theta0 + theta1 * x[lp][1] + theta2 * x[lp][2])) ** 2 / 2
30
31    if abs(error1 - error0) < epsilon:
32        break
33    else:
34        error0 = error1
35
36print('theta0 : %f, theta1 : %f, theta2 : %f, error1 : %f' % (theta0, theta1, theta2, error1))
37print('Done: theta0 : %f, theta1 : %f, theta2 : %f' % (theta0, theta1, theta2))
38print('迭代次数: %d' % cnt)
39


从结果来看,这里迭代24次即可获得最优解.

梯度不是"一个斜率",而是一组参数的导航向量
只有一个变量时,导数表示函数在某点的局部变化率.参数很多时,我们把所有偏导数组合起来:

∇ θ J = [ ∂ J ∂ θ 1 , ∂ J ∂ θ 2 , … , ∂ J ∂ θ n ] ⊤ \nabla_{\theta}J= \left[ \frac{\partial J}{\partial \theta_1}, \frac{\partial J}{\partial \theta_2}, \ldots, \frac{\partial J}{\partial \theta_n} \right]^\top ∇θ​J=[∂θ1​∂J​,∂θ2​∂J​,…,∂θn​∂J​]⊤

这个向量叫梯度.梯度指向函数局部上升最快的方向,所以负梯度 − ∇ θ J -\nabla_\theta J−∇θ​J 指向局部下降最快的方向.最基本的参数更新为:

θ t + 1 = θ t − η ∇ θ J ( θ t ) \theta_{t+1}=\theta_t-\eta\nabla_\theta J(\theta_t) θt+1​=θt​−η∇θ​J(θt​)

η \etaη 是学习率.它控制每次沿下降方向走多远.


"下山"比喻很有用,但需要补上三个边界:

  1. 梯度给出的只是当前位置附近的最快下降方向,不是从起点到终点的全局最短路线.
  2. 学习率太大可能跨过谷底甚至发散,太小则训练缓慢或停在平坦区域.
  3. 深度网络的损失地形是高维的,鞍点、平坦区和尺度不一致往往比二维图中的局部极小值更常见.

3.3最小二乘法的梯度下降算法及其Python实现

从前面的介绍可以得知,任何一个需要进行梯度下降的函数都可以比作一座山,而梯度下降的目标就是找到这座山的底部,也就是函数的最小值.根据之前道士下山的场景,最快的下山方式就是找到最为陡峭的山路,然后沿着这条山路走下去,直到下一个观望点.之后在下一个观望点重复这个过程,寻找最为陡峭的山路,直到山脚.
下面带领大家实现这个过程,求解最小二乘法的最小值,但是在开始之前,为大家展示一些需要掌
握的数学原理.

1.微分
在高等数学中,对函数微分的解释有很多,主要有两种:

  • 函数曲线上某点切线的斜率.
  • 函数的变化率.

因此,对于一个二元微分的计算如下:

2.梯度
所谓的梯度,就是微分的一般形式,对于多元微分来说,微分就是各个变量的变化率的总和,例子如下:

可以看到,求解的梯度值是分别对每个变量进行微分计算,之后用逗号隔开.这里用中括号"[]"将
每个变量的微分值包裹在一起形成一个三维向量,因此可以认为微分计算后的梯度是一个向量.
因此可以得出梯度的定义:在多元函数中,梯度是一个向量,而向量具有方向性,梯度的方向指出
了函数在给定点上的变化最快的方向.这与上面道士下山的过程联系在一起的表达就是,如果道士想最快到达山底,则需要在每一个观察点寻找梯度最陡峭下降的地方.如下图所示.而梯度的计算的目标就是得到这个多元向量的具体值.

3.梯度下降的数学计算
前面已经给出了梯度下降的公式,此处对其进行变形:

此公式中的参数的含义如下:
J是关于参数θ的函数,假设当前点为θ,如果需要找到这个函数的最小值,也就是山底的话,那么首先需要确定行进的方向,也就是梯度计算的反方向,之后走α的步长,走完这个步长之后就到了下
一个观察点.
α的意义前面已经介绍过了,是学习率或者步长,使用α来控制每一步走的距离.α过小会造成拟合时间过长,而α过大会造成下降幅度太大错过最低点,如下图所示.

学习率太小(左)与学习率太大(右)

这里需要注意的是,梯度公式中,J(θ)求出的是斜率的最大值,也就是梯度上升最大的方向,而这里所需要的是梯度下降最大的方向,因此在J(θ)前加一个负号.下面用一个例子演示梯度下降法的
计算.假设这里的公式为:

此时的微分公式为:

设第一个值θ0=1,α=0.3,则根据梯度下降公式:

这样依次运算,即可得到J(θ)的最小值,也就是"山底",如下图所示.

实现程序如下:

1import numpy as np
2     x = 1
3     def chain(x,gama = 0.1):
4        x = x - gama * 2 * x
5        return x
6     
7     for _ in range(4):
8        x = chain(x)
9        print(x)
10

多变量的梯度下降法和前文所述的多元微分求导类似.例如一个二元函数形式如下:

此时对其的梯度微分为:

此时将设置:

则依次计算的结果如下:

剩下的计算请大家自行完成.如果把二元函数采用图像的方式展示出来,可以很明显地看到梯度下降的每个"观察点"坐标,如下图所示.

4.使用梯度下降法求解最小二乘法
下面是本节的实战部分,使用梯度下降法计算最小二乘法.假设最小二乘法的公式如下:

其中参数解释如下:
m是数据点总数.

y是数据集中每个点的真实y坐标的值.
其中hθ(x )为预测函数,形式如下:

根据每个输入x,都有一个经过参数计算后的预测值输出.
hθ(x)的Python实现如下:

1h_pred = np.dot(x,theta)
2

其中x是输入的维度为[-1,2]的二维向量,-1的意思是维度不定.这里使用了一个技巧,即将hθ(x)的公式转化成矩阵相乘的形式,而theta是一个[2,1]维度的二维向量.
依照最小二乘法实现的Python代码如下:

1def error_function(theta,x,y):
2        h_pred = np.dot(x,theta)
3        j_theta = (1./2*m) * np.dot(np.transpose(h_pred), h_pred)
4        return j_theta
5

这里j_theta的实现同样是将原始公式转化成矩阵计算,即:

下面分析一下最小二乘法的公式J(θ),此此时如果求J(θ)的梯度,则需要对其中涉及的两个参数θ0和θ1进行微分:

下面分别对这两个参数的求导公式进行求导:

此时,将分开求导的参数合并可得新的公式:

此时,公式最右边的常数1可以被去掉,公式变为:

此时,依旧采用矩阵相乘的方式,使用矩阵相乘表示的公式为:


已经转换为矩阵相乘的表示形式.使用Python表示如下:

1def gradient_function(theta, X, y):
2        h_pred = np.dot(X, theta) - y
3        return (1./m) * np.dot(np.transpose(X), h_pred)
4

如果大家对np.dot(np.transpose(X),h_pred)理解有难度,可以将公式使用逐个x值的形式列出来看看如何,这里就不罗列了.最后是梯度下降的Python实现,代码如下:

1def gradient_descent(X, y, alpha):
2        theta = np.array([1, 1]).reshape(2, 1)  #[2,1]  这里的theta是参数
3        gradient = gradient_function(theta,X,y)
4        for i in range(17):
5           theta = theta - alpha * gradient
6           gradient = gradient_function(theta, X, y)
7        return theta
8

或者使用如下代码:

1def gradient_descent(X, y, alpha):
2    theta = np.array([1, 1]).reshape(2, 1)  # [2,1] 这里的theta是参数
3    gradient = gradient_function(theta, X, y)
4
5    while not np.all(np.absolute(gradient) <= 1e-4):  # 采用abs是因为gradient计算的是负梯度
6        theta = theta - alpha * gradient
7        gradient = gradient_function(theta, X, y)
8        print(theta)
9
10    return theta
11

这两组程序段的区别在于,第一个代码段是固定循环次数,可能会造成欠下降或者过下降,而第二
个代码段使用的是数值判定,可以设定阈值或者停止条件.
全部代码如下:

1import numpy as np
2import matplotlib.pyplot as plt
3
4m = 20
5
6# 生成数据集 x
7x0 = np.ones((m, 1))
8x1 = np.arange(1, m + 1).reshape(m, 1)
9x = np.hstack((x0, x1))  # [20, 2]
10
11y = np.array([
12    3, 4, 5, 5, 2, 4, 7, 8, 11, 8,
13    12, 11, 13, 13, 16, 17, 18, 17, 19, 21
14]).reshape(m, 1)
15
16alpha = 0.01
17
18
19# 误差函数 J(theta)
20def error_function(theta, x, y):
21    h_pred = np.dot(x, theta) - y
22    j_theta = (1. / (2 * m)) * np.dot(np.transpose(h_pred), h_pred)
23    return j_theta
24
25
26# 梯度函数
27def gradient_function(theta, x, y):
28    h_pred = np.dot(x, theta) - y
29    return (1. / m) * np.dot(np.transpose(x), h_pred)
30
31
32# 梯度下降
33def gradient_descent(x, y, alpha):
34    theta = np.array([1, 1], dtype=float).reshape(2, 1)
35    gradient = gradient_function(theta, x, y)
36
37    cnt = 0
38    max_iter = 100000
39
40    while not np.all(np.absolute(gradient) <= 1e-6):
41        theta = theta - alpha * gradient
42        gradient = gradient_function(theta, x, y)
43
44        cnt += 1
45        if cnt >= max_iter:
46            break
47
48    return theta, cnt
49
50
51theta, cnt = gradient_descent(x, y, alpha)
52
53y_pred = np.dot(x, theta)
54error = error_function(theta, x, y)[0, 0]
55
56print('optimal theta:')
57print(theta)
58
59print('theta0:', theta[0, 0])
60print('theta1:', theta[1, 0])
61print('error function:', error)
62print('迭代次数:', cnt)
63
64
65# 绘制拟合曲线
66plt.figure(figsize=(8, 5))
67
68plt.scatter(x1, y, color='red', label='真实数据')
69plt.plot(x1, y_pred, color='blue', label='拟合曲线')
70
71plt.xlabel('x')
72plt.ylabel('y')
73plt.title('Linear Regression Fitting')
74plt.legend()
75plt.grid(True)
76
77plt.show()
78



上图是打印结果和拟合曲线.

现在请大家回到前面的道士下山这个问题,这个下山的道士实际上就代表了反向传播算法,而要寻找的下山路径其实就代表着算法中一直在寻找的参数θ,山上当前点最陡峭的方向实际上就是代价函数在这个点的梯度方向,场景中观察最陡峭方向所用的工具就是微分.


4.反馈神经网络反向传播算法介绍

反向传播算法是神经网络的核心与精髓,在神经网络算法中拥有举足轻重的地位.

用通俗的话说,反向传播算法就是复合函数的链式求导法则的一个强大应用,而且实际上的应用比理论上的推导强大得多.下面将主要介绍反馈神经网络反向传播链式法则以及公式的推导,虽然整体过程比较简单,但这却是整个深度学习神经网络的理论基础.


4.1深度学习基础

机器学习在理论上可以看作是统计学在计算机科学上的一个应用.在统计学上,一个非常重要的内容就是拟合和预测,即基于以往的数据建立光滑的曲线模型来实现数据结果与数据变量的对应关系.

深度学习是统计学的应用,同样是为了寻找结果与影响因素的一一对应关系.只不过样本点由狭义的x和y扩展到向量、矩阵等广义的对应点.此时,由于数据变得复杂,对应关系模型的复杂度也随之增加,而不能使用一个简单的函数表达.

数学上通过建立复杂的高次多元函数解决复杂模型拟合的问题,但是大多数情况都会失败,因为过于复杂的函数式是无法求解的,也就是无法获取其公式.

基于前人的研究,科研工作人员发现可以通过神经网络来表示这样的一一对应关系,而神经网络本质就是一个多元复合函数,通过增加神经网络的层次和神经单元可以更好地表达函数的复合关系.

下图是多层神经网络的图像表达方式,通过设置输入层、隐藏层与输出层可以形成多元函数用于求解相关问题.

通过数学表达式将多层神经网络模型表达出来,公式如下:

其中x是输入数值,w是相邻神经元之间的权重,也就是神经网络在训练过程中需要学习的参数.与线性回归类似,神经网络学习同样需要一个"损失函数",即训练目标通过调整每个权重值w来使得损失函数最小.前面在讲解梯度下降算法的时候已经讲过,如果权重过大或者指数过大,直接求解系数是一件不可能的事情,因此梯度下降算法是求解权重问题的比较好的方法.


4.2链式求导法则

在前面介绍梯度下降算法时,没有对其背后的原理做出详细介绍.实际上,梯度下降算法就是链式
法则的一个具体应用,如果把前面公式中的损失函数以向量的形式表示为:

那么其梯度向量为:

可以看到,其实所谓的梯度向量就是求出函数在每个向量上的偏导数之和.这也是链式法则擅长解决的问题.



本例中为了求得最终值e对各个点的梯度,需要将各个点与e联系在一起,例如期望求得e对输入点a的梯度,则只需要求得:

这样就把e与a的梯度联系在一起了,同理可得:

如下图所示.这样做的好处是显而易见的,求e对a的偏导数只要建立一个e到a的路径即可,图中经过c,那么通过相关的求导链接就可以得到所需要的值.对于求e对b的偏导数,也只需要建立所有e到b路径中的求导路径,从而获得需要的值.


4.3反馈神经网络的原理与公式推导

在求导过程中,可能有大家已经注意到,如果拉长了求导过程或者增加了其中的单元,就会大大增
加其中的计算过程,即很多偏导数的求导过程会被反复计算,因此在实际应用中,对于权值达到十万甚至百万的神经网络来说,这样的重复冗余所导致的计算量是很大的.

同样是为了求得对权重的更新,反馈神经网络算法将训练误差E看作以权重向量每个元素为变量的高维函数,通过不断更新权重寻找训练误差的最低点,按误差函数梯度下降的方向更新权值.

提示:反馈神经网络算法的具体计算公式在本节后半部分进行推导.首先求得最后的输出层与真实值之间的差距,如下图所示.

反馈神经网络最终误差的计算

之后以计算出的测量值与真实值为起点,反向传播到上一个节点,并计算出节点的误差值,如下图所示.

反馈神经网络输出层误差的反向传播

以后将计算出的节点误差重新设置为起点,依次向后传播误差,如下图所示.

反馈神经网络隐藏层误差的反向传播

注意:对于隐藏层,误差并不是像输出层一样由单个节点确定,而是由多个节点确定,因此对它的
计算要求得所有的误差值之和.通俗地解释,一般情况下,误差的产生是由于输入值与权重的计算产生了错误,而输入值往往是固定不变的,因此对于误差的调节,需要对权重进行更新.而权重的更新又以输入值与真实值的偏差为基础,当最终层的输出误差被反向一层一层地传递回来后,每个节点都会被相应地分配适合其在神经网络中地位的误差,即只需要更新其所需承担的误差量,如下图所示.

反馈神经网络权重的更新

也就是在每一层需要维护输出对当前层的微分值,该微分值相当于被复用于之前每一层中权值的微分计算,因此空间复杂度没有变化.同时,也没有重复计算,每个微分值都会在之后的迭代中使用.

下面介绍公式的推导.公式的推导需要使用一些高等数学的知识,大家可以自由选择学习.
从前文分析来看,对于反馈神经网络算法主要需要得到输出值与真实值之前的差值,之后再利用这个差值去对权重进行更新.而这个差值在不同的传递层有着不同的计算方法:

  • 对于输出层单元:误差项是真实值与模型计算值之间的差值.
  • 对于隐藏层单元:由于缺少直接的目标值来计算隐藏层单元的误差,因此需要以间接的方式来计算隐藏层的误差项,并对受隐藏层单元影响的每个单元的误差进行加权求和.

而在其后的权值更新部分,则主要依靠学习速率、该权值对应的输入以及单元的误差项来完成.

1.前向传播算法
对于前向传播的值传递,隐藏层的输出值定义如下:

其中Xi是当前节点的输入值


是输出值.f是当前节点的激活函数,

为当前节点的输入值经过计算后被激活的值.
而对于输出层,定义如下:


为输入的权重

为将节点输入数据经过计算后的激活值作为输入值.这里对所有输入值进行权重计算后求得和值,作为神经网络的最后输出值

2.反向传播算法
与前向传播类似,首先需要定义两个值




为输出层的误差项,其计算值为真实值与模型计算值之间的差值.Y是计算值,T是真实值.

为输出层的误差.


来说,无论定义在哪个位置,都可以看作是当前的输出值对于输入值的梯度计算.

通过前面的分析可以知道,所谓的神经网络反馈算法,就是逐层地对最终误差进行分解,即每一层
只与下一层打交道,如下图所示.据此可以假设每一层均为输出层的前一个层级,通过计算前一个层级与输出层的误差得到权重的更新.

权重的逐层反向传导

因此,反馈神经网络的计算公式如下:

也就是当前层输出值对误差的梯度可以通过下一层的误差与权重和输入值的梯度乘积获得.公式

中,若

为输出层,则可以通过

求得

的值,若

为非输出层,则可以使用逐层反馈的方式求得

的值.



来说,其计算结果都是当前的输出值对于输入值的梯度计算,这是权重更新过程中一个非常重要的数据计算内容.也可以换一种表述形式,将前面的公式表示为:

可以看到,通过更为泛化的公式,把当前层的输出对输入的梯度计算转换成求下一个层级的梯度计算值.

3.权重的更新
反馈神经网络计算的目的是对权重的更新,因此与梯度下降算法类似,其更新可以仿照梯度下降对权值的更新公式:

即:

其中ji表示反向传播时对应的节点系数,通过对

的计算,就可以更新对应的权重值.Wji的计算公式如上所示.而对于没有推导的bji,其推导过程与Wji类似,但是在推导过程中输入值是被消去的,请大家自行学习.


4.4反馈神经网络原理的激活函数

现在回到反馈神经网络的函数:

对于此公式中的



以及所需要计算的目标

已经做了较为详尽的解释.但是对

却一直没有做出介绍.

回到前面生物神经元的图示,传递进来的电信号通过神经元进行传递,由于神经元的突触强弱是有一定的敏感度的,因此只会对超过一定范围的信号进行反馈,即这个电信号必须大于某个阈值,神经元才会被激活引起后续的传递.在训练模型中同样需要设置神经元的阈值,即神经元被激活的频率用于传递相应的信息,模型中这种能够确定是否为当前神经元节点的函数被称为激活函数,如下图所示.

激活函数示意图

激活函数代表生物神经元接收到的信号的强度,目前应用较广的是Sigmoid函数.因为其在运行过程中只接收一个值,所以输出也是一个经过公式计算后的值,且其输出值的取值范围为0~1.

Sigmoid激活函数图如下图所示.

其倒函数的求法也较为简单,即:

换一种表示方式为:

Sigmoid函数可以将任意实值映射到0~1.对于较大值的负数,Sigmoid函数将其映射成0,而对于较大值的正数Sigmoid函数将其映射成1.顺带讲一下,Sigmoid函数在神经网络模型中占据了很长时间的统治地位,但是目前已经不常使用,主要原因是其非常容易区域饱和,当输入开始非常大或者非常小的时候,Sigmoid会产生一个平缓区域,其中的梯度值几乎为0,而这又会造成梯度传播过程中产生接近0的传播梯度,这样在后续的传播中会造成梯度消散的现象,因此并不适合现代的神经网络模型使用.除此之外,近年来涌现出了大量新的激活函数模型,例如Maxout、Tanh和ReLU模型,这些模型解决了传统的Sigmoid模型在更深程度的神经网络上所产生的各种不良影响.


激活函数不仅决定前向传播如何变形数据,也决定反向传播时梯度能通过多少.
1.Sigmoid

σ ( x ) = 1 1 + e − x \sigma(x)=\frac{1}{1+e^{-x}} σ(x)=1+e−x1​

σ ′ ( x ) = σ ( x ) ( 1 − σ ( x ) ) \sigma'(x)=\sigma(x)(1-\sigma(x)) σ′(x)=σ(x)(1−σ(x))

Sigmoid 输出在 ( 0 , 1 ) (0,1)(0,1),适合表示二分类概率.但其最大导数只有 0.25 0.250.25;当 ∣ x ∣ |x|∣x∣ 较大时,导数接近 0.深层网络中连续乘上许多小于1的导数,梯度会快速衰减.

2.Tanh

tanh ⁡ ( x ) = e x − e − x e x + e − x \tanh(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}} tanh(x)=ex+e−xex−e−x​

d d x tanh ⁡ ( x ) = 1 − tanh ⁡ 2 ( x ) \frac{d}{dx}\tanh(x)=1-\tanh^2(x) dxd​tanh(x)=1−tanh2(x)

Tanh 输出在 ( − 1 , 1 ) (-1,1)(−1,1),以 0 为中心,通常比 Sigmoid 更适合隐藏层.但它在两端同样会饱和,仍可能出现梯度消失.

3.ReLU

R e L U ( x ) = max ⁡ ( 0 , x ) \mathrm{ReLU}(x)=\max(0,x) ReLU(x)=max(0,x)

其导数在负区间为 0、正区间为 1, x = 0 x=0x=0 处不可导,但实践中约定一个次梯度即可.ReLU 计算简单,在正区间不饱和,因此长期成为深层网络的主流基础激活函数.

它也并非完美:如果神经元长期落在负区间,梯度始终为 0,可能形成"死亡 ReLU".Leaky ReLU、GELU、SiLU 等函数从不同角度改善了这个问题.Transformer 和现代大模型中常见 GELU 或 SwiGLU 一类结构.

激活函数输出范围优点主要风险常见位置
Sigmoid( 0 , 1 ) (0,1)(0,1)概率解释直观饱和、非零中心、梯度小二分类输出层、门控
Tanh( − 1 , 1 ) (-1,1)(−1,1)零中心、平滑两端饱和经典 RNN、教学网络
ReLU[ 0 , + ∞ ) [0,+\infty)[0,+∞)简单、正区间梯度稳定死亡神经元经典深层隐藏层
GELU/SiLU平滑非线性梯度更平滑、现代模型常用计算略复杂Transformer、大模型

4.梯度消失与梯度爆炸:连乘效应的两面
设一个深层网络的局部导数连续相乘.如果每层导数的典型量级约为 0.5,经过 20 层后量级约为:

0.5 20 ≈ 9.54 × 10 − 7 0.5^{20}\approx9.54\times10^{-7} 0.520≈9.54×10−7

靠近输入端的层几乎收不到有效更新,这就是梯度消失.反过来,如果每层乘数典型量级为 1.5:

1.5 20 ≈ 3325 1.5^{20}\approx3325 1.520≈3325

梯度可能迅速膨胀,导致参数更新剧烈、数值溢出甚至出现 NaN.实际网络的雅可比矩阵远比这个标量例子复杂,但连乘导致尺度指数变化的直觉依然成立.

现代网络通过多种机制改善梯度传播:

  • 合理初始化:Xavier 初始化根据输入输出维度控制方差,常配合 Tanh;He 初始化考虑 ReLU 会截断负半轴,常用于 ReLU 网络.
  • 残差连接:令输出为 y = F ( x ) + x y=F(x)+xy=F(x)+x.反向时梯度中出现一条恒等路径,使深层网络不必完全依赖 F FF 内部的多次连乘.
  • 归一化:BatchNorm、LayerNorm、RMSNorm 等方法改善激活尺度与训练稳定性.它们不是简单"把梯度固定住",但能显著改变优化地形和数值行为.
  • 梯度裁剪:当整体梯度范数超过阈值时按比例缩小,常用于循环网络和大模型训练中的异常峰值保护.
  • 合适的激活与门控:ReLU、GELU、SiLU 以及门控前馈结构改善了不同模型中的表达与梯度特性.

这些技术并不意味着链式法则失效,而是在设计计算图时主动塑造每一段局部导数,让反向信号能够穿过更深的网络.


4.5反馈神经网络原理的Python实现

下面将使用Python语言实现神经网络的反馈算法.经过前面的讲解,大家对神经网络的算法和描
述应该有了一定的理解,将使用Python代码来实现一个自己的反馈神经网络.

为了简单起见,这里的神经网络被设置成3层,即只有一个输入层、一个隐藏层以及一个最终的输出层.

(1)确定辅助函数:

1def rand(a, b):
2        return (b - a) * random.random() + a
3     def make_matrix(m,n,fill=0.0):
4        mat = []
5        for i in range(m):
6           mat.append([fill] * n)
7        return mat
8     def sigmoid(x):
9        return 1.0 / (1.0 + math.exp(-x))
10     def sigmod_derivate(x):
11     return x * (1 - x)
12

代码首先定义了随机值,使用random包中的random函数生成了一系列随机数,之后的make_matrix函数生成了相对应的矩阵.sigmoid和sigmod_derivate分别是激活函数和激活函数的导函数.这也是前文所定义的内容.

(2)进入BP神经网络类的正式定义,类的定义需要对数据内容进行设定.

1def __init__(self):
2        self.input_n = 0
3        self.hidden_n = 0
4        self.output_n = 0
5        self.input_cells = []
6        self.hidden_cells = []
7        self.output_cells = []
8        self.input_weights = []
9        self.output_weights = []
10

init函数的作用是对神经网络参数的初始化,即在其中设置了输入层、隐藏层以及输出层中节点的个数;各个cell是各个层中节点的数值;weights代表各个层的权重.

(3)使用setup函数对init函数中设定的数据进行初始化.

1def setup(self,ni,nh,no):
2        self.input_n = ni + 1
3        self.hidden_n = nh
4        self.output_n = no
5        self.input_cells = [1.0] * self.input_n
6        self.hidden_cells = [1.0] * self.hidden_n
7        self.output_cells = [1.0] * self.output_n
8        self.input_weights = make_matrix(self.input_n,self.hidden_n)
9        self.output_weights = make_matrix(self.hidden_n,self.output_n)
10        # random activate
11        for i in range(self.input_n):
12           for h in range(self.hidden_n):
13              self.input_weights[i][h] = rand(-0.2, 0.2)
14        for h in range(self.hidden_n):
15           for o in range(self.output_n):
16              self.output_weights[h][o] = rand(-2.0, 2.0)
17
18

需要注意,输入层节点的个数被设置成ni+1,这是由于其中包含bias偏置数;各个节点与1.0相乘是初始化节点的数值;各个层的权重值根据输入层、隐藏层以及输出层中节点的个数被初始化并被赋值.

(4)定义完各个层的数目后,进入正式的神经网络内容的定义.对神经网络的前向计算如下:

1def predict(self,inputs):
2        for i in range(self.input_n - 1):
3           self.input_cells[i] = inputs[i]
4        for j in range(self.hidden_n):
5           total = 0.0
6           for i in range(self.input_n):
7              total += self.input_cells[i] * self.input_weights[i][j]
8           self.hidden_cells[j] = sigmoid(total)
9        for k in range(self.output_n):
10           total = 0.0
11           for j in range(self.hidden_n):
12              total += self.hidden_cells[j] * self.output_weights[j][k]
13           self.output_cells[k] = sigmoid(total)
14        return self.output_cells[:]
15

以上代码将数据输入函数中,通过隐藏层和输出层的计算,最终以数组的形式输出.案例的完整代
码如下:

1import numpy as np
2import math
3import random
4
5
6def rand(a, b):
7    return (b - a) * random.random() + a
8
9
10def make_matrix(m, n, fill=0.0):
11    mat = []
12    for i in range(m):
13        mat.append([fill] * n)
14    return mat
15
16
17def sigmoid(x):
18    return 1.0 / (1.0 + math.exp(-x))
19
20
21def sigmoid_derivate(x):
22    return x * (1 - x)
23
24
25class BPNeuralNetwork:
26    def __init__(self):
27        self.input_n = 0
28        self.hidden_n = 0
29        self.output_n = 0
30        self.input_cells = []
31        self.hidden_cells = []
32        self.output_cells = []
33        self.input_weights = []
34        self.output_weights = []
35
36    def setup(self, ni, nh, no):
37        self.input_n = ni + 1
38        self.hidden_n = nh
39        self.output_n = no
40
41        self.input_cells = [1.0] * self.input_n
42        self.hidden_cells = [1.0] * self.hidden_n
43        self.output_cells = [1.0] * self.output_n
44
45        self.input_weights = make_matrix(self.input_n, self.hidden_n)
46        self.output_weights = make_matrix(self.hidden_n, self.output_n)
47
48        # random activate
49        for i in range(self.input_n):
50            for h in range(self.hidden_n):
51                self.input_weights[i][h] = rand(-0.2, 0.2)
52
53        for h in range(self.hidden_n):
54            for o in range(self.output_n):
55                self.output_weights[h][o] = rand(-2.0, 2.0)
56
57    def predict(self, inputs):
58        for i in range(self.input_n - 1):
59            self.input_cells[i] = inputs[i]
60
61        for j in range(self.hidden_n):
62            total = 0.0
63            for i in range(self.input_n):
64                total += self.input_cells[i] * self.input_weights[i][j]
65            self.hidden_cells[j] = sigmoid(total)
66
67        for k in range(self.output_n):
68            total = 0.0
69            for j in range(self.hidden_n):
70                total += self.hidden_cells[j] * self.output_weights[j][k]
71            self.output_cells[k] = sigmoid(total)
72
73        return self.output_cells[:]
74
75    def back_propagate(self, case, label, learn):
76        self.predict(case)
77
78        # 计算输出层的误差
79        output_deltas = [0.0] * self.output_n
80        for k in range(self.output_n):
81            error = label[k] - self.output_cells[k]
82            output_deltas[k] = sigmoid_derivate(self.output_cells[k]) * error
83
84        # 计算隐藏层的误差
85        hidden_deltas = [0.0] * self.hidden_n
86        for j in range(self.hidden_n):
87            error = 0.0
88            for k in range(self.output_n):
89                error += output_deltas[k] * self.output_weights[j][k]
90            hidden_deltas[j] = sigmoid_derivate(self.hidden_cells[j]) * error
91
92        # 更新输出层的权重
93        for j in range(self.hidden_n):
94            for k in range(self.output_n):
95                self.output_weights[j][k] += learn * output_deltas[k] * self.hidden_cells[j]
96
97        # 更新隐藏层的权重
98        for i in range(self.input_n):
99            for j in range(self.hidden_n):
100                self.input_weights[i][j] += learn * hidden_deltas[j] * self.input_cells[i]
101
102        error = 0
103        for o in range(len(label)):
104            error += 0.5 * (label[o] - self.output_cells[o]) ** 2
105
106        return error
107
108    def train(self, cases, labels, limit=100, learn=0.05):
109        for i in range(limit):
110            error = 0
111            for i in range(len(cases)):
112                label = labels[i]
113                case = cases[i]
114                error += self.back_propagate(case, label, learn)
115
116    def test(self):
117        cases = [
118            [0, 0],
119            [0, 1],
120            [1, 0],
121            [1, 1],
122        ]
123
124        labels = [[0], [1], [1], [0]]
125
126        self.setup(2, 5, 1)
127        self.train(cases, labels, 10000, 0.05)
128
129        for case in cases:
130            print(self.predict(case))
131
132
133if __name__ == '__main__':
134    nn = BPNeuralNetwork()
135    nn.test()
136

5.NumPy手写反向传播:让网络学会 XOR

下面的示例只依赖 NumPy,网络结构为 2 → 4 → 1 2\rightarrow4\rightarrow12→4→1:隐藏层使用 Tanh,输出层使用 Sigmoid,损失函数使用二元交叉熵.代码完整展示前向传播、反向传播和参数更新.

1import numpy as np
2
3
4def sigmoid(x: np.ndarray) -> np.ndarray:
5    return 1.0 / (1.0 + np.exp(-x))
6
7
8rng = np.random.default_rng(42)
9
10X = np.array(
11    [
12        [0.0, 0.0],
13        [0.0, 1.0],
14        [1.0, 0.0],
15        [1.0, 1.0],
16    ]
17)
18y = np.array([[0.0], [1.0], [1.0], [0.0]])
19
20W1 = rng.normal(0.0, 0.5, size=(2, 4))
21b1 = np.zeros((1, 4))
22W2 = rng.normal(0.0, 0.5, size=(4, 1))
23b2 = np.zeros((1, 1))
24
25learning_rate = 0.3
26sample_count = X.shape[0]
27
28for epoch in range(10_001):
29    # Forward pass
30    Z1 = X @ W1 + b1
31    A1 = np.tanh(Z1)
32    Z2 = A1 @ W2 + b2
33    y_hat = sigmoid(Z2)
34
35    eps = 1e-8
36    loss = -np.mean(
37        y * np.log(y_hat + eps) + (1.0 - y) * np.log(1.0 - y_hat + eps)
38    )
39
40    # Backward pass
41    dZ2 = (y_hat - y) / sample_count
42    dW2 = A1.T @ dZ2
43    db2 = np.sum(dZ2, axis=0, keepdims=True)
44
45    dA1 = dZ2 @ W2.T
46    dZ1 = dA1 * (1.0 - A1**2)
47    dW1 = X.T @ dZ1
48    db1 = np.sum(dZ1, axis=0, keepdims=True)
49
50    W2 -= learning_rate * dW2
51    b2 -= learning_rate * db2
52    W1 -= learning_rate * dW1
53    b1 -= learning_rate * db1
54
55    if epoch % 2_000 == 0:
56        print(f"epoch={epoch:5d}, loss={loss:.6f}")
57
58print("probabilities:", np.round(y_hat.ravel(), 4))
59print("predictions:  ", (y_hat.ravel() >= 0.5).astype(int))
60


这段代码值得逐行观察:

  • A1 = np.tanh(Z1) 是隐藏层非线性;
  • dZ2 = (y_hat - y) / sample_count 来自 Sigmoid 与二元交叉熵组合后的化简;
  • dA1 = dZ2 @ W2.T 把输出误差传回隐藏表示;
  • dZ1 = dA1 * (1.0 - A1**2) 应用 Tanh 的局部导数;
  • W -= learning_rate * dW 才是真正的参数更新.

如果去掉隐藏层激活,把 Tanh 换成恒等函数,多个线性层仍等价于一层线性变换,XOR 就无法被正确分开.


6.PyTorch自动求导:loss.backward()做了什么

手写 BP 有助于理解数学,但真实项目通常让框架维护计算图.下面是等价的 PyTorch 实现:

1import torch
2from torch import nn
3
4
5torch.manual_seed(42)
6
7X = torch.tensor(
8    [
9        [0.0, 0.0],
10        [0.0, 1.0],
11        [1.0, 0.0],
12        [1.0, 1.0],
13    ],
14    dtype=torch.float32,
15)
16y = torch.tensor([[0.0], [1.0], [1.0], [0.0]], dtype=torch.float32)
17
18model = nn.Sequential(
19    nn.Linear(2, 4),
20    nn.Tanh(),
21    nn.Linear(4, 1),
22)
23loss_fn = nn.BCEWithLogitsLoss()
24optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
25
26for epoch in range(5_001):
27    logits = model(X)
28    loss = loss_fn(logits, y)
29
30    optimizer.zero_grad()
31    loss.backward()
32    optimizer.step()
33
34    if epoch % 1_000 == 0:
35        print(f"epoch={epoch:4d}, loss={loss.item():.6f}")
36
37with torch.no_grad():
38    probabilities = torch.sigmoid(model(X))
39    predictions = (probabilities >= 0.5).to(torch.int32)
40
41print("probabilities:", probabilities.squeeze(1).round(decimals=4).tolist())
42print("predictions:  ", predictions.squeeze(1).tolist())
43

这里有四个关键动作:

  1. logits = model(X):PyTorch 一边执行前向运算,一边建立动态计算图.
  2. optimizer.zero_grad():清空上一次迭代累积在参数上的梯度.
  3. loss.backward():从标量损失出发,按反向模式自动微分计算所有叶子参数的梯度.
  4. optimizer.step():读取参数的 .grad,按 SGD 规则更新权重.

BCEWithLogitsLoss 将 Sigmoid 与二元交叉熵组合在一起,利用数值稳定的形式避免先算概率再取对数可能产生的上溢或下溢.这也是工程代码与教学公式之间常见的差别:数学含义相同,实现会为数值稳定性做融合.


6.1动态计算图里到底保存了什么

当一个需要梯度的张量参与运算时,PyTorch 会为结果记录产生它的算子以及反向计算所需的上下文.例如乘法反向需要知道另一个乘数,Tanh 反向可以复用前向输出,线性层反向需要输入与权重.loss.backward() 从损失节点出发,按依赖关系的逆拓扑顺序调用这些反向规则.

"缓存中间量"会占用显存,这就是为什么模型训练通常比纯推理消耗更多显存.推理时使用 torch.no_grad(),框架不再构建需要反向遍历的图,可以节省大量内存.激活检查点则采取折中策略:前向时少保存一些中间量,反向时重新计算它们,用额外算力换取显存.

还要注意 PyTorch 默认会累积梯度.连续调用两次 backward(),参数 .grad 中通常是两次贡献之和.这一设计支持梯度累积和多分支损失,但普通训练循环如果忘记 zero_grad(),就会无意中把不同批次梯度叠加.


6.2用有限差分检查手写梯度

当你自己实现新算子或 BP 时,可以用中心差分检查某个参数 θ j \theta_jθj​:

g num = J ( θ j + ε ) − J ( θ j − ε ) 2 ε g_{\text{num}} =\frac{J(\theta_j+\varepsilon)-J(\theta_j-\varepsilon)}{2\varepsilon} gnum​=2εJ(θj​+ε)−J(θj​−ε)​

再与反向传播得到的解析梯度 g bp g_{\text{bp}}gbp​ 比较相对误差:

relative error = ∣ g num − g bp ∣ max ⁡ ( 1 , ∣ g num ∣ , ∣ g bp ∣ ) \text{relative error} =\frac{|g_{\text{num}}-g_{\text{bp}}|} {\max(1,|g_{\text{num}}|,|g_{\text{bp}}|)} relative error=max(1,∣gnum​∣,∣gbp​∣)∣gnum​−gbp​∣​

如果双精度下相对误差在 10 − 6 10^{-6}10−6 或更小量级,通常说明实现较可信;若差距明显,应检查转置、求和维度、广播和激活导数.这里的阈值只是经验值,会受到非光滑点和数值尺度影响.

有限差分需要为每个参数额外执行至少两次前向计算,复杂度很高,所以适合小模型抽样检查,不适合拿来训练网络.它像测量仪器,用于验证发动机,而不是替代引擎.


7.训练不收敛时,按这张清单排查

7.1先检查数据与目标

  • 输入尺度是否差异过大?必要时标准化或归一化.
  • 标签形状和类型是否符合损失函数要求?
  • 二分类是否混用了 Sigmoid 概率与 logits 版本损失?
  • 训练集是否真的包含可学习的输入与目标关系?

7.2再检查前向与损失

  • 初始损失是否为有限值,还是出现 NaN/inf
  • 最后一层输出维度是否与标签匹配?
  • 多分类是否错误地在 CrossEntropyLoss 前手动做了 Softmax?多数框架的交叉熵已经内部融合 LogSoftmax.

7.3然后检查梯度

  • 梯度是否全部接近 0?可能是饱和激活、错误的 detach 或冻结参数.
  • 梯度是否突然变得极大?可检查初始化、学习率,并考虑梯度裁剪.
  • 是否忘记清空梯度?PyTorch 默认会累积 .grad.
  • 是否在不该使用 no_grad() 的地方关闭了计算图?

7.4最后检查优化器超参数

  • 学习率过大:损失剧烈振荡或直接发散.
  • 学习率过小:损失缓慢下降,看起来像"完全没学".
  • 批大小改变后:梯度噪声和有效学习率可能需要重新调节.
  • 权重衰减过强:模型可能被压得无法拟合.

一个实用策略是先让模型在几十个样本上"过拟合成功".如果小数据都学不会,问题大概率在实现、数据管线或损失定义,而不是模型容量不足.

7.5五个高频问题

问题一:反向传播是否要求所有函数处处可导?

不要求严格意义上的处处可导.ReLU 在 0 点不可导,绝对值函数在 0 点也不可导,框架可以约定一个次梯度或固定返回值.真正重要的是:训练轨迹上的绝大多数位置存在可用的局部梯度,且算子实现了稳定的反向规则.离散采样、排序、硬阈值等操作更棘手,因为它们往往在大部分区域梯度为 0 或根本没有普通导数,需要连续松弛、直通估计器或策略梯度等替代方案.

问题二:损失下降是否等于模型变好?

只说明模型在当前损失定义和当前数据上表现更好.训练损失持续降低而验证损失上升,通常意味着过拟合;类别严重不平衡时,整体准确率提高也可能掩盖少数类性能下降.因此训练要同时观察验证集指标,并确认损失函数真的表达了业务目标.模型会忠实优化你给它的目标,而不是你心里没有写出来的目标.

问题三:偏置为什么也需要学习?

没有偏置时,线性层的超平面必须经过原点,激活函数的响应区间也被固定在某个位置.偏置相当于允许决策边界平移.例如一维模型 w x + b = 0 wx+b=0wx+b=0 的分界点是 x = − b / w x=-b/wx=−b/w;若强制 b = 0 b=0b=0,分界点只能在原点.偏置参数量通常不大,却能显著增加模型适应性.

问题四:为什么损失常取平均而不是求和?

如果对批内样本损失求和,批大小翻倍时梯度量级也大致翻倍,学习率含义随之变化.取平均能让梯度尺度对批大小更稳定,便于调参和比较.但在分布式训练、梯度累积和特殊加权损失中,summean 以及跨设备归约的顺序会影响有效梯度尺度,不能只看代码表面上的一个参数.

问题五:为什么深度网络不总被局部最小值困住?

高维非凸优化中确实没有一般性的全局最优保证,但大量过参数化网络存在许多性能相近的低损失区域.SGD 的噪声、动量、初始化、归一化与学习率调度共同塑造了实际轨迹.工程上更关注找到泛化良好的解,而不是证明某个权重向量是数学意义上的唯一全局最优.两个参数完全不同的网络,也可能实现几乎相同的函数.


8.从三层网络到大语言模型:规模变了,学习引擎没变

大语言模型的结构比三层网络复杂得多:输入先变成 token embedding,经过多层自注意力、前馈网络、残差连接和归一化,最后产生词表上的 logits.但训练循环仍然可以写成:

1tokens -> Transformer 前向计算 -> next-token loss
2       -> 反向传播 -> 参数梯度 -> 优化器更新
3

8.1大模型中的损失

对序列 x 1 , x 2 , … , x T x_1,x_2,\ldots,x_Tx1​,x2​,…,xT​,自回归语言模型学习:

p ( x 1 , … , x T ) = ∏ t = 1 T p ( x t ∣ x < t ) p(x_1,\ldots,x_T)=\prod_{t=1}^{T}p(x_t\mid x_{<t}) p(x1​,…,xT​)=t=1∏T​p(xt​∣x<t​)

训练时最小化 token 级交叉熵:

L = − ∑ t = 1 T log ⁡ p θ ( x t ∣ x < t ) \mathcal{L}=-\sum_{t=1}^{T}\log p_\theta(x_t\mid x_{<t}) L=−t=1∑T​logpθ​(xt​∣x<t​)

每个 token 的预测误差会通过输出投影、Transformer 层、自注意力和 embedding 反向传播到相关参数.

以自注意力为例,输入隐藏状态 X XX 经过三组投影得到:

Q = X W Q , K = X W K , V = X W V Q=XW_Q,\qquad K=XW_K,\qquad V=XW_V Q=XWQ​,K=XWK​,V=XWV​

注意力输出为:

A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K ⊤ d k ) V \mathrm{Attention}(Q,K,V) =\mathrm{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dk​ ​QK⊤​)V

反向传播时,输出梯度不仅要经过与 V VV 的矩阵乘法,还要穿过 Softmax、缩放点积,再分别回到 Q QQ、 K KK、 V VV,最后汇总到共享输入 X XX 和三组权重.这仍然遵守"路径上相乘、汇合处相加",只是计算图更大.多头注意力又把这套过程并行复制到多个子空间,最终通过输出投影合并.

这也是理解大模型训练成本的一把钥匙:前向传播产生并保存大量激活,反向传播还要遍历这些算子并计算参数梯度.训练不是简单地"把推理运行两遍",反向过程有自己的矩阵运算、缓存需求和跨设备通信.


8.2大模型训练增加了哪些工程机制

BP 的数学规则没变,但规模迫使工程系统加入更多技术:

  • AdamW 等优化器:为不同参数维护动量和二阶矩估计;
  • 学习率预热与衰减:改善训练早期稳定性并控制后期收敛;
  • 混合精度:使用 BF16/FP16 提高吞吐,必要时进行损失缩放;
  • 梯度裁剪:限制异常大的梯度范数;
  • 梯度累积:显存不足时用多个小批次模拟更大批次;
  • 激活检查点:少保存部分中间激活,在反向阶段重新计算,以时间换显存;
  • 数据、张量、流水线并行:把模型和计算分散到多张加速卡;
  • 通信重叠:让梯度同步与其他计算并行进行.

这些技术没有替代反向传播,而是在保护、压缩、分布和加速它.可以说,从 XOR 到大模型,变化的是计算图的规模与训练系统的复杂度,不变的是"损失 - 梯度 - 更新"这条主链.

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容


每日心灵鸡汤: 世界上最美的相遇,是遇见另外一个更优秀的自己!

很多人年轻时,总在向外寻找答案:寻找贵人、寻找机会、寻找救世主、寻找依靠.后来才慢慢发现,真正决定人生方向的,并不是外部给予了什么,而是自己能否完成认知的升级、能力的成长和内心的重建.于是你会发现:真正的贵人,是那个不断成长的自己;真正的朋友,是那个始终陪伴自己的自己;真正的劲敌,是自己的恐惧、懒惰、傲慢与执念;真正的成功,是不断超越过去的自己;真正的幸运,是在迷失之后重新找回自己;真正的美好,是看见更广阔、更成熟的自己.人并不是世界的中心,而是世界的一部分.一个人的成长,不是把所有人都从生命里拿掉,只剩下自己;而是在看清世界之后,既能独立站立,也能与他人连接.


【从零开始大模型开发与微调:基于PyTorch与ChatGLM】(一学就会的深度学习基础算法详解)》 是转载文章,点击查看原文


相关推荐


栈和队列的实现
纪念 2292026/6/21

༺ 个人主页 · 纪念229 ༻ 🏠我的博客主页🏠 ༒专栏目录:《数据结构》༒ ༒其它有趣的计算机知识༒ ༺世上本没有路,走的人多了自然就有了༻ 这篇文章讲述的是用顺序表实现栈、用单链表实现队列,本文主要讲述栈和队列的定义,希望对大家有所帮助! 文章目录 1.Stack.h2.Stack.c2.1 STackDesTroy(ST* ps)2.2STackPush(ST* ps, STDataType x)2.3 STackEmpty(ST* ps)2.4 S


【云计算】华为公有云构建高可用Redis集群
Harvy_没救了2026/6/13

华为公有云构建高可用Redis集群(3主3从 + AS弹性扩容)详细方案 一、方案概述 本方案基于华为云服务(ECS、VPC、AS、IMS、ELB),手动搭建一个 Redis Cluster(3主3从),并通过 弹性伸缩AS 实现自动扩容(增加节点),但不配置自动缩容。扩容时新节点自动加入集群,并通过脚本完成集群拓扑更新。整体架构满足 高可用、负载均衡、水平扩展 的需求。 核心技术栈 服务作用VPC + 安全组隔离网络,保证内网互通ECS运行Redis服务的云服务器(6台基础节点)IMS


从零实现《三角洲行动》手游自动跑刀脚本:ADB 直控 + OpenCV 视觉识别 + 固定点位搜刮)三角洲自动跑刀教程
深度学习教程,2026/6/6

从零实现《三角洲行动》手游自动跑刀脚本:ADB 直控 + OpenCV 视觉识别 + 固定点位搜刮 从零实现《三角洲行动》手游自动跑刀脚本:ADB 直控 + OpenCV 视觉识别 + 固定点位搜刮一、前言二、整体架构与技术栈三、ADB 控制层:截图、点击、滑动四、核心难点一:怎么判断"真进游戏了"?五、核心难点二:识别"这把刷在哪个出生点"5.1 用圆检测把小地图"圆形抠出来"5.2 模板匹配识别出生点 六、核心难点三:自动拾取物资七、把它串起来:固定路线八、一个诚实的工程结论九、


React Native + RNOH:跨页面数据回传的最佳实践与避坑指南
皮蛋小精灵2026/5/31

从内存闭包到官方路由通信的深度对比 技术栈: React Native 0.77 + React Navigation v7 + RNOH(React Native OpenHarmony) 适用场景: 表单录入、列表选择器、跨页面数据回传通信 一句话结论: 内存闭包 Bridge 体验虽好但存在“热重载失效、系统回收丢回调、嵌套覆盖”三大致命缺陷;官方 route.params 需配合 setParams 清洗;在嵌套/鸿蒙 JS Stack 下,使用 CommonActions.setPa


aardio从惊喜到失望到被拉黑二三事
1681692026/5/9

aardio从惊喜到失望到被拉黑二三事 前情 其实我非常喜欢开发一些小工具,用于解决工作中和生活中的问题,我个人也有开发一些小程序和一些cli,但是对于桌面端一直没的找到好的开发方案,其中有试过autohotkey,但是它的语法和学习资料非常少,带你开发GUI就更少了,平时也就用用热词热键什么的,electron我也用过,但是它太重了,一直想找一个轻量点的,我也知道c#开发桌面工具非常不错,但是那要重新学一门语言,有想过学,但是一看c#教程就犯困也就没什么尝试了,直到某一天看到了aardio,真


一觉醒来,大模型就帮我排查完页面性能问题
candyTong2026/4/29

最近遇到一个性能问题,我让大模型自己去处理,然后就去午休了,醒来之后,它还真的把问题找出来并修复了 先说结果 最终定位出来的根因是: 这个业务工作台的查询表单,把 Formily 的 form 实例塞进了带 devtools 的 Zustand store。 这件事在开发环境里会非常要命,因为 form 不是一个轻量对象,它里面带着大量字段状态、reaction、effect、schema 和联动信息。 一旦进了 store,又被 devtools 观察、快照、序列化,内存就会被瞬间放大。


打造工业级全栈文件管理器:深度解析上传、回收站与三重下载流控技术
微特尔普拉斯2026/4/20

在构建企业级 Web 应用时,文件管理器是一个看似简单实则充满挑战的模块。面对大文件上传卡顿、大文件下载导致浏览器崩溃、以及误删不可恢复等痛点,我们需要一套更科学的架构方案。 本文将通过 Vue 2/3 + Spring Boot 的组合,详细拆解如何实现一套具备:排重检测、多线程后台下载、流式下载进度监控、以及回收站机制的文件管理系统。 一、 核心技术原理分析 1. 智能冲突检测上传 在上传文件前,系统会先发起一个“预检请求”(Check Exists)。 原理:前端获取文件名


一文搞懂Harness Engineering与Meta-Harness
GreenTea2026/4/12

一、什么是Harness Engineering harness engineering目前没有官方的英文翻译,但是我认为“驾驭工程”非常合适。“驾驭”一词本身有两层含义,“释放”与“约束”这两个相辅相成的维度,打个形象的比方,跟古代君臣关系一样,既要委以重任,又要设立制衡。 我们可以将这两层含义拆解如下: 1.1 释放潜力:让 AI 像工程师一样“真刀真枪”地干活 把模型放到现场,赋予了工程现场的实权,像一个工程师一样干活,能够接触代码库、执行命令,释放模型的潜力 传统的 Copilot


大模型应用开发学习第一天
程序员雷欧2026/4/4

从今天开始,雷欧将和大家一起学习大模型应用开发。我们不搞基础,不搞虚的,只搞最重要的知识来学习。         今天,我们要学习的是Transformer架构!!当然,底层机理,包括代码实现,并不需要我们知道,那么,我们需要学会什么呢?咱接着往下看……         首先,简单介绍一下什么是Transformer,Transformer是一种基于纯注意力机制的神经网络架构,由谷歌在2017年提出,最初用于机器翻译任务,现在已成为NLP和CV领域的基础架构。 1.Transformer整


腾讯云WorkBuddy实战, 全场景智能体工作搭子,这只龙虾真能帮你干活吗
不惑_2026/3/26

全网都在养虾。 朋友圈被刷屏了。同事也在搞。连高盛的分析师都惊了,说中国人接受AI的速度令人震惊。 但说实话,在我真正装上WorkBuddy之前,我是持怀疑态度的。 之前OpenClaw火的时候,很多人的真实体验是,折腾三小时,报错二十次,连命令行都没跑起来。一个面向普通人的AI工具,如果连安装都搞不定,那跟没有有什么区别? 所以当腾讯说WorkBuddy零部署、下载就能用的时候, 我第一反应是,真的假的。 ▲ WorkBuddy桌面端主界面,打开就是一个对话框,简洁到有点不像腾讯的风格 装上

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读