2. 神经网络基础
从生物神经元到NPU加速——这一章我们聊聊神经网络最底层的砖石。
2.1 从生物神经元到数学神经元
我个人习惯把神经网络比作一个「乐高积木系统」。最基础的积木块,就是神经元。生物神经元有树突、轴突、突触,而数学神经元就三样东西:输入、权重、激活。
说白了,一个神经元就是做了一件事:加权求和 + 非线性映射。公式长这样:
z = w₁·x₁ + w₂·x₂ + ... + wₙ·xₙ + b
a = f(z) // f 就是激活函数
我在项目中遇到过不少新手,一上来就堆层数,结果连单个神经元输出都算不对。嗯,先把这个基础打牢,后面NPU的SIMD流水线才能理解透彻。
2.2 感知机:最简单的神经网络
感知机就是单层神经元,输出只有0或1。你想想看,它本质上是一个线性二分类器。当年Rosenblatt提出感知机时,大家兴奋得不行,后来发现连异或(XOR)都搞不定……
不过别小看它。我在做NPU指令集设计时,感知机就是「向量-矩阵乘-累加」的最小原型。NPU里一个周期能算几百个感知机,这就是加速的起点。
2.3 激活函数:给神经网络注入非线性
没有激活函数,再多层也只是线性变换,那跟单层没区别。激活函数就是「灵魂」。常用的就两个:
| 激活函数 | 公式 | 特点 | 我常用的场景 |
|---|---|---|---|
| Sigmoid | σ(z) = 1/(1+e⁻ᶻ) | 输出(0,1),平滑,但梯度饱和 | 二分类输出层,偶尔用在门控单元 |
| ReLU | f(z) = max(0, z) | 稀疏激活,计算极快,梯度不饱和 | 隐藏层首选,NPU上延迟极低 |
为什么会这样?因为NPU擅长线性运算,ReLU就是个比较器,硬件开销极小。而Sigmoid涉及指数运算,在NPU上要么查表,要么用PWL(分段线性)拟合。
2.4 前向传播:数据流过网络
前向传播就是输入数据一层层往前算,直到输出。每一层都是:加权求和 → 激活。代码实现非常直观:
def forward(x, weights, biases):
z = np.dot(weights, x) + biases
a = relu(z) # 或者 sigmoid
return a
你想想看,这个计算过程在NPU上就是典型的「矩阵乘+向量加+激活」。我设计NPU微架构时,前向传播的流水线是重中之重——数据从DMA搬进SRAM,然后 systolic array 做矩阵乘,激活单元直接跟在后面,一个时钟出结果。
2.5 反向传播:神经网络的学习引擎
反向传播就是链式法则的工程实现。从损失函数开始,逐层计算梯度,然后更新权重。公式链长这样:
∂L/∂w = ∂L/∂a · ∂a/∂z · ∂z/∂w
说白了,就是误差从输出层往回传,每一层都算出权重该调整多少。我曾经在调试NPU反向传播算子时,发现梯度爆炸,最后定位到是激活函数导数没做clip。嗯,细节决定成败。
1. 计算损失函数梯度
2. 计算激活函数导数
3. 计算权重梯度
4. 更新权重(SGD / Adam 等)
在NPU上,反向传播比前向传播更吃带宽。因为需要同时读取激活值、权重和梯度。我建议做异构计算时,把反向传播的梯度更新放在CPU或者专用梯度累加单元上,别跟前向争带宽。
2.6 从基础到NPU加速的直觉
讲完这些基础,你大概能感觉到:神经网络的计算核心就是矩阵乘法 + 激活函数 + 梯度链式法则。NPU之所以快,就是把这三种操作硬化为专用流水线。
- 矩阵乘 → Systolic Array / 脉动阵列
- 激活函数 → 查表或PWL单元
- 梯度计算 → 共享前向的矩阵乘单元,减少数据搬运
我个人习惯在写NPU驱动时,先把前向和反向的访存模式画出来,再分配DMA通道。这样能避免一半以上的性能坑。