导言
神经网络是人工智能时代的核心技术。无论是深度学习,还是当前大热的大模型,其底层都以神经网络为基础。可以毫不夸张地说,没有神经网络,就没有今日人工智能的蓬勃景象——它是整个领域的基石。
然而,对许多初学者而言,神经网络的入门并不轻松。我们经常看到一些过度抽象的类比:把神经网络比做人脑,把梯度下降比作从山坡往下走。这些类比虽然有助于建立直觉,但总让人觉得隔了一层,不够扎实。另一种常见情况则是,一上来就被带进复杂的数学推导,面对密密麻麻的符号和公式,不仅云里雾里,还容易迷失方向——只见树木,不见森林。
我看了安德烈·卡帕斯(Andrej Karpathy)在 YouTube 上的一节神经网络教学视频。他的切入点和讲法令人耳目一新,甚至可以说是拍案叫绝。不愧是大佬中的大佬。这篇文章的主要内容,正是基于他那近两个小时的课程,我将其中的核心观点提炼出来,并结合图示、代码,将这些关键思想串联成一个易于理解的整体。
阅读本文,你只需要具备一点 Python 基础,以及初高中水平的数学知识即可。
现在,让我们正式开始吧。
回顾导数知识
在开始之前,我们需要复习一个最关键的数学概念——导数。当然,这里我们并不需要像在学校那样,去背各种导数公式,或者手工推导复杂表达式的导数。在神经网络中,模型的结构往往包含大量的嵌套函数和参数,想靠手工求导几乎是不可能的。
在神经网络的语境下,我们真正需要理解的不是“怎么求导”,而是导数的含义是什么、它代表了什么信息、以及它在神经网络中发挥什么作用。掌握了这些核心概念,就能够理解神经网络是如何学习、如何优化的。
导数的定义与本质
导数用于衡量当输入发生微小变化时,函数输出会随之发生多大变化。直观来说,它回答了一个问题:,导数刻画的是:函数对输入参数有多敏感,即:当我微微改变输入,输出会改变多少?。
因此,导数是描述函数变化的重要工具。从函数变化率的角度来看,导数可以用来描述函数在某一点的变化速度、变化趋势、单调性以及极值点。
假设有一个单变量函数 f(x)f(x)f(x),变量为 xxx。
函数的导数可以表示为:
f′(x)=limh→0f(x+h)−f(x)hf'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}f′(x)=h→0limhf(x+h)−f(x)
这里,hhh 是自变量 xxx 的一个很小的增量(趋近于 0),分子表示函数值的变化量,分母表示变量的变化量。导数 f′(x)f'(x)f′(x) 描述了函数在每一点的变化快慢和方向。
从图像角度理解导数
下面这张图展示了函数 f(x)f(x)f(x) 与其导数 f′(x)f'(x)f′(x) 的关系:

下方曲线 f(x)f(x)f(x): 表示函数的形状。可以看到函数先上升、再下降,最后快速上升。
上方曲线 f′(x)f'(x)f′(x):表示函数在各点的导数值,也就是函数变化的快慢和方向。
**红点:**标记了函数上的对应点,通过垂线可以看到每个点的切线斜率如何映射到导数图上:
- 当函数上升时,导数为正(红点对应在上方正区间)。
- 当函数下降时,导数为负(红点对应在下方负区间)。
- 当函数达到极值(峰顶或谷底)时,导数为 0(红点位于 x 轴上)。
理解了导数的本质,就能明白它为什么如此重要。神经网络本质上就是一个超级复杂的函数,而我们做的所有操作,本质上都是在与这个函数打交道。
导数告诉我们函数对输入参数的敏感程度,是理解和调控神经网络的核心工具。随着学习的深入,大家会对它的重要性有更深刻的体会。
从最简单的加、乘法运算开始
万丈高楼平地起。虽然神经网络看起来复杂,但其底层运算依然基于数学最基本的操作——主要是加法和乘法(尤其是在矩阵和向量运算中)。下面,我们先从最简单的**“加、乘”运算开始,逐步理解神经网络的运算机制。**
class Value:
# 构造函数初始化对象
def __init__(self,data):
self.data=data
# 定义对象的字符串表示形式
def __repr__(self):
return f"Value(data={self.data})"
# 实现加法运算
def __add__(self,other):
out =Value(self.data+other.data)
return out
# 定义乘法运算
def __mul__(self,other):
out=Value(self.data*other.data)
return out
上面的代码定义了一个简单的类,它像一个“小容器”,用来封装数字。计算前的输入和计算后的结果都可以用这个类来表示。
虽然,现在它只支持最基本的加法和乘法运算,但随着学习的深入,我们会逐步丰富它的功能,让它不仅能做更多运算,还能支撑更多的功能。
假设要计算表达式 a×b+ca×b+ca×b+c 的值,就可以先用 Value 类将数字封装成对象,然后在这些对象上进行运算,如下面代码:
a=Value(2.0)
b=Value(3.0)
c=Value(4.0)
print(a*b+c) # 表达式计算
print((a.__mul__(b)).__add__(c)) # 函数式计算
输出结果为:
Value(data=10.0) Value(data=10.0)
修改Value类已支持可视化展示
上面的代码实现了最基本的加法和乘法运算,运行的表达式也比较简单。但在实际中,神经网络对应的表达式会非常庞大、复杂。为了更好地理解神经网络的计算过程,我们有必要对运算进行可视化。
例如,考虑如下表达式:
L=(a×b+c)×fL=(a×b+c)×fL=(a×b+c)×f
当 (a=2,b=−3,c=10,f=−2)(a=2, b=-3, c=10, f=-2)(a=2,b=−3,c=10,f=−2) 时,我们希望展示如下图所示的计算过程。
这张图直观、清晰地呈现了整个表达式的运算流程,使我们能够方便地理解每一步的计算过程及其结果。

为了实现可视化计算的目标,我们需要对 Value 类进行如下改进。在上一个版本的基础上新增了三个参数:
-
_prevNodes:记录当前 Value 对象是由哪些之前的 Value 对象计算得到的,它用于构建表达式的计算图或树形结构,可以追踪每一步计算的来源。在可视化或反向传播中,可以通过 _prevNodes 找到计算的依赖关系。 -
_op:用于在可视化图形中显示运算符号(如加号、乘号),便于直观理解计算操作。 -
_label:在可视化计算图时,可以在图中椭圆里显示运算符号,让每个节点代表的运算类型一目了然。
如下面代码:
class Value:
# 构造函数初始化对象
def __init__(self,data,_prevNodes=(),_op='',_label=''):
"""
data: 实际的标量值
_prevNodes: 前置节点,每次表达式计算,在得到新的值中,将原值作为_prevNodes
_op: 用于标识运算
"""
self.data=data
self._prevNodes=set(_prevNodes)
self._op=_op
self._label=_label
# 定义对象的字符串表示形式
def __repr__(self):
return f"Value(data={self.data})"
# 实现加法运 +
def __add__(self,other):
out =Value(self.data+other.data,(self,other),'+')
return out
# 定义乘法运 *
def __mul__(self,other):
out=Value(self.data*other.data,(self,other),'*')
return out
使用修改后的 Value 类进行计算,我们来计算表达式L=(a×b+c)×fL = (a \times b + c) \times fL=(a×b+c)×f,,代码如下:
a = Value(2.0, _label='a')
b = Value(-3.0, _label='b')
c = Value(10.0, _label='c')
f = Value(-2.0, _label='f')
# 逐步计算中间结果
e = a * b
e._label = 'e'
d = e + c
d._label = 'd'
L = d * f
L._label = 'L'
print(L)
- 首先将每个数字用
Value类封装,并添加_label以标记变量名。 - 然后按计算顺序依次生成中间值
e、d,最后计算最终结果L。 - 打印
L时,输出的是Value对象,方便后续进行可视化或追踪计算。
计算结果为:Value(data=-8.0)
编写格式化工具类
前面我们已经对 Value 类进行了扩展,使其可以记录前置节点、运算符和标签,从而支持计算图的可视化展示。
下面,我们将展示一段稍微复杂的示例代码,用于生成并可视化整个表达式的计算过程。该方法是独立实现的,如果不想深入研究,可以暂时把它当作一个“黑盒”使用。代码中附有详细注释,有兴趣的读者可以仔细查看。
⚠️ 注意:此示例需要安装 graphviz Python 库,同时系统中必须安装 Graphviz 可执行程序(Digraph 所依赖的 dot),并配置好环境变量。具体安装和配置方法可以参考官方文档或使用 AI 完成。
from graphviz import Digraph
def trace(root):
"""
从最终结果节点 root 出发,
递归遍历整个表达式图,收集所有节点(nodes)和边(edges),
为后续绘图做准备。
"""
nodes, edges = set(), set() # nodes 存储所有参与运算的 Value 节点;edges 存储节点之间的连接关系
def recursionBuild(node):
# 避免重复添加同一个节点
if node not in nodes:
nodes.add(node)
# 遍历当前节点的所有前置节点(即参与生成该节点的输入节点)
for child in node._prevNodes:
edges.add((child, node)) # 记录 child -> node 的连接关系
recursionBuild(child) # 递归处理 child,继续向上追溯整个计算图
recursionBuild(root) # 从最终输出 root 开始向前递归追踪
return nodes, edges
def draw_dot(root, format='svg', rankdir='LR'):
"""
将表达式的计算过程绘制为可视化计算图。
format: 输出图像格式,如 'png'、'svg'
rankdir: 图的方向
- 'LR': 从左到右布局
- 'TB': 从上到下布局
"""
assert rankdir in ['LR', 'TB']
# 获取整个计算图的节点与边
nodes, edges = trace(root)
# 创建一个 Graphviz 的 Digraph 对象,用于绘制图
dot = Digraph(
format='png',
graph_attr={'rankdir': rankdir, 'size': '11,11'} # 设置图的方向和大小
)
# 遍历所有节点,绘制变量节点和运算符节点
for n in nodes:
sid = str(id(n)) # 使用节点内存地址作为节点的唯一 ID
# 绘制变量节点(方框:显示 label 与 data)
dot.node(
name=sid,
label="{%s | data %.1f }" % (n._label, n.data), # 显示标签与数据值
shape='record'
)
# 如果当前节点是由某个运算生成的(例如 + 或 *)
if n._op:
# 额外绘制一个小圆节点显示运算符
dot.node(
name=sid + n._op,
label=n._op
)
# 连接运算符节点 → 变量节点
dot.edge(sid + n._op, sid)
# 绘制所有边: child → op → parent
for n1, n2 in edges:
dot.edge(
str(id(n1)),
str(id(n2)) + n2._op
)
return dot
有了可视化方法,我们就可以应用到上面的表达式L=(a×b+c)×fL = (a \times b + c) \times fL=(a×b+c)×f 上。
当得到最终结果 L 后,只需将它传入 draw_dot(L) 这个可视化函数,整个计算过程就会被直观地呈现在图上。
- 矩形表示
Value对象,同时显示了变量的_label名称和数值。 - 椭圆形表示运算符,每一步加法或乘法都清晰标注。
从左到右,计算流程按顺序展现,每个变量、每个运算步骤都一目了然。
这种可视化不仅帮助我们快速理解复杂表达式的计算过程,也为后续更深入的研究打下基础——正所说,“磨刀不误砍柴工”。

由于文章内容较多,为了提升写作效率,同时减轻读者的阅读负担,我们将整篇内容拆分为多篇文章。本篇主要讲解导数的概念,以及最基本的数字封装与计算可视化。
在后续文章中,我们会逐步深入,最终一步步构建出一个完整的神经网络框架。请大家耐心等待后续内容的发布。
1678

被折叠的 条评论
为什么被折叠?



