Attention Is All You Need #2:从神经元到矩阵
OK!现在是2026年10月5日,手头还有一个科创比赛。。。。好累。。。
这篇博客的目的是把上一篇博客的MLP从单个神经元遍历升级成矩阵计算!
可是。。。什么是矩阵啊
前置知识
矩阵
GPT讲到
比如一层有 3 个输入、2 个神经元。
以前你可能写:
神经元1: w11, w12, w13
神经元2: w21, w22, w23
现在直接把所有权重塞进一个矩阵:
我去了矩阵的Latex咋写啊,学习一下
无括号矩阵:
\begin{array}{c}
a & b \\
c & d
\end{array}
方括号矩阵
\begin{bmatrix}
a & b \\
c & d
\end{bmatrix}
圆括号矩阵
\begin{pmatrix}
a & b \\
c & d
\end{pmatrix}
大括号矩阵
\begin{Bmatrix}
a & b \\
c & d
\end{Bmatrix}
ok~差不多啦
所以矩阵是什么呢?
矩阵是线性代数领域的核心数学术语,指由若干元素按行、列排列形成的矩形阵列,最常见为实数矩阵与复数矩阵,元素也可取自域、环乃至符号、函数等其他数学对象。作为早期解线性方程组的工具,其雏形对应方程组的系数表与增广矩阵,无需为正方形阵列。
没明白,通俗一点的呢
最通俗地说:
矩阵就是“按行和列排好的一堆数”。
比如:
这就是一个两行三列的小表格
但是矩阵真正厉害的地方是:它不只是“表格”,还可以表示很多组数据,或者一套变换规则。
放到我现在的神经网络里就特别好理解:
可以直接把它读成:
第一行,是第一个神经元的全部权重
第二行,是第二个神经元的全部权重
之前说过权重是向量,所以
矩阵 = 多个向量摞在一起
用Python讲解的话就是
neuron1.weights = [w11, w12, w13]
neuron2.weights = [w21, w22, w23]
现在只是写成:
W = [
[w11, w12, w13],
[w21, w22, w23]
]
本质上没变
为了神经网络的话,先吃透 加法、乘一个数、矩阵乘法就好啦~
矩阵加法
对应位置直接加:
现在有
那么加一块就是:
所以矩阵加法有个前提:形状必须一样。
单数字乘矩阵
然后矩阵乘一个数字,就是挨个乘
矩阵乘法
真正有点东西的是矩阵乘法
先看个熟悉的情况:
然后有一个向量:
计算:
GPT说算的时候,不要去想“矩阵怎么乘”,直接想一句
A的每一行,都去和x做一次点乘
第一行:
第二行:
所以:
通俗理解矩阵乘法:
左边每一行,和右边每一列做点乘。
比如两个矩阵:
那么 AB 的左上角,就是:
左下角:
右下角:
于是:
规律就是
还有一个今晚一定要记住的尺寸规则:
中间必须一样,中间消掉,留下两边。
这玩意儿放神经网络里马上就活了。假设一层有 3 个输入,2 个神经元:
这是 2\times3。 输入:
这是 3\times1。 所以:
尺寸就是:
结果正好两个数:
也就是 两个神经元各自算出来的结果。
转置
把矩阵沿主对角线“翻一下”,行变列,列变行。
比如:
它有 2 行 3 列,也就是:
转置记作:
结果是:
一句话:
哈达玛积
这个东西在神经网络里通常就是
对应位置的元素分别相乘
比如:
区别:
- AB:矩阵乘法,行 × 列再求和
- A\odot B:逐元素乘法,同位置直接乘
- \vec a\cdot\vec b:向量点乘,乘完还要加起来得到一个数
外积
外积可以理解成一个列向量 × 一个行向量,结果变成一个矩阵。
比如:
那么:
所以:
点积:
最后得到 一个数。
外积得到一个矩阵
至此,把MLP从真正的神经元转为矩阵的前置知识,正式学习完啦~
那么接下来的任务就是!把99的MLP变成矩阵版!
听GPT说Neuron类就要消失了,该怎么做呢?
先抓住今天唯一的核心变化:
原来的神经网络结构类似:
Network
├─ Layer
│ ├─ Neuron
│ ├─ Neuron
│ └─ Neuron
└─ Layer
└─ Neuron
现在我们要变成:
Network
├─ Layer(W, b)
└─ Layer(W, b)
forward矩阵化
别急着碰反向传播。咱们先真正感受到矩阵到底替代了什么。
假设这一层:
- 输入3个数
- 有两个神经元
咱们现在的代码:
neuron1.weights = [w11, w12, w13]
neuron1.bias = b1
neuron2.weights = [w21, w22, w23]
neuron2.bias = b2
现在用矩阵表示:
然后bias也不用两个神经元分别存了:
输入变成:
于是整个Layer:
就完事了!?
因为
展开看看呢
所以矩阵压根没有改变神经网络的数学原理。
它只是把:
for neuron in neurons:
for x, w in zip(inputs, neuron.weights):
z += x * w
变成:
z = w @ x + b
矩阵化不是换了一种神经网络,而是把大量相同结构的神经元计算合并成一次线性代数运算。
神了!
下面开始写代码~~~
因为矩阵运算需要numpy,所以先导入
import numpy as np
我也是终于开始用numpy了呜呜呜太好了
class Layer:
def __init__(self, neuron_num, weights_num):
# 初始化神经元矩阵,random.randn是生成行x列的矩阵,里面的数值随机
# 因为每个输入要和每个权重点乘,又因为矩阵乘法是行点乘列,所以每个神经元的每个权重存在矩阵的行中
# 然后每行里的每个权重就可以和输入(看做多行1列的矩阵)点乘啦~
self.W = np.random.randn(neuron_num, weights_num)
# 创建一个neuron_num行,1列的矩阵,也就是咱们的偏置矩阵!
# 偏置通常可以直接初始化为 0
# 因为 bias 不需要像权重一样靠随机初始化来打破神经元之间的对称性。
self.b = np.zeros((neuron_num,1))
def leaky_ReLU(self, x):
# np.where可以理解为批量1f
# np.where(条件, 条件成立时的值, 条件不成立时的值)
return np.where(x > 0, x, 0.01*x)
def forward(self, x):
# 按照咱们的矩阵乘法公式,numpy里使用@进行矩阵乘法
self.z = self.W @ x + self.b
#激活函数
self.haty = self.leaky_ReLU(self.z)
return self.haty
然后写Network,网络的作用就是让数据穿过每一层
class Network:
def __init__(self, layers):
self.layers = layers
def forward(self, x):
for layer in self.layers:
x = layer.forward(x) #上一层输出就是下一次输入
return x
Neuron类真的没了!
因为全放到矩阵里去了
Backward
矩阵版Backward怎么做呢
首先看看咱们的矩阵版Forward
def forward(self, x):
self.x = x #依旧保存输入
# 按照咱们的矩阵乘法公式,numpy里使用@进行矩阵乘法
self.z = self.W @ x + self.b
#激活函数
self.haty = self.leaky_ReLU(self.z)
return self.haty
换成数学:
backword反过来:
梯度公式的链式法则:
把前两项合起来:
这里我得记一下,为什么合体呢?
因为一个神经元中的所有权重,共用一个\delta
又因为神经元线性表达式:
一个神经元最后只得到一个z,所以一个神经元共用一个\delta
有:
i表示第i个神经元,j表示这个神经元的j个输入/权重
于是单个梯度可以变成:
现在假设一层有2个神经元,3个输入:
输入:
因为有俩神经元,所以会得到两个 \delta ,一人一个:
补充一下,在这里的\delta矩阵里,每个元素都是直接的\frac{\partial L}{\partial \hat y} \cdot \frac{\partial \hat y}{\partial z}。所以两个矩阵也要一个对一个的相乘。所以这里不能用矩阵乘法,翻看之前的笔记,这里要用哈达玛积,就是直接的对应位置相乘
numpy中哈达玛积就是直接用*号
咱们已经知道了单个权重梯度的写法,那现在就给所有的权重一个一个写出来。
第一个神经元:
第二个神经元:
然后每一个的算法都一样对吧,咱们按照神经元和权重原来的矩阵,对应着再整一个“梯度矩阵”
hmmmm,我得好好看看这个东西
我去这不就是!?
吗?!
没问题,行乘列,\delta矩阵的第一行是\delta_1,然后分别乘以第二个矩阵的x_1 ,x_2, x_3
但是咱们的输入是一列多行的矩阵啊,这咋办
翻了翻前面的笔记,找到了!转置!
行变成列,列变成行
所以最后的对于一个权重矩阵的梯度:
在这里加一个我的小疑问,为什么不转置\delta呢?
因为转置了以后
式子变成
\delta^T= \begin{bmatrix} \delta_1&\delta_2 \end{bmatrix}\\ \\ \delta^T x这个矩阵乘法能算吗?
矩阵乘法的法则是,两个矩阵必须遵循
(行1x列1)(行2x列2)
左边矩阵的列数必须等于右边矩阵的行数。
也就是列1=行2
现在是
(1×2)(3×1)
乘不了
那掉个个不就能乘了吗?
确实可以,但是咱们得到的是梯度矩阵的转置版
必须再转置回来才能和 W 的排列对应,因此不如一开始直接构造与 W 同形状的梯度矩阵。
好!接下来咱们已经找到了梯度矩阵,那怎么梯度下降呢?
回顾一下上次的笔记
以前的单个权重:
w减去单个的权重梯度乘以学习率
那矩阵呢?以此类推
也就是直接
numpy中常数乘矩阵也直接使用*
对于偏置的梯度下降呢?
对于每个偏置
这个就好理解多啦
来写代码试试看
先把Leaky ReLU的导数写了,numpy版的
因为是矩阵,所以代码要用np.where
def leaky_ReLU_derivative(self, z):
return np.where(z > 0, 1, 0.01)
那么backward就是
def backward(self, output_gradient, learning_rate):
dhaty_dz = self.leaky_ReLU_derivative(self.z)
dL_dhaty = output_gradient
delta = dL_dhaty * dhaty_dz
dL_dW = delta @ self.x.T #.T转置矩阵
dL_db = delta
#梯度下降
self.W -= learning_rate * dL_dW
self.b -= dL_db * learning_rate
梯度下降完成!好耶!
但只下降是不行的,咱们还得把输入的梯度反向传播给上一层,当做上一层神经元输出产生的梯度
回顾一下之前的笔记,怎么求单个输入产生的梯度呢?
用前面的\delta代换就是
因为神经元激活函数前的函数是线性的,所以
所以单个神经元通过某个权重,对对应输入产生的梯度贡献就是:
同一个输入 x_j 会连接到这一层的多个神经元,所以它最终的梯度,要把所有神经元对它的梯度贡献加起来。
那换成矩阵怎么办呢?
一个一个排列试试看呗
比如:
那么:
把它写成矩阵:
也就是摞在一起:
想要矩阵乘法怎么办呢?咱们的公式是和\delta相乘。
试试看:
(2×3)(2×1)
啊噢,乘不了
转置试试看!
(3×2)(2×1)
好耶!可以啦!最后会得到三行一列的矩阵
于是:
发现了吗!长得跟输入的矩阵一样!
至此,梯度的反向传播完成!
来写代码~~
Layer的backward
def backward(self, output_gradient, learning_rate):
# dhaty_dz = self.leaky_ReLU_derivative(self.z)
if self.activation == "sigmoid":
dhaty_dz = self.sigmoid_derivative(self.z)
else:
dhaty_dz = self.leaky_ReLU_derivative(self.z)
dL_dhaty = output_gradient
delta = dL_dhaty * dhaty_dz
dL_dW = delta @ self.x.T #.T转置矩阵
dL_db = delta
#在更新梯度前算出Loss对贡献的影响
dL_dx = self.W.T @ delta
#梯度下降
self.W -= learning_rate * dL_dW
self.b -= dL_db * learning_rate
return dL_dx
Network的Backward
def backward(self, output_gradient, learning_rate):
gradient = output_gradient
# hhh我上次写竟然忘了reversed这个函数
for layer in reversed(self.layers):
gradient = layer.backward(gradient, learning_rate)
return gradient
按照上次定的神经元分布,实例化神经网络
这里我还踩了个坑,
ValueError: non-broadcastable output operand with shape (1,1) doesn't match the broadcast shape (1,2)因为我还是用的python默认的列表,没有用
numpy的矩阵需要转换一下
Layer的forwarddef forward(self, x): x = np.asarray(x, dtype=float).reshape(-1, 1) #转换为numpy矩阵,不管有几个元素,都给我整理成n行1列 self.x = x #依旧保存输入 # 按照咱们的矩阵乘法公式,numpy里使用@进行矩阵乘法 self.z = self.W @ x + self.b #激活函数 self.haty = self.leaky_ReLU(self.z) return self.haty
实例化:
train_data = [
(1,1,0),
(0,1,1),
(1,0,1),
(0,0,0)
]
network = Network([
Layer(2, 2),
Layer(3, 2),
Layer(2, 3),
Layer(1, 2)
])
for epoch in range(20000):
for simple in range(len(train_data)):
y = train_data[simple][2]
haty = network.forward([train_data[simple][0],train_data[simple][1]])
# 因为forward最后返回的是每个神经元的输出,然后最后一层就一个神经元,所以用[0]
dL_dhaty = 2*(haty[0] - y)
network.backward(dL_dhaty,0.01)
print(network.forward([train_data[0][0], train_data[0][1]]))
print(network.forward([train_data[1][0], train_data[1][1]]))
print(network.forward([train_data[2][0], train_data[2][1]]))
print(network.forward([train_data[3][0], train_data[3][1]]))
训练后结果:
[[-0.00797035]]
[[-0.00797016]]
[[-0.00797033]]
[[-0.00797034]]
omg现在需要用Sigmoid附属给输出层了,跟上一篇博客一样
class Layer:
def __init__(self, neuron_num, weights_num, activation="relu"):
# 初始化神经元矩阵,random.randn是生成行x列的矩阵,里面的数值随机
# 因为每个输入要和每个权重点乘,又因为矩阵乘法是行点乘列,所以每个神经元的每个权重存在矩阵的行中
# 然后每行里的每个权重就可以和输入(看做多行1列的矩阵)点乘啦~
self.W = np.random.randn(neuron_num, weights_num)
# 创建一个neuron_num行,1列的矩阵,也就是咱们的偏置矩阵!
# 偏置矩阵都填充0就好,偏置初始值设置为0更高效
self.b = np.zeros((neuron_num,1))
self.activation = activation #激活函数可以设定模式了
def leaky_ReLU(self, x):
# np.where可以理解为批量1f
# np.where(条件, 条件成立时的值, 条件不成立时的值)
return np.where(x > 0, x, 0.01*x)
def leaky_ReLU_derivative(self, z):
return np.where(z > 0, 1, 0.01)
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(self, x):
s = self.sigmoid(x)
return s * (1 - s)
def forward(self, x):
x = np.asarray(x, dtype=float).reshape(-1, 1) #转换为numpy矩阵,不管有几个元素,都给我整理成n行1列
self.x = x #依旧保存输入
# 按照咱们的矩阵乘法公式,numpy里使用@进行矩阵乘法
self.z = self.W @ x + self.b
#激活函数
if self.activation == "sigmoid":
self.haty = self.sigmoid(self.z)
else:
self.haty = self.leaky_ReLU(self.z)
return self.haty
def backward(self, output_gradient, learning_rate):
# dhaty_dz = self.leaky_ReLU_derivative(self.z)
if self.activation == "sigmoid":
dhaty_dz = self.sigmoid_derivative(self.z)
else:
dhaty_dz = self.leaky_ReLU_derivative(self.z)
dL_dhaty = output_gradient
delta = dL_dhaty * dhaty_dz
dL_dW = delta @ self.x.T #.T转置矩阵
dL_db = delta
#在更新梯度前算出Loss对贡献的影响
dL_dx = self.W.T @ delta
#梯度下降
self.W -= learning_rate * dL_dW
self.b -= dL_db * learning_rate
return dL_dx
网络实例化:
network = Network([
Layer(2, 2),
Layer(3, 2),
Layer(2, 3),
Layer(1, 2, activation="sigmoid")
])
训练结果:
[[0.00244575]]
[[0.98330621]]
[[0.98220073]]
[[0.00799492]]
实测下来成功率还是稍低,之后会好好学习初始化的!
那今天就先到这里啦~祝贺99mlp终于升级为了矩阵版!!!!
评论(4)