简介
本文展示了小型人工神经网络 (NN) 如何表示基本功能。其目的是提供有关 NN 工作原理的基本直觉,并作为对机制可解释性的温和介绍--机制可解释性是一个寻求对 NN 进行逆向工程的领域。
我将举出三个基本函数的例子,用简单的算法来描述每个函数,并展示如何将算法 “编码 ”到神经网络的权重中。然后,我将探讨神经网络能否利用反向传播学习算法。我鼓励读者把每个例子当作一个谜语,并在阅读解答之前花上一分钟。
三个基本函数
在以下所有示例中,我使用术语 “神经元 ”来表示神经网络计算图中的单个节点。每个神经元只能使用一次(没有循环;例如,不是 RNN),并按以下顺序执行 3 个操作:
我只提供极少量的代码片段,以便阅读流畅。
< 运算符
学习函数 “x < 10 ”需要多少个神经元?编写一个神经元网络,当输入小于 10 时返回 1,否则返回 0。
解决方案
首先,让我们按照我们想要学习的模式创建样本数据集
X = [[i] for i in range(-20, 40)]
Y = [1 if z[0] < 10 else 0 for z in X]
这项分类任务可以使用逻辑回归和 Sigmoid 作为输出激活来解决。b 是偏置项,可视为神经元的阈值。直观地说,我们可以设置 b = 10 和 a = -1 得到 F=Sigmoid(10-x)
让我们用 PyTorch 来实现并运行 F
model = nn.Sequential(nn.Linear(1,1), nn.Sigmoid())
d = model.state_dict()
d["0.weight"] = torch.tensor([[-1]]).float()
d['0.bias'] = torch.tensor([10]).float()
model.load_state_dict(d)
y_pred = model(x).detach().reshape(-1)
似乎是正确的模式,但我们能否做一个更严格的近似值?例如,F(9.5) = 0.62,我们希望它更接近 1。
对于 Sigmoid 函数,当输入接近 -∞ / ∞ 时,输出分别接近 0 / 1。因此,我们需要让我们的 10 - x 函数返回大数,这可以通过乘以一个更大的数来实现,比如 100,得到 F=Sigmoid(100(10-x)),现在我们将得到 F(9.5) =~1。
事实上,当训练一个只有一个神经元的网络时,它会收敛到 F=Sigmoid(M(10-x)),其中 M 是一个标量,它在训练过程中会不断增长,以使近似更紧密。
需要说明的是,我们的单神经元模型只是“<10 ”函数的近似值。我们永远无法达到零损失,因为神经元是一个连续函数,而“<10 ”不是一个连续函数。
最小值(a,b)
编写一个神经网络,获取两个数字并返回它们之间的最小值。
解决方案
和之前一样,我们先创建一个测试数据集,并将其可视化
X_2D = [
[random.randrange(-50, 50),
random.randrange(-50, 50)]
for i in range(1000)
]
Y = [min(a, b) for a, b in X_2D]
在这种情况下,ReLU 激活是一个很好的选择,因为它本质上是一个最大值函数(ReLU(x) = max(0,x))。事实上,使用 ReLU 可以将最小函数写成如下形式
min(a, b) = 0.5 (a + b -|a - b|) = 0.5 (a + b - ReLU(b - a) - ReLU(a - b))
[等式 1]
现在,让我们构建一个能够学习等式 1 的小型网络,并尝试使用梯度下降法对其进行训练
class MinModel(nn.Module):
def __init__(self):
super(MinModel, self).__init__()
# For ReLU(a-b)
self.fc1 = nn.Linear(2, 1)
self.relu1 = nn.ReLU()
# For ReLU(b-a)
self.fc2 = nn.Linear(2, 1)
self.relu2 = nn.ReLU()
# Takes 4 inputs
# [a, b, ReLU(a-b), ReLU(b-a)]
self.output_layer = nn.Linear(4, 1)
def forward(self, x):
relu_output1 = self.relu1(self.fc1(x))
relu_output2 = self.relu2(self.fc2(x))
return self.output_layer(
torch.cat(
(x, Relu_output1, relu_output2),
dim=-1
)
)
训练 300 个历元就足以收敛。让我们看看模型的参数
>> for k, v in model.state_dict().items():
>> print(k, ": ", torch.round(v, decimals=2).numpy())
fc1.weight : [[-0. -0.]]
fc1.bias : [0.]
fc2.weight : [[ 0.71 -0.71]]
fc2.bias : [-0.]
output_layer.weight : [[ 1. 0. 0. -1.41]]
output_layer.bias : [0.]
许多权重都归零了,剩下的是看起来很不错的
model([a,b]) = a - 1.41 * 0.71 ReLU(a-b) ≈ a - ReLU(a-b)
这不是我们预期的解法,但它是一个有效的解法,甚至比公式 1 更简洁!通过观察网络,我们发现了一个新的漂亮公式!证明:
证明:
是偶数吗?
创建一个神经网络,将整数 x 作为输入,并返回 x mod 2。也就是说,如果 x 是偶数,则返回 0;如果 x 是奇数,则返回 1。
这个问题看起来很简单,但令人惊讶的是,要创建一个有限大小的网络,并能正确分类(-∞,∞)中的每个整数(使用标准的非周期性激活函数,如 ReLU),是不可能的。
定理:is_even 至少需要对数神经元
使用 ReLU 激活函数的网络至少需要 n 个神经元才能将 2^n 个连续自然数中的每个数正确地分类为偶数或奇数(即求解 is_even)。
证明: 使用归纳法
基础:n == 2:直观地说,单个神经元(ReLU(ax + b)形式)无法求解 S = [i + 1, i + 2, i + 3, i + 4],因为它不是线性可分的。例如,在不失一般性的前提下,假设 a > 0 且 i + 2 为偶数。如果 ReLU(a(i + 2) + b) = 0,那么 ReLU(a(i + 1) + b) = 0(单调函数),但 i + 1 是奇数。
假设为 n,再看 n+1: 让 S = [i + 1, ..., i + 2^(n+1)],为了避免矛盾,假设 S 可以用 n 大小的网络求解。取第一层的一个输入神经元 f(x) = ReLU(ax+b),其中 x 是网络的输入。根据 ReLU 的定义,存在一个这样的 j:
S' = [i + 1, ..., i + j], S'' = [i + j + 1, ..., i + 2^(n + 1)] S'' = [i + j + 1, ..., i + 2^(n + 1)].
f(x ≤ i) = 0
f(x ≥ i) = ax + b
有两种情况需要考虑:
对数算法
多少个神经元足以对 [1, 2^n] 进行分类?我已经证明了 n 个神经元是必要的。接下来,我将证明 n 个神经元也足够了。
一个简单的实现方法是一个网络不断加减 2,并检查是否在某一点上达到 0。更有效的算法是加减 2 的幂次,这只需要 O(n) 个神经元。更正式的算法是:
f_i(x) := |x - i|
f(x) := f_1∘ f_1∘ f_2 ∘ f_4∘ ... ∘ f_(2^(n-1)) (|x|)
证明:
算法实现
让我们尝试在一个小范围内使用神经网络来实现这一算法。我们从定义数据开始。
X = [[i] for i in range(0, 16)]
Y = [z[0] % 2 for z in X]
由于域包含 2⁴ 个整数,我们需要使用 6 个神经元。5 个神经元用于 f_1∘ f_1∘ f_2 ∘ f_4∘ f_8, + 1 个输出神经元。让我们构建网络并硬连接权重
def create_sequential_model(layers_list = [1,2,2,2,2,2,1]):
layers = []
for i in range(1, len(layers_list)):
layers.append(nn.Linear(layers_list[i-1], layers_list[i]))
layers.append(nn.ReLU())
return nn.Sequential(*layers)
# This weight matrix implements |ABS| using ReLU neurons.
# |x-b| = Relu(-(x-b)) + Relu(x-b)
abs_weight_matrix = torch_tensor([[-1, -1],
[1, 1]])
# Returns the pair of biases used for each of the ReLUs.
get_relu_bias = lambda b: torch_tensor([b, -b])
d = model.state_dict()
d['0.weight'], d['0.bias'] = torch_tensor([[-1],[1]]), get_relu_bias(8)
d['2.weight'], d['2.bias'] = abs_weight_matrix, get_relu_bias(4)
d['4.weight'], d['4.bias'] = abs_weight_matrix, get_relu_bias(2)
d['6.weight'], d['6.bias'] = abs_weight_matrix, get_relu_bias(1)
d['8.weight'], d['8.bias'] = abs_weight_matrix, get_relu_bias(1)
d['10.weight'], d['10.bias'] = torch_tensor([[1, 1]]), torch_tensor([0])
model.load_state_dict(d)
model.state_dict()
不出所料,我们可以看到该模型对 [0,15] 进行了完美预测
而且,不出所料,它并不能概括新的数据点
我们看到,我们可以硬连接模型,但使用梯度下降法,模型会收敛到相同的解吗?
答案是--没那么容易!相反,它陷入了局部最小值--预测平均值。
这是一个已知的现象,梯度下降会卡在局部最小值。对于高度非线性函数(如 is_even)的非光滑误差曲面,这种现象尤其普遍。
结论
希望本文能帮助你了解小型神经网络的基本结构。分析大型语言模型要复杂得多,但这是一个进展迅速、充满挑战的研究领域。
在使用大型语言模型时,我们很容易将注意力集中在提供数据和计算能力上,以获得令人印象深刻的结果,而不去了解它们是如何运行的。然而,可解释性提供了至关重要的见解,有助于解决公平性、包容性和准确性等问题。