神经网络基础:从神经元到卷积池化
各位同学,今天我们来聊聊神经网络的基础。说实话,这部分内容看起来简单,但我在NPU设计这行干了十几年,发现很多工程师栽跟头就栽在这些基础概念上。你想想看,指令集设计也好,代码生成也罢,底层逻辑都离不开这些基本运算。
神经元模型:计算的最小单元
神经元模型,说白了就是一个数学函数。它接收多个输入,每个输入带一个权重,加上偏置,最后通过激活函数输出。
我记得刚入行那会儿,带我的老工程师跟我说:「别看这公式简单,NPU里90%的功耗都花在这上面。」后来我做NPU架构设计时,才真正体会到这句话的分量。
数学表达是这样的:
y = f(Σ(wᵢ × xᵢ) + b)
其中:
- xᵢ — 输入信号
- wᵢ — 对应的权重
- b — 偏置项
- f — 激活函数
核心要点:神经元本质就是「加权求和 + 非线性映射」。NPU的乘累加器(MAC)阵列,就是为这个操作量身定做的。
激活函数:非线性才是灵魂
如果没有激活函数,多层神经网络就退化成线性变换。那跟单层感知机有啥区别?所以激活函数是神经网络的灵魂。
ReLU — 工业界的最爱
ReLU(Rectified Linear Unit)是我个人最常用的激活函数。公式简单到令人发指:
f(x) = max(0, x)
为什么它这么受欢迎?
- 计算量极小 — 一个比较器就搞定
- 梯度不饱和 — 正区间梯度恒为1
- 稀疏激活 — 约一半神经元输出为0
我的经验:在NPU设计中,ReLU几乎不占硬件资源。我曾在某款AI芯片里,直接把ReLU合并到MAC输出级,省掉了单独的激活模块。
不过ReLU有个坑 — 神经元死亡问题。当输入为负时,梯度为0,参数再也不更新了。我曾经调试过一个模型,发现30%的神经元都「死」了,后来改用Leaky ReLU才解决。
Sigmoid — 经典但昂贵
Sigmoid函数把输入映射到(0,1)区间:
f(x) = 1 / (1 + e⁻ˣ)
它的优点是输出平滑,适合做概率输出。但缺点也很明显:
- 计算开销大 — 涉及指数运算
- 梯度饱和 — 两端梯度接近0
- 输出非零中心 — 影响收敛速度
避坑指南:我曾经在NPU里用查找表实现Sigmoid,结果精度不够导致模型不收敛。后来改用分段线性逼近,精度和面积才达到平衡。
Tanh — 零中心版本
Tanh是Sigmoid的零中心版本:
f(x) = (eˣ - e⁻ˣ) / (eˣ + e⁻ˣ)
输出范围(-1,1),收敛速度比Sigmoid快。但同样有梯度饱和和计算开销大的问题。
前向传播与反向传播
这两个概念,我建议你从「计算图」的角度理解。
前向传播
前向传播就是数据从输入层流到输出层的过程。每一层做三件事:
- 接收上一层的输出
- 做加权求和
- 通过激活函数
代码实现很简单:
def forward(x, weights, bias):
z = np.dot(weights, x) + bias
a = relu(z)
return a
反向传播
反向传播是训练的核心。它用链式法则计算损失函数对每个参数的梯度。
我刚开始学的时候,总觉得这玩意儿很玄乎。后来做NPU指令集设计时,发现反向传播其实就是「前向传播的逆过程」加上「梯度计算」。
关键公式:
∂L/∂w = ∂L/∂a × ∂a/∂z × ∂z/∂w
其中:
- ∂L/∂a — 损失对激活输出的梯度
- ∂a/∂z — 激活函数的导数
- ∂z/∂w — 加权求和对权重的导数(就是输入x)
设计启示:NPU要同时支持前向和反向传播。我建议在指令集里设计专门的「梯度计算指令」,把激活函数的导数计算硬件化。
卷积与池化:特征提取的利器
卷积和池化是CNN的核心操作。在NPU设计中,这两个操作的硬件实现占了芯片面积的大头。
卷积运算
卷积说白了就是「滑动窗口内积」。一个卷积核在输入特征图上滑动,每个位置做一次点积。
数学表达:
y[i,j] = Σₘ Σₙ x[i+m, j+n] × w[m,n] + b
关键参数:
| 参数 | 含义 | 典型值 |
|---|---|---|
| Kernel Size | 卷积核尺寸 | 3×3, 5×5 |
| Stride | 滑动步长 | 1, 2 |
| Padding | 边缘填充 | Same, Valid |
硬件优化技巧:我在设计NPU时,把卷积操作拆成「数据加载」和「MAC计算」两个阶段。利用数据重用,把内存带宽利用率提升了3倍。
池化运算
池化就是下采样。常见的有两种:
- 最大池化 — 取窗口内的最大值
- 平均池化 — 取窗口内的平均值
池化的作用:
- 降低特征图尺寸,减少计算量
- 增强平移不变性
- 防止过拟合
代码实现:
def max_pooling(x, pool_size=2, stride=2):
h, w = x.shape
out_h = (h - pool_size) // stride + 1
out_w = (w - pool_size) // stride + 1
output = np.zeros((out_h, out_w))
for i in range(out_h):
for j in range(out_w):
output[i,j] = np.max(x[i*stride:i*stride+pool_size,
j*stride:j*stride+pool_size])
return output
注意:池化层没有需要训练的参数。在NPU里,池化可以用简单的比较器或加法器实现,不需要MAC单元。
本章知识体系
下面这张图展示了本章的核心逻辑:
嗯,到这里神经网络的基础就讲完了。这些概念看起来简单,但每个都值得深挖。我在NPU设计中最深的体会是:基础越扎实,后面做指令集设计和代码生成就越顺手。