神经网络基础与Tensorflow实战

更新时间: 2026-07-01 09:38:32

# 神经网络基础

# 深度学习框架

深度学习框架:Tensorflow, Keras, PyTorch, Paddle

  1. 核心框架对比:TensorFlow vs PyTorch

TensorFlow(谷歌开发):像一辆“重型工业卡车”

  • 特点:它功能极其强大,生态系统非常完善,尤其是在把模型真正用到实际生产环境(比如手机APP、服务器后台、自动驾驶系统)时,它提供了很多现成的工具(如TF Serving, Lite)。

  • 开发风格:早期的1.0版本非常复杂,需要先画好图纸(静态图)才能运行。但到了2.0版本,它吸取了PyTorch的优点,支持了“动态图(Eager模式)”,让写代码和调试变得简单了很多。

  • 适用场景:非常适合工业界、大规模部署和需要极致性能优化的项目。

PyTorch(Facebook/Meta开发):像一辆“灵活的实验跑车”

  • 特点:它的代码写起来非常像标准的Python,简洁直观,调试起来极其方便。因为它采用“原生动态图”设计,你可以一边写代码一边看结果,非常适合反复试错和修改。

  • 学术地位:因为它灵活好上手,目前学术界(大学、实验室、发论文)几乎都在用它。很多最新的AI算法(比如Hugging Face上的大模型)都是优先用PyTorch写的。

  • 适用场景:非常适合快速实验、学术研究以及新算法的原型开发。

  1. 另外两个框架是什么?

Keras:新手友好的“自动驾驶辅助系统”

  • Keras其实不是一个完全独立的底层框架,它是建立在TensorFlow之上的一个高级封装(API)。

  • 它把复杂的底层操作都简化了,就像搭积木一样,几行代码就能搭建一个神经网络。对于初学者或者想要快速验证想法的人来说,Keras是最容易上手的工具。

Paddle(飞桨,百度开发):国产的“全能商务车”

  • 这是中国首个自主研发的深度学习平台。它集成了训练、推理、部署的全套工具,并且对中文NLP(自然语言处理)和国内的硬件适配做得非常好。在国内的工业界应用和中文AI开发中,是一个非常值得推荐的选择。

动态图 vs 静态图

  • 静态图(早期TF):像“拍电影”。你必须先写好完整的剧本(定义好整个计算图),检查无误后才能开机拍摄(运行)。优点是运行效率高,缺点是改剧本很麻烦,调试困难。
  • 动态图(PyTorch / TF 2.0):像“现场直播”。你一边说台词(写代码),摄像机一边拍(即时执行)。你可以随时停下来调整,非常适合调试和灵活修改网络结构。

科学计算库 / 代替Numpy

Numpy是Python里处理数字表格的常用工具,但它只能在CPU上跑。PyTorch和TensorFlow里的“张量(Tensor)”功能和Numpy几乎一样,但它们可以利用GPU(显卡)进行加速,处理海量数据时速度能快几十倍甚至上百倍。

# 总结建议

如果你是刚入门或者做学术研究、发论文,推荐从 PyTorch 入手;如果你未来打算在企业里做AI产品的落地部署,或者处理超大规模数据,TensorFlow 是必须掌握的技能;而 Keras 可以作为你入门TensorFlow的最佳捷径。

# 什么是深度学习

图中展示了一个最基础的人工神经网络(Artificial Neural Network, ANN)模型。你可以把它想象成一个多层的信息处理工厂:

  • 输入层: 这是数据的入口。比如你要识别一张猫的照片,输入层就是照片上的每一个像素点。

  • 隐藏层: 这是核心加工车间。数据在这里经过复杂的数学运算(加权、求和、激活函数)。传统的神经网络可能只有1-2个隐藏层。

  • 输出层: 这是最终结果。比如输出“是猫”的概率是90%,“是狗”的概率是10%。

这是一个典型的深层卷积神经网络(CNN)结构图。你看它像千层饼一样,一层叠一层。

传统的神经网络可能只有3-5层。
现在的深度学习网络动辄几十层、上百层。

  • 为什么要这么深? 每一层都在提取更高级的特征。

    • 第1层可能只看到了线条和边缘;
    • 第5层看到了眼睛、鼻子等器官;
    • 第20层看到了整张脸的结构。
  • 层数越深,理解能力越强。

    • GoogleNet (22层): 在2014年 ImageNet 比赛中夺冠的网络,证明了增加深度可以提高准确率。
    • ResNet (50, 101, 152层): 这是一个里程碑式的网络。在它之前,网络太深会导致训练不动(梯度消失问题)。ResNet 发明了“残差连接”(相当于修了高速公路捷径),让网络可以深达152层甚至更多,而且还能训练得很好。

深度学习和老式神经网络的联系

  • “神经网络不是一个新的概念”: 其实早在上世纪50年代,科学家就提出了神经元的数学模型。深度学习只是在这个老地基上盖了摩天大楼。

  • “端到端的黑盒”:

    • 端到端: 你只需要给它原始数据(比如一堆乱糟糟的语音录音)和标签(这是“你好”),它自己就能学会怎么识别,不需要人工去设计中间步骤(比如不需要人工先提取音调特征)。
    • 黑盒: 虽然它很厉害,但中间那几层到底是怎么思考的,人类很难完全看懂,就像把东西扔进黑盒子,出来就是答案,过程不透明。
  • “可解释性较差,自适应完成特征提取”: 这是“黑盒”的代价。以前的算法是人告诉电脑“猫有尖耳朵”,电脑去找尖耳朵;深度学习是电脑自己看了一万张猫照后,自己总结出了“尖耳朵”这个特征,但它不会写报告告诉你它是怎么总结出来的。

本质没变: 深度学习依然是基于神经元连接的数学模型,依然是“黑盒”操作。

量变引起质变: 最大的区别在于“深”。通过堆叠大量的隐藏层,机器能够自动从简单的像素点中,一步步抽象出极其复杂的高级概念(比如从线条到人脸,再到表情)。这就是深度学习之所以强大的原因。

# 人工神经元

人类是如何向大自然“抄作业”,把大脑的工作原理变成了电脑里的代码的。

# 第一步:大自然的灵感——生物神经元

这是我们要模仿的对象——你大脑里的神经细胞。

大家可以把它想象成一个“微型情报站”。

  • 树突(Dendrite):就像情报站的“天线”,负责接收四面八方传来的信号。
  • 细胞体(Cell body):这是“指挥部”。它会把所有天线收到的信号汇总起来。
  • 轴突(Axon):这是一根长长的“电缆”,负责把处理好的结果传给下一个情报站。

提问: 这个情报站是怎么决定要不要把消息传出去的呢?

这就涉及到了图里提到的核心机制——“兴奋”与“抑制”。

简单来说,就是一个“投票表决”的过程:

  1. 很多个上游神经元给这个神经元发信号:“我觉得这事儿很重要!”或者“我觉得这事儿不重要!”
  2. 这些信号有强有弱(这就是权重的概念)。
  3. 细胞体把所有信号加起来。如果总分超过了一个“及格线”(也就是图里说的“阈值”),这个神经元就会瞬间“兴奋”,顺着轴突发出一道电脉冲,告诉下游:“收到!我也觉得重要,传下去!”
  4. 如果没达到及格线,它就保持沉默(抑制),什么都不做。

总结一句话:生物神经元本质上就是一个带门槛的开关。

# 第二步:数学的翻译——激活函数

好了,原理懂了。但是计算机听不懂什么叫“兴奋”,它只认识数字。那怎么把这个生物过程变成数学公式呢?

这就引出了核心概念:激活函数。

我们可以把激活函数看作是那个“把关的裁判”。

  • 图里展示了一个最简单的裁判,叫阶跃函数(Step Function)。

  • 它的规则特别死板(看图里的公式 𝑓(𝑥)):

    • 只要输入的信号总和 𝑥 小于0,它就输出 0(代表“抑制”,不传递)。
    • 只要输入的信号总和 𝑥大于等于0,它就输出 1(代表“兴奋”,传递)。

为什么要加这个函数?

如果没有这个函数,神经网络就只是一堆简单的加减乘除(线性运算)。不管你怎么叠加,它永远只能画直线,学不会复杂的曲线逻辑(比如识别猫和狗)。
激活函数引入了“非线性”,让网络有了“弯曲”的能力,这才是智能产生的根源。

# 第三步:工程的实现——人工神经元

最后,我们来看第三张图。这就是我们在写代码、搭模型时真正用到的东西——人工神经元。

大家看这个流程图,它完美地把前两步结合起来了:

  1. 输入层 (x1,x2...):这就是数据。比如图片的像素点。

  2. 权重 ( w1,w2...):注意看连线上的 w。这代表了每个输入的重要性。有的输入很重要,权重就大;有的不重要,权重就小。

  3. 求和 ( ∑ ):中间那个圆圈是个加法器。它做的事情就是:输入 × 权重 + 偏置( b )。

    • 注:偏置 b 就像是那个“及格线”的调节旋钮,用来调整触发的难易程度。
    • 这一步对应了生物神经元在细胞体里的“汇总”过程。
  4. 激活函数 (𝑓):求和后的结果𝑧 ,被送进了f 函数里。

    • 这一步对应了生物神经元的“开关机制”。
  5. 输出 ( a ):最终的结果。

前面的加权求和是线性的。
后面的激活函数是非线性的。
当千百万个这样简单的人工神经元连接在一起,层层叠叠(就像我们之前看的深度学习图),它们就能模拟出极其复杂的大脑功能,比如看懂这张图,甚至写出这首诗。

# 神经网络结构

# 第一步:宏观架构——三层楼的设计

这是一个标准的神经网络,就像是一个三层的加工厂。

  1. 输入层 看图: 最左边那一列圆圈,标着 x1,x2。

这是工厂的“进货口”。

比如你要识别一张猫的照片,这里的 x1 可能代表照片左上角像素的颜色,x2 代表它旁边像素的颜色。

注意: 这一层通常不进行复杂的计算,它只负责把数据接进来,传给下一层。图中有2个圆圈,说明这次进货有2个特征数据。

  1. 隐藏层
    看图: 中间那两列圆圈。

这是工厂的“核心车间”。

为什么叫“隐藏”?因为我们在外面看不到它们,它们夹在输入和输出中间。

“隐藏层的层数=?”

在这张图里,我们可以看到中间有两列圆圈,所以这里有2个隐藏层。

  • 第1层(隐藏层1): 有3个神经元。
  • 第2层(隐藏层2): 有2个神经元。

作用: 每一层都在对数据进行更深一层的加工。第1层可能在看“线条”,第2层可能就把线条组合成了“眼睛”或“耳朵”。层数越多,它能学到的特征就越复杂(这就是为什么叫“深度”学习)。

  1. 输出层
    最右边那一列,标着y1,y2。

这是工厂的“出货口”。

经过中间层层加工后,最终的结果从这里出来。

比如y1输出 0.9,y2输出 0.1,那就告诉老板:“我有90%的把握这是猫,10%的把握这是狗。”

# 第二步:微观细节——信号是怎么传递的?

  1. 权重的符号Wij(l)

这个符号看着吓人,其实它是三个部分的拼盘:

  • W (Weight): 代表权重。就是连接两个神经元那根线的“粗细”。线越粗,说明这个信号越重要。

  • 上标(l): 代表层数。

    • 图中的(1)表示这是第1层的权重(也就是从输入层连到第1个隐藏层的那些线)。
  • 下标ij: 这是最容易晕的地方,大家记住口诀:“前j后i”(或者“源j目标i”)。

    • j (来源): 代表上一层(发送方)的第几个神经元。
    • i (去向): 代表下一层(接收方)的第几个神经元。

请看那条加黑的粗线,标着 W12(1)

  • (1) :说明这是第1层的连接线。

  • 2 (后一位):代表来自上一层(输入层)的第 2 个神经元(也就是 x2)。

  • 1 (前一位):代表传给下一层(隐藏层)的第 1 个神经元(也就是 a1(1) ) ​ 翻译成人话: W12(1) 的意思就是——“从输入层的第2个点,连到第1层隐藏层的第1个点的那根线的权重。”

  1. 信号的汇聚与激活
    图中箭头都指向了a1(1)这个圆圈。
  • 这意味着,a1(1)这个神经元很忙碌,它同时收到了来自 𝑥1 𝑥2 还有一个常数 1(偏置项 𝑏)的信号。

  • 它会把这些信号乘以各自的权重( 𝑊11,𝑊12,𝑏1 ),加起来,然后塞进激活函数 ℎ() 里处理一下,变成 z1(1) ,再传给更后面的一层。

  • 神经网络是由输入层、一堆隐藏层、输出层像三明治一样叠起来的。隐藏层越多,网络越深,脑子越“聪明”。

  • 层与层之间是通过权重 𝑊连接的。每一个 𝑊 都有明确的编号,规定了谁传给谁。

# 使用numpy模拟前向传播

模拟前向传播是理解神经网络如何“思考”的第一步,它让你亲眼看到数据如何从输入层一步步流经隐藏层,最终在输出层产生预测结果。这个过程就像拆解一台精密仪器,让你看清每个齿轮(权重、偏置、激活函数)是如何协同工作的。

# 第一步:理解“前向传播”是什么?

你可以把神经网络想象成一个多层流水线工厂:

  • 原材料(输入数据)从第一层进入;
  • 经过每一层的加工(加权求和 + 激活函数);
  • 最终产出成品(预测结果)。

这个“从输入到输出”的单向计算过程,就叫前向传播。它不涉及学习或调整参数,只是“用现有的权重和偏置,算出当前网络的输出”。

# 第二步:初始化网络 —— 给工厂配好“设备参数”

def init_network():
    network = dict()
    network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
    network['b3'] = np.array([0.1, 0.2])
    return network  
1
2
3
4
5
6
7
8
9

这相当于给工厂的每一层“配置了机器参数”:

  • W1, W2, W3:是各层的权重矩阵,代表神经元之间的连接强度。比如 W1 是第0层(输入层)到第1层(隐藏层1)的连接权重。
  • b1, b2, b3:是各层的偏置向量,相当于每个神经元的“启动门槛”,即使输入为0,也能让神经元有基础活性。

关键点:

这些权重和偏置在训练前是随机初始化的(这里为了演示用了固定值),训练的目标就是通过反向传播不断调整它们,让输出更接近真实答案。

# 第三步:定义激活函数 —— 给每层加“决策开关”

# sigmoid激活函数
def sigmoid(x):
return 1 / (1 + np.exp(-x))
#恒等函数,作为输出层的激活函数
def identity_function(x):
return x
1
2
3
4
5
6
  • Sigmoid函数:用于隐藏层。它是一个“平滑开关”,输入任意实数,输出都在0~1之间。作用是引入非线性,让网络能学习复杂模式。如果不用它,多层网络就退化成单层线性模型。
  • 恒等函数:用于输出层。意思是“原样输出”,不做任何压缩或变换。常用于回归任务(比如预测房价、温度等连续值)。如果是分类任务,通常会换成Softmax函数。

为什么隐藏层用Sigmoid,输出层用恒等?

因为隐藏层需要“挤压”信号,制造非线性;而输出层如果是回归任务,我们希望直接看到原始数值,不需要压缩。

# 第四步:前向传播 —— 让数据流过整个网络

#前向传播
def forward(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    a1 = np.dot(x, W1) + b1 # 第1层:加权求和
    z1 = sigmoid(a1)   # 第1层:激活  
    a2 = np.dot(z1, W2) + b2  # 第2层:加权求和
    z2 = sigmoid(a2)  # 第2层:激活
    a3 = np.dot(z2, W3) + b3  # 第3层:加权求和
    y = identity_function(a3)  # 输出层:恒等激活
    return y

#初始化网络
network = init_network()
#设置输入值
x = np.array([1.0, 0.5])
#前向传播
y = forward(network, x)
print(y)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

我们逐层拆解:
第1层(输入 → 隐藏层1)

  • 输入 x = [1.0, 0.5](两个特征)
  • 与 W1(2×3矩阵)做点乘,加上 b1(3维向量),得到 a1(3维向量)——这是线性组合。
  • 再通过 sigmoid(a1) 得到 z1 ——这是非线性激活,让每个神经元决定是否“兴奋”。

第2层(隐藏层1 → 隐藏层2)

  • 用 z1 作为输入,与 W2(3×2矩阵)点乘,加 b2,得 a2。
  • 再经 sigmoid(a2) 得 z2。

第3层(隐藏层2 → 输出层)

  • 用 z2 作为输入,与 W3(2×2矩阵)点乘,加 b3,得 a3。
  • 最后用 identity_function(a3) 直接输出 y。

# 第五步:运行结果 —— 看工厂产出什么?

x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)  # 输出: [0.31682708 0.69627909] 
1
2
3

这意味着:当输入 [1.0, 0.5] 时,经过三层网络的计算,最终输出两个值:约 0.32 和 0.70。

这个输出代表什么?

取决于你的任务:

  • 如果是二分类,可能表示属于类别A的概率是32%,类别B是70%。
  • 如果是回归,可能就是两个连续变量的预测值。

# 激活函数

为什么神经网络必须是非线性的?

如果不理解这一点,你就无法理解为什么我们需要“深度”学习。

核心问题:如果没有激活函数会怎样?

如果不用激活函数,就相当于 𝑓(𝑥)=𝑥

让我们做一个思想实验。假设你的神经网络有100层,但是每一层都没有激活函数(或者说激活函数就是恒等映射,输入啥输出啥)。

哪怕你堆叠了100层、1000层,只要中间没有激活函数,整个网络在数学上就等价于单层感知机(也就是一个简单的线性回归模型)。这就好比你把一张纸折叠了一万次,它本质上还是一张纸,并没有变成一个立体的雕塑。

解决方案:引入非线性(图片的第二点)
为了让神经网络变得强大,我们必须打破这种“线性叠加”的魔咒。这就需要激活函数。

什么是“非线性”?

线性函数就像是一条直尺,只能画直线。
而非线性函数(如 Sigmoid, ReLU, Tanh)就像是弯曲的软管,它可以拐弯、可以扭曲。

为什么要“逼近任意函数”? 现实世界的数据是非常复杂的,几乎都不是线性的。

例子: 判断一张图是不是猫。

  • 像素值(输入)和“是不是猫”(输出)之间绝对不是简单的倍数关系(线性关系)。
  • 它们之间存在着极度复杂的曲线关系。

当我们引入了非线性的激活函数(比如图片右侧圆圈里的 𝑓(𝑧)),每一层就不再只是简单地放大或缩小信号,而是对信号进行了扭曲和变换。

  • 第1层: 把数据稍微扭一下。
  • 第2层: 在扭过的数据基础上,再扭一下。
  • 第N层: 经过多次扭曲,原本杂乱无章的数据就被折叠、拉伸成了可以被分类的形状。

这就是著名的通用近似定理:只要有足够的神经元和非线性激活函数,神经网络理论上可以模拟任何复杂的函数(无论是识别图像、翻译语言还是预测股票)。

这是标准的M-P神经元模型:

  • 输入端 ( 𝑥1,𝑥2,𝑥3 ): 接收上一层传来的信号。
  • 加权求和 ( Σ ): 这是一个线性操作。每个输入乘以权重 𝑤 ,加上偏置 𝑏 。此时结果 𝑧z 依然是输入的线性组合。
  • 关键一步 ( 𝑓(𝑧)f(z) ): 激活函数登场!
    • 它接收线性的结果𝑧
    • 输出一个非线性的结果 ℎ。
    • 正是这一步,赋予了网络“灵魂”,让它具备了处理复杂逻辑的能力。

线性模型的叠加依然是线性模型(没意义)。 只有加入非线性激活函数,多层神经网络才拥有了“深度”的意义,才能去拟合这个复杂的世界。

# 激活函数(sigmoid)

Sigmoid函数的数学表达式为:

它的核心特性是:

  • 输入:任意实数( −∞到 +∞ )
  • 输出:严格限制在 (0, 1) 区间内
  • 形状:呈“S”形曲线,中间陡峭,两端平缓

为什么用它?

  • 概率解释:输出值可以被理解为“属于某一类的概率”,非常适合二分类任务。
  • 平滑可导:函数处处连续且可导,便于后续用梯度下降法优化。
  • 压缩范围:将任意大小的输入“压缩”到0~1之间,避免数值爆炸。
def sigmoid(x):
    s = 1 / (1 + np.exp(-x))
    return s

def plot_sigmoid():
    #设置参数x(起点,终点,间距)
    x = np.arange(-8, 8, 0.2)
    y = sigmoid(x)
    plt.plot(x, y)
    plt.show()
1
2
3
4
5
6
7
8
9
10

# 第二步:Sigmoid函数的导数

在神经网络训练中,我们需要知道“如何调整权重才能让输出更接近目标”,这就依赖于导数(梯度)。

Sigmoid函数的导数有一个非常优雅的自表达形式:

𝑓′(𝑥)=𝑓(𝑥)⋅(1−𝑓(𝑥))

这个公式意味着:你只需要知道函数当前的输出值 𝑓(𝑥) ,就能直接算出它的导数,无需重新计算指数项。这在编程中极大提升了效率。

def derivative_sigmoid(x):
    s = 1 / (1 + np.exp(-x))
    ds = s*(1-s)
    return ds

#绘制sigmoid函数的导数
def plot_ds():
    #设置参数x(起点,终点,间距)
    x = np.arange(-8, 8, 0.2)
    y = derivative_sigmoid(x)
    plt.plot(x, y)
    plt.show()
1
2
3
4
5
6
7
8
9
10
11
12

# 第三步:Sigmoid的致命缺陷——梯度消失

从第二张图的导数公式 𝑓′(𝑥)=𝑓(𝑥)⋅(1−𝑓(𝑥)) 可知:

  • 当𝑥=0 时, 𝑓(𝑥)=0.5 ,导数最大值为 0.5×0.5=0.25
  • 当 𝑥 很大或很小时(比如 𝑥>4 或 𝑥<−4 ), 𝑓(𝑥) 趋近于1或0,导数趋近于0

这意味着:在反向传播时,如果某一层的输入落在Sigmoid的“平坦区”,它传回的梯度就会非常小。

梯度消失的后果:
假设一个网络有10层,每层的梯度都乘以0.25(最理想情况),那么传到第1层的梯度就是原始梯度的 0.2510≈0.000 ——几乎为零!

结果就是:前面的层几乎学不到任何东西,网络退化成只有最后几层在工作。这就是著名的“梯度消失”问题。

# 梯度消失

深度学习中一个非常经典且致命的问题——梯度消失。简单来说,就是网络在训练时,越靠近输入层的神经元“学不动”了。

核心概念:什么是梯度消失

在神经网络训练中,我们需要通过“反向传播”算法把误差从输出层一层层往回传,以此来调整每一层的参数(权重)。这个传递的“误差信号”就是梯度。
梯度消失指的就是:当这个误差信号经过多层传递回到前面几层时,变得微乎其微(趋近于0),导致前面的层几乎无法更新参数,就像网络的前半部分“死掉”了一样。

# Sigmoid函数的“原罪”

  • 导数太小: Sigmoid函数的导数最大值只有0.25(当输入为0时)。
  • 连乘效应: 根据链式法则,反向传播时梯度是层层相乘的。如果每一层的导数都小于1(比如0.25),那么传个几层之后:0.25 × 0.25 × 0.25... 结果会迅速变成无限接近于0的极小值。
  • 若初始化的神经网络权重|w|小于1”,再加上导数也小于1,这就导致了“小于1的值不断相乘”,最终导致梯度消失。

# 深层网络的“灾难”

  • 层级越深,消失越快: 图中文字解释了,当层数增多时,这种衰减会被放大。

  • 后果: 靠近输出层的隐藏层(如Hidden Layer 3)还能收到正常的梯度,能正常学习;但靠近输入层的隐藏层(如Hidden Layer 1),收到的梯度几乎为0,参数停止更新。这就导致整个网络实际上只相当于后面那几层浅层网络在工作,前面的层白搭了。

# 通俗类比

想象你在传话(反向传播梯度):

  • 第1个人告诉第2个人:“误差是100”。
  • 第2个人传给第3个人时打了个折,变成了“25”(乘以0.25)。
  • 第3个人传给第4个人,变成了“6.25”。
    ...
  • 传到第10个人时,声音已经小到听不见了(梯度消失)。
  • 结果就是,排在最前面的人(输入层附近的神经元)根本听不到指令,不知道该干什么。

# 激活函数

神经网络里的神经元是如何做决定的?为什么有的决定做得好,有的做得不好?

我们把神经网络想象成一个“层层汇报的公司”。数据从底层员工(输入层)往上汇报给经理(隐藏层),最后由 CEO(输出层)拍板。而激活函数,就是每个经理在汇报前必须经历的“审批过滤器”。

# Sigmoid 函数:老派的“概率”过滤器

它是一个平滑的“S”形曲线。不管输入多大或多小,输出永远被压缩在 0 到 1 之间。

它特别适合做“二选一”的决定。比如判断一张图是猫还是狗。输出 0.9 就代表“90% 可能是猫”。

它的致命缺点(重点):


梯度消失: 你看图里那个像小山丘一样的导数图。当输入很大或很小时,曲线变得非常平缓(斜率接近 0)。这意味着,如果这一层的经理觉得“这事儿太确定了”或者“太不确定了”,他在往回传递反馈(梯度)时,信号就会变得极弱。经过好几层传递,最底层的员工根本听不到上面的反馈,导致前面的层学不到东西。 不是以 0 为中心: 它的输出全是正数(0 到 1)。这会导致下一层的输入总是正的,参数更新时会走“之字形”路线,效率很低,收敛慢。

# Tanh 函数:Sigmoid 的“改进版”


它也是 S 形,但范围变成了 -1 到 1。

它好在哪里?

  • 以 0 为中心: 它的输出有正有负,均值是 0。这就像经理汇报时,既有表扬也有批评,数据分布更均衡,下一层处理起来更高效,收敛速度比 Sigmoid 快。
  • 依然有老毛病: 注意看,它的两头依然是平缓的。所以,梯度消失的问题依然存在,只是比 Sigmoid 稍微好了一点点。

# ReLU 函数:简单粗暴的“现代之王”


小于 0 的部分直接砍掉变成 0,大于 0 的部分原样保留(是一条直线)。公式就是 f(x)=max⁡(0,x)。

为什么它现在是主流?

  • 解决梯度消失: 你看它的右边,是一条斜率为 1 的直线。这意味着,只要输入是正数,梯度传回去就是 1,完全不衰减!这让深层网络终于能训练动了。
  • 计算超快: 不需要像 Sigmoid 那样算复杂的指数函数,只需要判断一下是不是大于 0,计算机最喜欢这种简单的操作。

它的小缺点: “神经元死亡”。如果某个神经元的输入一直是负数,它就永远输出 0,梯度也是 0,这就相当于这个神经元“死”了,再也不更新了。

提示

ReLU 的公式虽然简单,但在早期实践中却面临几个致命问题,导致它长期被忽视:

“死亡神经元”问题:当输入为负数时,ReLU 的输出恒为 0,梯度也为 0。这意味着一旦某个神经元在训练初期被“关死”(输入始终为负),它就永远不会再被激活,相当于从网络中“消失”了。在早期缺乏有效初始化方法和正则化手段的情况下,这个问题非常严重。
缺乏理论支撑:ReLU 不是一个光滑函数(在 x=0 处不可导),这与当时主流的数学分析框架格格不入。研究者们担心这种“不连续”会导致训练不稳定或理论分析困难。
硬件与算力的限制:在 GPU 尚未普及的年代,神经网络的训练速度极慢。Sigmoid 虽然计算复杂,但其“饱和”特性(两端平缓)在某些情况下反而能起到“正则化”作用,防止模型过拟合。而 ReLU 的线性特性在浅层网络中优势不明显,反而可能因为“死亡神经元”导致训练失败。
ReLU 真正成为主流,是在深度学习进入“深水区”之后。当网络层数从几层增加到几十层甚至上百层时,Sigmoid 和 Tanh 的梯度消失问题变得无法忍受——误差信号传不到底层,网络根本学不动。而 ReLU 在正区间的梯度恒为 1,完美解决了这个问题,让深层网络的训练成为可能。同时,GPU 的普及让大规模并行计算成为现实,ReLU 的计算效率优势被放大;加上 Xavier/He 初始化、BatchNorm 等技术的出现,有效缓解了“死亡神经元”问题,ReLU 才终于迎来了它的黄金时代。

# 总结

  • Sigmoid / Tanh: 适合处理概率问题,或者在网络的输出层(比如你要输出一个 0-1 的概率)。但在深层网络的中间层,尽量别用,因为会让网络“学不动”。
  • ReLU: 现在的默认首选。只要你不知道用什么,就用 ReLU。它让深度学习真正火了起来。
  • Leaky ReLU / ELU: 这些是 ReLU 的“补丁版”。为了解决 ReLU “神经元死亡”的问题,它们在负数区留了一点点斜率(比如 0.01x),让神经元即使输了也能“喘口气”,不至于彻底死掉。

# 从0编写一个神经网络

  1. 定义网络结构
    人话翻译:决定大脑长什么样。是有1层脑细胞,还是100层?每个脑细胞有多少个触手?
    指定输入层、隐藏层、输出层的大小。这就好比规定试卷有几道题,草稿纸有多大,最后要填几个空。

  2. 初始化模型参数
    人话翻译:给大脑通电,但刚开始是“乱通电”。
    也就是生成权重(Weights)。刚开始网络什么都不懂,我们会给它一些随机的数值,让它开始瞎猜。

  3. 循环操作(这是核心!)
    这一步是神经网络“学习”的本质,它会重复成千上万次。

  • 3.1 执行前向传播:“做题”。数据从输入层流向输出层,网络给出一个预测结果(比如:“我觉得这只猫是狗”)。
  • 3.2 计算损失函数:“对答案”。拿网络的预测结果和真实答案对比,算出错了多少(比如:“错得离谱,偏差值很大”)。
  • 3.3 执行后向传播:“找原因”。这是最数学的一步。根据错误的程度,倒推回去,看看到底是哪根神经(参数)导致了错误。
  • 3.4 权值更新:“改正错题”。根据找到的原因,微调参数。下次再遇到类似的题,就不会犯同样的错了。

我们的原材料是什么? 我们可以把这个数据结构想象成一个巨大的 Excel 表格:

  1. 样本大小为 64 (Batch Size)
  • 含义:我们不是拿着一张卷子去训练,而是一次性拿 64张卷子(64个样本)一起喂给网络。
  • 为什么? 一次教一个太慢,一次教一万个电脑内存不够。64个是一个折中的“批次”。
  1. 输入层维度为 1000 (Input Dimension)
  • 含义:每一张卷子上有 1000道填空题(特征)。
  • 例子:如果你要识别图片,这1000可能代表图片的像素点;如果你要预测房价,这可能代表面积、位置、房龄等1000个指标。
  1. 输出层维度为 10 (Output Dimension)
  • 含义:网络最后要吐出 10个结果。
  • 例子:这通常代表分类任务。比如识别手写数字(0-9),刚好就是10类。网络会输出10个概率,告诉你它是0的可能性多大,是1的可能性多大...
  1. 隐藏层维度为 100 (Hidden Layer Dimension)
  • 含义:这是网络内部的“黑盒”大小。
  • 作用:输入是1000维,输出是10维,中间怎么转换?我们需要一个中间的“加工车间”。这个车间有 100个工人(神经元)在处理信息。这个数字是我们人为设定的超参数。
  1. 数据通过 numpy.random 随机生成
  • 含义:注意!这里说了数据是随机的。
  • 老师的提醒:这意味着我们在做一个“假实验”。因为数据是随机的,其实输入和输出之间没有任何逻辑关系(比如输入天气,输出股票)。
  • 目的:我们现在的目的不是为了“预测未来”,而是为了“调试代码”。我们要验证的是:“我的代码能不能跑通?我的矩阵乘法维度对不对?” 只要Loss(误差)能降下来,说明代码逻辑就是对的。

其实是在告诉你接下来的代码该怎么写:

嘿,我要写一个程序。你要帮我创建一个矩阵 X,它的形状必须是 (64, 1000);还要创建一个矩阵 Y,形状是 (64, 10)。然后你要造两个权重矩阵 W1 和 W2,把它们连起来,形成一个 1000 -> 100 -> 10 的结构。最后,我们要不断地让它们做乘法、算误差、改错,直到它们能把这些随机数背下来为止。

# 计算损失函数

损失函数(Loss Function),它是神经网络训练过程中用来衡量“预测值”与“真实值”之间差距的核心工具。你可以把它想象成考试后的“扣分系统”——错得越多,扣得越狠,网络就越知道该往哪个方向调整自己。

三种最常用的损失函数,它们分别适用于不同场景:

  1. 二分类交叉熵损失函数

这是专门用于二分类任务(比如判断一张图是猫还是狗)的损失函数。它的公式是:

  • 含义:如果真实标签是1(比如“是猫”),而模型预测概率y^接近0,那么 logy^ 会是一个很大的负数,前面加个负号后,损失值就会变得非常大——意味着“错得很离谱”。反之,如果预测接近1,损失就很小。

  • 为什么用它:它对“置信度错误”惩罚很重,能迫使模型输出更准确的概率值,而不是模棱两可的0.5。

  1. 平均绝对误差
    这是用于回归任务(比如预测房价、温度)的损失函数。它的公式是:
  • 含义:就是把每个样本的预测值和真实值的“绝对差”加起来,再除以样本总数。它不关心误差是正还是负,只看“差了多少”。

  • 优点:对异常值不敏感。比如你预测房价,有一个房子实际是100万,你预测成1000万,MAE只会记录900万的误差,不会像MSE那样被平方放大成8100亿。

  1. 均方误差
    这也是用于回归任务的损失函数,但比MAE更“严厉”。它的公式是:
  • 含义:把每个样本的误差“平方”后再求平均。平方操作会让大误差被放大很多倍。

  • 为什么用平方:一是数学上更容易求导(梯度下降需要),二是它能“惩罚”那些预测特别离谱的样本,让模型更关注“极端错误”。

  • 注意:公式里有个 1/2,这是为了方便求导时消掉平方带来的系数2,不影响最终优化方向。

这三种损失函数的选择,取决于你的任务是“分类”还是“回归”,以及你对“错误”的容忍度。如果你在做图像分类,交叉熵是首选;如果你在预测连续数值,MAE和MSE是基础选项,通常MSE更常用,因为它对大误差更敏感,能加速模型收敛。

# 反向传播

# 买车的例子

  • 场景:你要买车。

    • 单价 50万。
    • 购置税 10%(也就是乘以 1.1)。
    • 买 2 辆。
    • 最终价格 = 50×1.1×2=110万。
  • 核心问题:如果车价涨了 1 万(变成 51 万),你的总花费会涨多少?

    • 很简单,因为你买了 2 辆,还要交税,所以总花费会增加 1×1.1×2=2.2万。
  • 图中的箭头含义:

    • 蓝色箭头向右(前向传播):算总价。
    • 橙色箭头向左(反向传播):这就是重点!它在计算“敏感度”(也就是数学里的导数/梯度)。
    • 从右向左一次求导,①110/110=1,②110/100=1.1,③100/50=2,这些其实就是在算每一步的放大倍数。

结论:① * ② * ③ = 2.2 就是我们要找的梯度。它告诉我们:输入端(车价)变动一点点,输出端(总价)会变动 2.2 倍。

反向传播就是从结果往回推,看看每个环节对最终结果有多大的“责任”或“影响力”。

反向传播就是通过链式法则,从最终结果开始,一层层地向后计算每个节点、每个参数对最终结果的“影响力”(即梯度)。在真实的神经网络中,计算出这个“影响力”后,就可以根据它来微调参数=>这就是“学习”的过程。

# 误差是怎么“分赃”的?

从买车例子跨越到神经网络结构。

  • 场景:
    • 左边是输入层(a, b)。
    • 中间是隐藏层(c, d)。
    • 右边是输出层(e, f)。
    • 现在的任务是:我们发现最终结果(e, f)算错了,有了误差 eo1和eo2​。我们要把这个误差传回去,告诉中间的节点 c 和 d:“你们也有责任,你们得改!”

Thinking: 节点 c 的误差和输出层的哪个节点有关?
答案是:都有关!
节点 c 既连着 e,也连着 f。所以,c 的误差 eh1​ 是由 e 的误差和 f 的误差共同组成的。

  • 公式翻译: eh1=(c对e的贡献比例)×eo1+(c对f的贡献比例)×eo2

  • 图里的公式虽然写得很长(带平方什么的),但核心逻辑就是加权求和。

  • 谁连的线(权重 w )粗,谁分到的误差就多。

输出层的错误,要顺着网线(权重),按比例“甩锅”给上一层的每一个节点。

# 矩阵魔法(让计算机算得更快)

反向传播(backpropagation):使用矩阵相乘的形式,求解隐藏层的误差

忽略掉分母部分,重新改成矩阵形式为

为什么可以直接“约分”掉分母?
数学上,A/B​ 怎么可能直接变成 A 呢?除非 B=1 。

但在神经网络的工程实现中,我们之所以能写成下面那个简单的矩阵形式,是因为以下原因:

  • 我们只关心“方向”,不关心“绝对大小”

在训练神经网络时,我们使用梯度下降法。我们的目的是告诉参数 W :“嘿,你应该往哪个方向走才能减小误差?”

- 带分母的公式算出的是:精确的误差分配比例。  
- 不带分母的公式算出的是:误差的加权和。  

关键点:这两个公式算出来的结果,正负号是一样的,方向是一致的!

只要方向对了,我们就能通过调整学习率(Learning Rate,也就是代码里的 learning_rate)来控制步子迈多大。既然方向没错,那个复杂的分母(归一化系数)就可以被视为被“吸收”进了学习率里。

怎么理解下面那个简单的矩阵公式?

让我们忘掉上面那个复杂的,只看下面这个: Eh=WT⋅Eo
这才是你在写代码(比如 NumPy 或 PyTorch)时真正要用的公式。

  • Eo(Output Error):输出层的误差向量。比如 [0.1, -0.2],表示第一个输出多了0.1,第二个少了0.2。

  • W (Weights):连接隐藏层和输出层的权重矩阵。

  • WT(Transpose):权重的转置。

    • 前向传播时,信号是从左往右流( Input×W )。
    • 反向传播时,误差是从右往左流。为了能让矩阵乘起来(维度匹配),我们需要把矩阵“transpose”一下,相当于把路反过来铺,让误差能顺着原来的路流回去。

# 优化方法

在神经网络里,算出梯度(知道往哪走)只是第一步,怎么迈步(优化方法) 才是决定训练效果的关键。

这张图列出了深度学习进化史上的 5位“大神”。我们可以把训练神经网络想象成 “下山”:你的目标是走到山谷最低点(误差最小),但山上大雾弥漫,你只能感觉到脚下的坡度。

  1. SGD (随机梯度下降) —— “莽撞的探险家”
    这是最基础的方法。它每走一步,都只看脚下这一小块地的坡度(当前batch的梯度)。
    缺点:因为它太短视了,如果脚下是个小坑(局部最优),它就以为到底了,直接停在那儿不动了。而且因为只看眼前,它走路摇摇晃晃(不稳定),容易走冤枉路。

  2. Momentum (动量法) —— “惯性滑行者”
    借用了物理中的动量概念...模拟了运动惯性
    这个方法给探险家装上了轮子。
    它不光看现在的坡度,还记得刚才冲下来的速度。如果刚才一直是下坡,哪怕现在遇到一点点上坡或者小坑,凭着惯性(之前更新的方向),它也能直接冲过去,不会轻易停下来。
    好处:这就解决了SGD容易陷在“小坑”里的问题,而且走路更稳、更快。

  3. Adagrad —— “偏心的教练”
    这是一个很聪明的教练,它会区别对待不同的参数。
    有些参数很重要但数据很少(比如生僻字),Adagrad就会给它大开绿灯(用大的学习率 α ),让它赶紧学;有些参数天天见(比如常用字),它就让你慢慢学(用小的学习率)。
    缺点:它有点“记仇”,会把历史上所有的梯度都累加起来。结果就是学到后面,学习率变得太小太小,直接“躺平”不学了(训练提前结束)。

  4. RMSprop —— “健忘的改良派”
    它是为了解决Adagrad“躺平”问题而生的。
    它的策略是:“别老翻旧账”。它不再累加所有的历史梯度,而是只关注最近一段时间的平均情况(指数加权移动平均)。
    好处:这样学习率就不会一直减小到0,保证了模型能持续学习到最后。

  5. Adam (目前的大神) —— “集大成者”
    这是目前的版本之子,也是你以后写代码时默认首选的优化器。
    它既有 Momentum 的惯性(跑得快,能冲过小坑),又有 RMSprop 的自适应能力(能根据不同参数调整步伐大小)。
    你看图表,紫色的线(Adam)下降得最快,而且最终达到的位置(training cost)也是最低的。这就是实力的证明!

虽然原理都要懂,但在实际写代码(比如用 PyTorch 或 TensorFlow)时:
无脑选 Adam 就对了!
除非你是专门研究优化算法的专家,否则 Adam 几乎能解决 90% 的训练问题。

# 使用numpy实现一个神经网络

  1. 第一部分:参数设置与数据准备
    这部分是在搭建舞台,定义我们的“演员”和“剧本”。
import numpy as np

# 1. 定义超参数和网络结构
# n: 样本数量 (Batch Size),一次喂给网络64个数据
# d_in: 输入层维度,每个数据有1000个特征
# h: 隐藏层维度,中间层有100个神经元
# d_out: 输出层维度,最终预测10个结果
n, d_in, h, d_out = 64, 1000, 100, 10

# 2. 随机生成模拟数据
# x: 输入数据矩阵,形状为 (64, 1000)。randn生成的是标准正态分布的随机数
x = np.random.randn(n, d_in)

# y: 真实标签(目标值),形状为 (64, 10)。
# 注意:这里为了演示简单,y也是随机生成的。在实际项目中,y应该是我们要预测的真实答案。
y = np.random.randn(n, d_out)

# 3. 随机初始化权重 (Weights)
# 这是神经网络的“记忆”,一开始它是乱猜的。
# w1: 连接输入层和隐藏层的权重,形状必须是 (1000, 100) 才能和 x 做矩阵乘法
w1 = np.random.randn(d_in, h)

# w2: 连接隐藏层和输出层的权重,形状必须是 (100, 10)
w2 = np.random.randn(h, d_out)

# 4. 设置学习率 (Learning Rate)
# 这是一个很小的数,控制我们每次修改权重的幅度。
# 如果太大,模型会震荡不收敛;如果太小,学习太慢。
learning_rate = 1e-6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

这里的 np.dot 是核心。在神经网络里,矩阵乘法就是数据流动的方式。x (64x1000) 乘以 w1 (1000x100),消去中间的1000,得到 (64x100) 的结果,正好对应64个样本在100个隐藏层神经元上的激活值。

# 第二部分:训练循环

这是最核心的部分,包含了前向传播(算结果)、计算损失(算误差)和反向传播(算梯度)。

# 开始循环训练 500 次 (Epochs)
for t in range(500):
    # ==========================================
    # 阶段一:前向传播 (Forward Pass) - "做题"
    # ==========================================

    # 1. 输入层 -> 隐藏层
    # 矩阵乘法:输入数据 x 乘以 权重 w1
    temp = x.dot(w1)  # 形状: (64, 100)

    # 2. 激活函数 (ReLU)
    # ReLU的作用是把所有负数变成0,正数保持不变。
    # 这引入了非线性,让神经网络能拟合复杂的曲线。如果没有它,多层网络就退化成了单层线性回归。
    temp_relu = np.maximum(temp, 0) # 形状: (64, 100)

    # 3. 隐藏层 -> 输出层
    # 将激活后的结果乘以第二层权重 w2,得到预测值
    y_pred = temp_relu.dot(w2) # 形状: (64, 10)

    # ==========================================
    # 阶段二:计算损失 (Loss) - "对答案"
    # ==========================================

    # 计算预测值 y_pred 和 真实值 y 之间的差距。
    # 这里使用的是均方误差 (MSE) 的变体(省略了除以2m,不影响梯度方向)。
    # .sum() 把所有样本、所有维度的误差加起来,变成一个标量数值。
    loss = np.square(y_pred - y).sum()

    # 打印当前的迭代次数和损失值,用来监控训练效果
    # 随着 t 增加,loss 应该越来越小
    print(t, loss)

    # ==========================================
    # 阶段三:反向传播 (Backward Pass) - "找原因"
    # ==========================================
    # 这里运用了链式法则 (Chain Rule),从后往前推导梯度。

    # 1. 计算输出层的梯度 (grad_y_pred)
    # 也就是 Loss 对 y_pred 的导数。
    # 因为 Loss = sum((y_pred - y)^2),根据求导公式 (x^2)' = 2x
    # 所以导数是 2 * (y_pred - y)
    grad_y_pred = 2.0 * (y_pred - y) # 形状: (64, 10)

    # 2. 计算 w2 的梯度 (grad_w2)
    # 根据链式法则:dL/dw2 = (dL/dy_pred) * (dy_pred/dw2)
    # dy_pred/dw2 其实就是上一层的输出 temp_relu
    # 矩阵转置 (.T) 是为了匹配矩阵乘法的维度
    grad_w2 = temp_relu.T.dot(grad_y_pred) # 形状: (100, 10)

    # 3. 计算隐藏层输出的梯度 (grad_temp_relu)
    # 继续向前传导误差:dL/d(temp_relu) = (dL/dy_pred) * (dy_pred/d(temp_relu))
    # dy_pred/d(temp_relu) 其实就是 w2
    grad_temp_relu = grad_y_pred.dot(w2.T) # 形状: (64, 100)

    # 4. ReLU 的反向传播 (grad_temp)
    # ReLU 的导数很简单:如果原值 > 0,导数为 1(梯度直接通过);如果原值 < 0,导数为 0(梯度阻断)。
    grad_temp = grad_temp_relu.copy()
    grad_temp[temp < 0] = 0 # 把刚才前向传播中 temp < 0 的位置对应的梯度设为 0

    # 5. 计算 w1 的梯度 (grad_w1)
    # 同理:dL/dw1 = (dL/dtemp) * (dtemp/dw1)
    # dtemp/dw1 其实就是输入 x
    grad_w1 = x.T.dot(grad_temp) # 形状: (1000, 100)

    # ==========================================
    # 阶段四:权重更新 (Update Weights) - "改正错题"
    # ==========================================

    # 使用梯度下降法更新权重。
    # 新权重 = 旧权重 - 学习率 * 梯度
    # 减去梯度是因为我们要沿着梯度的反方向走(下山),以减小 Loss。
    w1 -= learning_rate * grad_w1
    w2 -= learning_rate * grad_w2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

反向传播(Backpropagation)确实是神经网络中最难理解、但也是最核心的部分。很多教程只给公式,不给“体感”。

今天我们就抛弃复杂的数学符号,用一个极简的数值例子 + 图解,带你彻底看透反向传播到底在干什么。

# 一、 核心心法:反向传播到底在干嘛?

请记住一句话:反向传播就是“分锅大会”。

前向传播是“做题”:输入数据,经过层层计算,得出一个预测结果。

计算 Loss是“对答案”:发现做错了,算出总共扣了多少分(误差)。

反向传播是“追责”:
输出层说:“我错了这么多,是因为上一层传给我的数据不对!” -> 把误差传给上一层。

隐藏层说:“收到!但这误差也不全赖我,是因为再上一层给我的数据就不对,而且我的激活函数也‘杀’死了一部分信号。” -> 继续往前传,并计算自己的权重该背多少锅。

# 二、 极简数值演示(手动模拟代码逻辑)

为了让你看懂代码里的矩阵乘法,我们把网络缩小到最小单位。

设定场景

  • 输入x :[2] (假设只有一个特征,值为2)
  • 真实标签 y :[10] (我们希望输出10)
  • 权重w1,w2:假设都是 1 (为了方便计算)
  • 学习率:0.1
  1. 前向传播(做题) 对应代码:temp = x.dot(w1) -> relu -> y_pred = temp_relu.dot(w2)
  • 第一层计算:2×1=2 。
  • ReLU 激活:2>0,所以保持不变,还是 2。(注意:这里存下了一个关键信息:它是正数,路是通的)。
  • 第二层计算:2×1=2。
  • 最终预测:y_pred = 2。
  1. 计算 Loss(对答案)
    对应代码:loss = (y_pred - y)^2
  • 误差 = (2−10)2=(−8)2=64。
  • 结论:错得很离谱!我们需要把预测值从 2 往 10 的方向拉。
  1. 反向传播(分锅 & 算梯度)—— 重点来了!
    我们要算出 w2 和 w1 该怎么改。

第一步:输出层的“惨叫” (grad_y_pred)
代码:grad_y_pred = 2.0 * (y_pred - y)

  • 计算:2×(2−10)=−16 。
  • 含义:这是一个负数,意思是“输出值太小了,必须变大!”。这个 -16 就是我们要往回传的“误差信号”。

第二步:追究w2的责任 (grad_w2)
代码:grad_w2 = temp_relu.T.dot(grad_y_pred)

  • 计算:上一层传来的值(2)× 现在的误差信号(-16) = -32。
  • 含义:w2 的梯度是 -32。
    • 因为梯度是负的,根据“梯度下降”(减去梯度),w2 会变大 ( w2=1−0.1×(−32)=4.2w2=1−0.1×(−32)=4.2 )。
    • 直觉:w2 变大了,下一次2×w2 的结果就会变大,这就修正了“输出太小”的问题。逻辑通了!

第三步:把锅甩给隐藏层 (grad_temp_relu)
代码:grad_temp_relu = grad_y_pred.dot(w2.T)

  • 计算:误差信号(-16)× 当前权重(1) = -16。
  • 含义:隐藏层的输出端收到了 -16 的信号。意思是:“你传给下一层的值太小了,你得变大点!”

第四步:ReLU 的“关卡检查” (grad_temp)
代码:grad_temp[temp < 0] = 0

  • 检查:刚才前向传播时,temp 是 2 (正数)。
  • 操作:既然是正数,说明 ReLU 当时是放行的。所以梯度原样通过,还是 -16。
  • 关键点:如果刚才 temp 是 -5,ReLU 把它变成了 0。那么在反向传播时,ReLu 会说:“这信号是我掐断的,跟你前面的输入没关系,你也别改了。” 于是梯度直接变成 0。这就是代码里那行赋值为 0 的意义。

第五步:追究 w1 的责任 (grad_w1)
代码:grad_w1 = x.T.dot(grad_temp)

  • 计算:原始输入(2)× 传回来的误差信号(-16) = -32。
  • 含义: w1 也要变大!因为输入是正的,只有 w1 变大,第一层的输出才会变大,进而让最终结果变大。

Loss 在不断减小,说明你的神经网络真的在“学习”! 同学,恭喜你!当你亲手跑通这段代码时,你就已经超越了绝大多数只会调用框架的“调包侠”。你已经明白了 AI 是如何从一堆随机数,通过做题、挨骂、改正,一步步变聪明的。