Hakula

PRML Lab 2: FNN 模型 实验报告
Pattern Recognition and Machine Learning @ Fudan Universi...
扫描右侧二维码阅读全文
02
2021/05

PRML Lab 2: FNN 模型 实验报告

Pattern Recognition and Machine Learning @ Fudan University, spring 2021.

封面:「封鎖」/「SWAV」

PRML Lab 2: FNN 模型 实验报告

本次作业完成了选题 1 的实验内容,利用 NumPy 实现了一个 FNN 模型,并在 MNIST 数据集上进行了训练。

如需参考请务必注明 Reference

仅供参考。

实验简介

See: README

实验报告

FNN 算子的反向传播

1. Matmul

1.1 公式推导

输入一个 $n\times d$ 的矩阵 $X$ 和一个 $d\times d'$ 的矩阵 $W$,算子 Matmul 的正向传播公式为:

$$
Y = X\times W \tag{1.1}
$$

输出一个 $n\times d'$ 的矩阵 $Y$。

对于梯度的反向传播,有

$$
\nabla{z} = (
\frac{\partial z}{\partial X}_{n\times d},
\frac{\partial z}{\partial W}_{d\times d'}
) \tag{1.2}
$$

我们利用向量化(vectorization)进行对矩阵求导的求解:

$$
\begin{split}
\mathit{vec}(\frac{\partial z}{\partial X}_{n\times d})
&= {(\frac{\partial Y}{\partial X})^T}_{nd\times {nd'}}
\times {\mathit{vec}(\frac{\partial z}{\partial Y})}_{nd'} \\
&= {({\frac
{\partial {\mathit{vec}(X\times W)}_{nd'}}
{\partial {\mathit{vec}(X)}_{nd}}
})^T}_{nd\times {nd'}}
\times {\mathit{vec}(\frac{\partial z}{\partial Y})}_{nd'} \\
&= {({\frac
{\partial (
{\mathit{vec}(X)}_{nd}
\times {(I_n\otimes W)}_{nd\times {nd'}}
)}
{\partial {\mathit{vec}(X)}_{nd}}
})^T}_{nd\times {nd'}}
\times {\mathit{vec}(\frac{\partial z}{\partial Y})}_{nd'} \\
&= {(I_n\otimes W)}_{nd\times {nd'}}
\times {\mathit{vec}(\frac{\partial z}{\partial Y})}_{nd'} \\
&= \mathit{vec}({(\frac{\partial z}{\partial Y}\times W^T)}_{n\times d})
\end{split} \tag{1.3}
$$

这里 $\mathit{vec}(X_{m\times n})$ 表示向量

$$
\begin{split}
[&x_{11}, x_{12},\ &..., x_{1n}, \\
&x_{21}, x_{22},\ &..., x_{2n}, \\
&&... \\
&x_{m1}, x_{m2},\ &..., x_{mn}]
\end{split}
\tag{1.3.1}
$$

$\otimes$ 表示 Kronecker 积,下标表示矩阵或向量的维度。

因此有

$$
\frac{\partial z}{\partial X}_{n\times d}
= {(\frac{\partial z}{\partial Y}\times W^T)}_{n\times d} \tag{1.4}
$$

类似 $(1.3)$ 的推导,同理可得

$$
\frac{\partial z}{\partial W}_{d\times d'}
= {(X^T\times \frac{\partial z}{\partial Y})}_{d\times d'} \tag{1.5}
$$

1.2 代码实现
# numpy_fnn.py

class Matmul(NumpyOp):
    '''
    Matrix multiplication unit.
    '''

    def forward(self, x: np.ndarray, w: np.ndarray) -> np.ndarray:
        '''
        Args:
            x: shape(N, d)
            w: shape(d, d')

        Returns:
            shape(N, d')
        '''

        self.memory['x'] = x
        self.memory['w'] = w
        return np.matmul(x, w)

    def backward(self, grad_y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        '''
        Args:
            grad_y: shape(N, d')

        Returns:
            grad_x: shape(N, d)
            grad_w: shape(d, d')
        '''

        x: np.ndarray = self.memory['x']
        w: np.ndarray = self.memory['w']
        grad_x: np.ndarray = np.matmul(grad_y, w.T)
        grad_w: np.ndarray = np.matmul(x.T, grad_y)
        return grad_x, grad_w

2. Relu

2.1 公式推导

输入一个 $n\times d$ 的矩阵 $X$,对于 $X$ 中的每个元素 $X_{ij}$,算子 Relu 的正向传播公式为:

$$
Y_{ij} = \left\{
\begin{aligned}
&X_{ij}\quad &(X_{ij} > 0) \\
&0\quad &(X_{ij} \le 0)
\end{aligned}
\right.
\tag{2.1}
$$

输出一个 $n\times d$ 的矩阵 $Y$。

对于梯度的反向传播,有

$$
\begin{split}
\nabla{z}
&= \frac{\partial z}{\partial X}_{n\times d} \\
&= \frac{\partial z}{\partial Y}_{n\times d}
\odot \frac{\partial Y}{\partial X}_{n\times d}
\end{split} \tag{2.2}
$$

这里 $\odot$ 表示 Hadamard 积,即逐元素(element-wise)乘积。

其中,对于 $\frac{\partial Y}{\partial X}_{n\times d}$ 中的每个元素 ${Y'}_{ij}$,由 $(2.1)$ 有

$$
{Y'}_{ij} = \left\{
\begin{aligned}
&1\quad &(X_{ij} > 0) \\
&0\quad &(X_{ij} \le 0)
\end{aligned}
\right.
\tag{2.3}
$$

因此,对于 $\frac{\partial z}{\partial X}_{n\times d}$ 中的每个元素 ${Z'}_{ij}$,令 ${Z_Y}' = \frac{\partial z}{\partial Y}_{n\times d}$,由 $(2.2)$ 有

$$
{Z'}_{ij} = \left\{
\begin{aligned}
&{({Z_Y}')}_{ij}\quad &(X_{ij} > 0) \\
&0\quad &(X_{ij} \le 0)
\end{aligned}
\right.
\tag{2.4}
$$

2.2 代码实现
# numpy_fnn.py

class Relu(NumpyOp):
    '''
    Rectified Linear Unit.
    '''

    def forward(self, x: np.ndarray) -> np.ndarray:
        '''
        Args:
            x: shape(N, d)

        Returns:
            shape(N, d)
        '''

        self.memory['x'] = x
        return np.where(x > 0, x, 0)

    def backward(self, grad_y: np.ndarray) -> np.ndarray:
        '''
        Args:
            grad_y: shape(N, d)

        Returns:
            shape(N, d)
        '''

        x: np.ndarray = self.memory['x']
        return np.where(x > 0, grad_y, 0)

3. Log

3.1 公式推导

输入一个 $n\times d$ 的矩阵 $X$,对于 $X$ 中的每个元素 $X_{ij}$,算子 Log 的正向传播公式为:

$$
Y_{ij} = \log{X_{ij}} \tag{3.1}
$$

输出一个 $n\times d$ 的矩阵 $Y$。

对于梯度的反向传播,有

$$
\begin{split}
\nabla{z}
&= \frac{\partial z}{\partial X}_{n\times d} \\
&= \frac{\partial z}{\partial Y}_{n\times d}
\odot \frac{\partial Y}{\partial X}_{n\times d}
\end{split} \tag{3.2}
$$

其中,对于 $\frac{\partial Y}{\partial X}_{n\times d}$ 中的每个元素 ${Y'}_{ij}$,由 $(3.1)$ 有

$$
{Y'}_{ij} = \frac{1}{X_{ij}} \tag{3.3}
$$

因此,对于 $\frac{\partial z}{\partial X}_{n\times d}$ 中的每个元素 ${Z'}_{ij}$,令 ${Z_Y}' = \frac{\partial z}{\partial Y}_{n\times d}$,由 $(3.2)$ 有

$$
{Z'}_{ij} = \frac{{({Z_Y}')}_{ij}}{X_{ij}} \tag{3.4}
$$

3.2 代码实现

为了防止 $X_{ij} = 0$ 时出现 $\log{X_{ij}} = -\infty$ 导致溢出,这里我们给 $X_{ij}$ 附加了一个 $\epsilon = 10^{-12}$ 的修正。

# numpy_fnn.py

class Log(NumpyOp):
    '''
    Natural logarithm unit.
    '''

    def forward(self, x: np.ndarray) -> np.ndarray:
        '''
        Args:
            x: shape(N, d)

        Returns:
            shape(N, d)
        '''

        self.memory['x'] = x
        return np.log(x + self.epsilon)

    def backward(self, grad_y: np.ndarray) -> np.ndarray:
        '''
        Args:
            grad_y: shape(N, d)

        Returns:
            shape(N, d)
        '''

        x: np.ndarray = self.memory['x']
        return grad_y / x

4. Softmax

4.1 公式推导

输入一个 $n\times d$ 的矩阵 $X$,对于 $X$ 中的每个元素 $X_{ij}$,算子 Softmax 的正向传播公式为:

$$
Y_{ij} = \frac{e^{X_{ij}}}{\sum\limits_{k=1}^d e^{X_{ik}}} \tag{4.1}
$$

输出一个 $n\times d$ 的矩阵 $Y$。

对于梯度的反向传播,有

$$
\begin{split}
\nabla{z}
&= \frac{\partial z}{\partial X}_{n\times d}
\end{split} \tag{4.2}
$$

其中,对于 $\frac{\partial z}{\partial X}_{n\times d}$ 中的每个元素 ${Z'}_{ij}$ 有

$$
\begin{split}
{Z'}_{ij}
&= \frac{\partial z}{\partial X_{ij}} \\
&= {(\frac{\partial z}{\partial Y_i})}_d
\cdot {(\frac{\partial Y_i}{\partial X_{ij}})}_d \\
&= \frac{\partial z}{\partial Y_{ij}}
\cdot \frac{\partial Y_{ij}}{\partial X_{ij}} +
\sum\limits_{k=1,\,k\ne j}^d
\frac{\partial z}{\partial Y_{ik}}
\cdot \frac{\partial Y_{ik}}{\partial X_{ij}} \\
&= \frac{\partial z}{\partial Y_{ij}}
\cdot \frac{\partial}{\partial X_{ij}}(
\frac{e^{X_{ij}}}{\sum\limits_{t=1}^d e^{X_{it}}}
) +
\sum\limits_{k=1,\,k\ne j}^d
\frac{\partial z}{\partial Y_{ik}}
\cdot \frac{\partial}{\partial X_{ij}}(
\frac{e^{X_{ik}}}{\sum\limits_{t=1}^d e^{X_{it}}}
) \\
&= \frac{\partial z}{\partial Y_{ij}}
\cdot \frac{e^{X_{ij}}}{\sum\limits_{t=1}^d e^{X_{it}}}
\cdot (1 - \frac{e^{X_{ij}}}{\sum\limits_{t=1}^d e^{X_{it}}}) -
\sum\limits_{k=1,\,k\ne j}^d
\frac{\partial z}{\partial Y_{ik}}
\cdot \frac{e^{X_{ij}}}{\sum\limits_{t=1}^d e^{X_{it}}}
\cdot \frac{e^{X_{ik}}}{\sum\limits_{t=1}^d e^{X_{it}}} \\
&= \frac{\partial z}{\partial Y_{ij}}\cdot Y_{ij}\cdot (1 - Y_{ij}) -
\sum\limits_{k=1,\,k\ne j}^d
\frac{\partial z}{\partial Y_{ik}}\cdot Y_{ij}\cdot Y_{ik} \\
&= Y_{ij}\cdot (
\frac{\partial z}{\partial Y_{ij}} -
\sum\limits_{k=1}^d
\frac{\partial z}{\partial Y_{ik}}\cdot Y_{ik}
)
\end{split}
\tag{4.3}
$$

4.2 代码实现
# numpy_fnn.py

class Softmax(NumpyOp):
    '''
    Softmax over last dimension.
    '''

    def forward(self, x: np.ndarray) -> np.ndarray:
        '''
        Args:
            x: shape(N, d)

        Returns:
            shape(N, d)
        '''

        y: np.ndarray = np.exp(x) / np.exp(x).sum(axis=1)[:, None]
        self.memory['y'] = y
        return y

    def backward(self, grad_y: np.ndarray) -> np.ndarray:
        '''
        Args:
            grad_y: shape(N, d)

        Returns:
            shape(N, d)
        '''

        y: np.ndarray = self.memory['y']
        return y * (grad_y - (grad_y * y).sum(axis=1)[:, None])

函数 mini_batch 实现

我们使用 NumPy 重写了函数 mini_batch,用于之后的训练。

# numpy_mnist.py

def mini_batch(dataset: List[Tuple[Any, int]], batch_size=128) -> np.ndarray:
    '''
    Split the data and labels from the given dataset into batches.

    Args:
        dataset: the given dataset
        batch_size: the size of retrieved data

    Returns:
        Batches of [data, labels] pair.
    '''

    data: np.ndarray = np.array([np.array(pair[0]) for pair in dataset])
    labels: np.ndarray = np.array([pair[1] for pair in dataset])

    # Shuffle the dataset
    size: int = len(dataset)
    indices: np.ndarray = np.arange(size)
    np.random.shuffle(indices)

    batches: List[Tuple[np.ndarray, np.ndarray]] = []
    for i in range(0, size, batch_size):
        chunk: np.ndarray = indices[i:i+batch_size]
        batches.append((data[chunk], labels[chunk]))
    return batches

实验过程与结果

执行以下命令开始训练。

python ./numpy_mnist.py

1. 实验 1

1.1 参数
epoch_number = 3
batch_size = 128
learning_rate = 0.1
1.2 预测准确率
[0] Accuracy: 0.9485
[1] Accuracy: 0.9647
[2] Accuracy: 0.9715
1.3 损失函数值

Loss value 1

2. 实验 2

这次,我们调大训练轮数(epoch_number),观察预测准确率的变化。

2.1 参数
epoch_number = 10
batch_size = 128
learning_rate = 0.1
2.2 预测准确率
[0] Accuracy: 0.9496
[1] Accuracy: 0.9600
[2] Accuracy: 0.9675
[3] Accuracy: 0.9761
[4] Accuracy: 0.9755
[5] Accuracy: 0.9775
[6] Accuracy: 0.9791
[7] Accuracy: 0.9795
[8] Accuracy: 0.9781
[9] Accuracy: 0.9810

可见,训练轮数越多,预测准确率越高,但到达一定准确率后开始波动,不再明显上升。

2.3 损失函数值

Loss value 2

可见,训练轮数越多,损失函数值越低,波动越小。

3. 实验 3

这次,我们调大批处理时每批数据大小(batch_size),观察预测准确率的变化。

3.1 参数
epoch_number = 10
batch_size = 256
learning_rate = 0.1
3.2 预测准确率
[0] Accuracy: 0.9253
[1] Accuracy: 0.9477
[2] Accuracy: 0.9522
[3] Accuracy: 0.9651
[4] Accuracy: 0.9680
[5] Accuracy: 0.9731
[6] Accuracy: 0.9755
[7] Accuracy: 0.9766
[8] Accuracy: 0.9766
[9] Accuracy: 0.9790

可见,每批数据的大小越大,训练速度越慢,但最终达到的准确率没有明显变化。

3.3 损失函数值

Loss value 3

可见,每批数据的大小越大,损失函数值整体的波动越小。

4. 实验 4

这次,我们提高学习率(learning_rate),观察预测准确率的变化。

4.1 参数
epoch_number = 10
batch_size = 256
learning_rate = 0.5
4.2 预测准确率
[0] Accuracy: 0.9505
[1] Accuracy: 0.9675
[2] Accuracy: 0.9706
[3] Accuracy: 0.9471
[4] Accuracy: 0.9737
[5] Accuracy: 0.8980
[6] Accuracy: 0.9757
[7] Accuracy: 0.9573
[8] Accuracy: 0.9770
[9] Accuracy: 0.9792

可见,学习率越高,训练速度越快,但训练时的波动也可能较大。

4.3 损失函数值

Loss value 4

可见,学习率越高,损失函数值下降越快,但整体的波动也可能较大。


版权声明:本文为原创文章,版权归 Hakula 所有。

本文链接:https://hakula.xyz/prml/lab_2_fnn.html

本文采用 CC BY-NC-SA 4.0 许可协议 进行许可。

最后修改:2021-07-04
如果我的文章对你有帮助,欢迎赞赏,谢谢你!111

发表评论