# 第七章——前馈神经网络

<img src="../assets/image-20240424204837275.png" alt="前馈神经网络" width="550" />

### 前言

在[A Neural Network Playground](https://playground.tensorflow.org/)这个网址玩过的应该对神经网络有了基本的了解，大部分情况下，随着层数跟神经元的增加（或者特征x的增加），结果一般也会变好，即正相关，但同时也意味着更多的资源投入等。我们对神经网络这块讲的会比较简单，因为更底层的原理短时间无法讲明白，大家了解稍微深一点即可。



### GPT-2里的前馈神经网络

[源代码](https://github.com/openai/gpt-2/blob/master/src/model.py)如下。需要看的点击前面链接跳转

~~~python
def conv1d(x, scope, nf, *, w_init_stdev=0.02):
    with tf.variable_scope(scope):
        *start, nx = shape_list(x)
        w = tf.get_variable('w', [1, nx, nf], initializer=tf.random_normal_initializer(stddev=w_init_stdev))  # 训练中更新的权重w
        b = tf.get_variable('b', [nf], initializer=tf.constant_initializer(0))  # 训练中更新的偏值项b
        c = tf.reshape(tf.matmul(tf.reshape(x, [-1, nx]), tf.reshape(w, [-1, nf]))+b, start+[nf])
        return c
      
def mlp(x, scope, n_state, *, hparams):
    with tf.variable_scope(scope):
        nx = x.shape[-1].value
        h = gelu(conv1d(x, 'c_fc', n_state))  # 第一层是一个线性变换，后面跟着一个GELU激活函数
        h2 = conv1d(h, 'c_proj', nx)  # 二层是另一个线性变换，将数据从隐藏层的维度映射回原始维度
        return h2
~~~

可以看到上面是非常简单的两层线性变换，而且没有其它隐藏层。

前馈神经网络（FFNN） 在 Transformer 中的作用是为了引入非线性并增加模型的表达能力。多头注意力机制虽然能够捕捉输入序列中的长距离依赖关系，但它本身是一个线性操作。FFNN 通过在注意力机制之后添加非线性变换，使得模型能够学习更复杂的特征表示。



### 神经网络demo

我们来手动推一个简单的神经网络并更新权重，用一个单层神经网络做例子。这个网络将只有一个输入、一个输出和一个权重（没有偏值项b）。我们将使用均方误差作为损失函数，并通过梯度下降来更新权重。

~~~markdown
输入 (x)  输出 (y)
   1       2
   2       4
   3       6
~~~

我们的目标是训练一个模型来预测输出`y`，给定输入`x`。我们的模型是一个简单的单变量线性回归模型：`y_pred = w * x`。

初始化权重 `w` 为 0.5，学习率 `lr` 为 0.1。我们将通过编写代码模拟进行3次迭代的权重更新。

#### 迭代 1:

1. **前向传播**：计算预测值

   ```
   y_pred = w * x = 0.5 * 1 = 0.5  (对于第一个样本)
   ```

2. **计算损失**：使用均方误差

   ```
   loss = (y_pred - y)^2 = (0.5 - 2)^2 = 2.25
   ```

   > 这时候可以看到损失是2.25，一个很大的值，神经网络的最终目的是要降为无限接近于0甚至0（当然一般是达不到0 的，达到的情况下只有可能是错了的）。

3. **反向传播**：计算损失关于权重w的梯度

   ```
   dloss/dw = 2 * (y_pred - y) * x = 2 * (0.5 - 2) * 1 = -3.0
   ```

   > 我们需要通过反向传播来更新权重。

4. **更新权重**：

   ```
   w = w - lr * dloss_dw = 0.8 - 0.1 * -3.0 = 0.8
   ```

#### 迭代 2:

重复上述步骤，使用更新后的权重：

1. **前向传播**：

   ```
   y_pred = w * x = 0.8 * 1 = 0.8
   ```

2. **计算损失**：

   ```
   loss = (y_pred - y)^2 = (0.8 - 2)^2 = 1.44
   ```

   > 可以看到loss值下降了，2.25下降为1.44，但是还不够，我们的目标是无限接近0。

3. **反向传播**：

   ```
   dloss/dw = 2 * (y_pred - y) * x = 2 * (0.8 - 2) * 1 = -2.4
   ```

4. **更新权重**：

   ```
   w = w - lr * dloss_dw = 0.8 - 0.1 * -2.4 = 1.04
   ```

#### 迭代 3:

重复上述步骤，使用更新后的权重：

1. **前向传播**：

   ```
   y_pred = w * x = 1.04 * 1 = 1.04
   ```

   > 预测结果从0.5到0.8再到1.04，逐步接近2这个正确的值。

2. **计算损失**：

   ```
   loss = (y_pred - y)^2 = (1.04 - 2)^2 = 0.9216
   ```

   > 可以看到loss又一次下降了，也就是只要我们反复循环，那么最终的loss值，一定能无限接近于0。

3. **反向传播**：

   ```
   dloss/dw = 2 * (y_pred - y) * x = 2 * (1.04 - 2) * 1 = -1.92
   ```

4. **更新权重**：

   ```
   w = w - lr * dloss_dw = 1.04 - 0.1 * -1.92 = 1.232
   ```

在这个非常简单的例子中，我们可以看到权重`w`在每次迭代后都在逐渐增加（直到loss无限接近0），以减少预测值`y_pred`和真实值`y`之间的差异。在实际应用中，我们会使用所有样本来计算损失和梯度，并可能使用更复杂的网络结构和优化算法。但这个例子展示了神经网络权重更新的基本原理。

代码如下，可以自由改变`lr`值和`Epoch`值，来观察`loss`和`y_pred`

~~~python
w = 0.5
lr = 0.1
x = 1
y = 2
Epoch = 3

for i in range(Epoch):
    print(f"=========")
    print(f"迭代{i+1}:")
    y_pred = w * x
    print("y_pred =", y_pred)
    loss = (y_pred - y)**2
    print("loss =", loss)
    dloss_dw = 2 * (y_pred - y) * x
    print("loss =", dloss_dw)
    w = w - lr * dloss_dw
    print("w =", w)
~~~

**简单的神经网络等价于线性回归**，想要深入了解的可以看[线性回归原理](https://github.com/ben1234560/AiLearning-Theory-Applying/blob/53ad238b5b7dbb5c39520401de2f10208825e4f9/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AE%97%E6%B3%95%E5%8E%9F%E7%90%86%E5%8F%8A%E6%8E%A8%E5%AF%BC/%E7%AC%AC%E4%B8%80%E7%AB%A0%E2%80%94%E2%80%94%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92%E5%8E%9F%E7%90%86.md)

### 总结

在GPT-2中，前馈神经网络由两层线性变换组成，第一层线性变换后应用了GELU激活函数以引入非线性。FFNN在Transformer架构中紧随多头注意力层之后，其目的是增强模型的表达能力，使其能够捕捉更复杂的特征表示。通过手动迭代一个简单的单层神经网络示例，我们展示了权重更新的基本过程：前向传播计算预测值，计算损失函数，通过反向传播求梯度，最后使用梯度下降法更新权重。这个过程在多次迭代中重复，目标是最小化损失函数，从而训练出能够更准确地预测输出的模型。虽然这是一个简化的例子，但它揭示了深度学习模型训练的核心机制。

