框架核心抽象层:张量、计算图与自动微分
大家好,我是你们的老朋友。今天咱们聊聊深度学习框架最底层的三驾马车:张量、计算图和自动微分。这三个东西,说白了就是框架的「灵魂三件套」。你不管用 PyTorch 还是 TensorFlow,底层都在玩这三样。
我记得刚入行那会儿,总觉得框架就是个黑盒子。把数据扔进去,调几个 API,模型就跑起来了。直到有一次,我自己手写了一个简单的自动微分引擎,才真正理解了框架的设计哲学。嗯,今天我就把这点心得分享给你。
张量:框架里的「一等公民」
张量是什么?说白了就是多维数组。但框架里的张量,可比 NumPy 的 ndarray 要「重」得多。
我个人习惯把张量理解为「带属性的数据容器」。它不光存数据,还带着梯度、设备信息、计算历史。你看下面这个定义:
class Tensor:
def __init__(self, data, requires_grad=False):
self.data = data # 实际数据
self.grad = None # 梯度
self.grad_fn = None # 梯度计算函数
self.requires_grad = requires_grad
self.device = 'cpu' # 运行设备
这里有个关键点:requires_grad 这个标志。我在项目中遇到过不少新手,把所有张量都设成需要梯度,结果内存直接爆了。记住,只有需要求导的参数才设这个标志。
张量的操作也很讲究。加减乘除这些基础操作,框架都做了重载。比如:
a = Tensor([1, 2, 3])
b = Tensor([4, 5, 6])
c = a + b # 这里触发了 __add__ 方法
你想想看,这个 + 操作背后发生了什么?框架不光做了数值相加,还悄悄记录了这个操作,为后面的自动微分做准备。
计算图:静态图 vs 动态图
计算图,就是记录运算过程的「流水账」。框架通过它知道:谁是谁的输入,谁是谁的输出。
这里有个经典之争:静态图还是动态图?
| 特性 | 静态图(TensorFlow 1.x) | 动态图(PyTorch) |
|---|---|---|
| 构建方式 | 先定义后执行 | 边定义边执行 |
| 调试难度 | 较高,不能逐行调试 | 较低,可以 print 中间结果 |
| 性能优化 | 全局优化,性能好 | 局部优化,稍差 |
| 控制流 | 需要特殊 API | 原生 Python 控制流 |
我个人更偏爱动态图。为什么?因为调试太方便了。我曾经在一个静态图项目里,为了查一个 bug,硬是花了三天。换成动态图,print 一下中间变量,十分钟就搞定了。
但静态图也有它的优势。TensorFlow 后来搞了 tf.function,其实就是想结合两者的优点。说白了,动态图方便开发,静态图适合部署。
下面我用 SVG 画了一张图,帮你理清计算图的整体逻辑:
自动微分:框架的「魔法」
自动微分,英文叫 Autograd。这玩意儿是框架最核心的「魔法」。你想想看,你写了一个 100 层的网络,框架是怎么自动算出所有参数的梯度的?
原理其实不复杂。框架在计算图中记录了每个操作的「局部导数」。然后利用链式法则,从输出一路反向传播到输入。
举个例子:
# 假设我们有这个计算
a = Tensor([2.0], requires_grad=True)
b = Tensor([3.0], requires_grad=True)
c = a * b # c = 6.0
d = c + 1 # d = 7.0
# 反向传播
d.backward()
# 查看梯度
print(a.grad) # 输出 3.0 (因为 dd/da = b = 3)
print(b.grad) # 输出 2.0 (因为 dd/db = a = 2)
这里有个细节:backward() 是怎么知道要计算谁的梯度的?它从 d 这个张量出发,沿着计算图反向遍历,每经过一个操作节点,就调用该节点注册的梯度函数。
自动微分的实现,核心就是两样东西:
- 梯度函数注册表:每个操作(加法、乘法等)都注册了对应的梯度计算函数
- 拓扑排序:反向传播时,按照计算图的拓扑顺序反向遍历
说白了,框架就是帮你把微积分里的链式法则,用代码实现了一遍。你不需要手动推导梯度公式,框架替你干了。
好了,这一章的内容就到这里。张量是数据载体,计算图是运算记录,自动微分是梯度引擎。这三者配合,构成了深度学习框架的基石。下一章我们会深入聊聊框架的算子库和内存管理,到时候见。
公众号:蓝海资料掘金营,微信deep3321