多样本向量化前向与后向传播:参考答案

目录

\[ \begin{align}\begin{aligned}\newcommand{\ba}{\boldsymbol{a}} \newcommand{\bb}{\boldsymbol{b}} \newcommand{\be}{\boldsymbol{e}} \newcommand{\bq}{\boldsymbol{q}} \newcommand{\bk}{\boldsymbol{k}} \newcommand{\bw}{\boldsymbol{w}} \newcommand{\bx}{\boldsymbol{x}} \newcommand{\by}{\boldsymbol{y}} \newcommand{\bz}{\boldsymbol{z}} \newcommand{\bd}{\boldsymbol{d}} \newcommand{\bv}{\boldsymbol{v}} \newcommand{\bs}{\boldsymbol{s}}\\\newcommand{\btheta}{\boldsymbol{\theta}} \newcommand{\bbeta}{\boldsymbol{\beta}} \newcommand{\bgamma}{\boldsymbol{\gamma}} \newcommand{\bsigma}{\boldsymbol{\sigma}} \newcommand{\md}{\mbox{d}} \newcommand{\bmu}{\boldsymbol{\mu}} \newcommand{\bone}{\boldsymbol{1}} \newcommand{\bzero}{\boldsymbol{0}} \newcommand{\bepsilon}{\boldsymbol{\epsilon}} \newcommand{\bphi}{\boldsymbol{\phi}} \newcommand{\bh}{\boldsymbol{h}} \newcommand{\bc}{\boldsymbol{c}} \newcommand{\br}{\boldsymbol{r}} \newcommand{\bQ}{\boldsymbol{Q}} \newcommand{\bK}{\boldsymbol{K}} \newcommand{\bV}{\boldsymbol{V}} \newcommand{\bSigma}{\boldsymbol{\Sigma}} \newcommand{\bg}{\boldsymbol{g}} \newcommand{\bxi}{\boldsymbol{\xi}} \newcommand{\bvarepsilon}{\boldsymbol{\varepsilon}} \newcommand{\bdelta}{\boldsymbol{\delta}} \newcommand{\bq}{\boldsymbol{q}} \newcommand{\bk}{\boldsymbol{k}} \newcommand{\bJ}{\boldsymbol{J}} \newcommand{\bp}{\boldsymbol{p}} \newcommand{\bi}{\boldsymbol{i}} \newcommand{\bo}{\boldsymbol{o}} \newcommand{\bE}{\boldsymbol{E}} \newcommand{\bH}{\boldsymbol{H}} \newcommand{\bL}{\boldsymbol{L}} \newcommand{\bu}{\boldsymbol{u}} \newcommand{\bLambda}{\boldsymbol{\Lambda}} \newcommand{\trans}{^{\rm\scriptsize T}} \newcommand{\var}{\mathrm{var}}\\\newcommand{\bA}{\boldsymbol{A}} \newcommand{\bB}{\boldsymbol{B}} \newcommand{\bC}{\boldsymbol{C}} \newcommand{\bD}{\boldsymbol{D}} \newcommand{\bG}{\boldsymbol{G}} \newcommand{\bI}{\boldsymbol{I}} \newcommand{\bM}{\boldsymbol{M}} \newcommand{\bP}{\boldsymbol{P}} \newcommand{\bS}{\boldsymbol{S}} \newcommand{\bU}{\boldsymbol{U}} \newcommand{\bW}{\boldsymbol{W}} \newcommand{\bX}{\boldsymbol{X}} \newcommand{\bY}{\boldsymbol{Y}} \newcommand{\bZ}{\boldsymbol{Z}} \newcommand{\cotp}{\textcolor[RGB]{48,209,88}{TP}} \newcommand{\cotn}{\textcolor[RGB]{100,210,255}{TN}} \newcommand{\cofp}{\textcolor[RGB]{94,92,230}{FP}} \newcommand{\cofn}{\textcolor[RGB]{191,90,242}{FN}}\\\newcommand{\numcotp}{\textcolor[RGB]{48,209,88}{50}} \newcommand{\numcotn}{\textcolor[RGB]{100,210,255}{30}} \newcommand{\numcofp}{\textcolor[RGB]{94,92,230}{10}} \newcommand{\numcofn}{\textcolor[RGB]{191,90,242}{10}} \DeclareMathOperator*{\argmin}{arg\,min}\end{aligned}\end{align} \]

多样本向量化前向与后向传播:参考答案#

返回正文练习 · 返回答案索引

说明#

以下答案与正文7道题逐题对应。推导题给出关键等式;编程题给出可扩展的代码骨架;效率实验必须先核验循环版与向量化版的结果一致。

  1. \(\bA^{[l-1]}=[(\ba_1^{[l-1]})\trans;\ldots;(\ba_n^{[l-1]})\trans]\in\mathbb R^{n\times d^{[l-1]}}\)⁠,且 \(\bW^{[l]}=[(\bw_1^{[l]})\trans;\ldots;(\bw_{d^{[l]}}^{[l]})\trans]\in\mathbb R^{d^{[l]}\times d^{[l-1]}}\)⁠。第 \(i\) 个样本在第 \(l\) 层的线性运算为

    \[z_{ij}^{[l]}=(\bw_j^{[l]})\trans\cdot\ba_i^{[l-1]}+b_j^{[l]}.\]

    把所有 \(i,j\) 同时排列,得到

    \[\bZ^{[l]}=\bA^{[l-1]}\cdot(\bW^{[l]})\trans +\bone\cdot(\bb^{[l]})\trans, \qquad \bA^{[l]}=g^{[l]}(\bZ^{[l]}),\]

    其中 \(\bone\in\mathbb R^{n\times1}\)⁠、\(\bb^{[l]}\in\mathbb R^{d^{[l]}\times1}\)⁠,两项和输出均为 \(n\times d^{[l]}\)⁠。全 1 向量与偏置转置的外积把同一偏置行复制 \(n\) 次;在 NumPy 中写成 Z = A @ W.T + b.reshape(1, -1) 即可由广播完成。

  2. 对平均损失,输出层误差信号已经包含平均因子:

    \[\mathrm d\bZ^{[2]}=\frac1n(\bA^{[2]}-\by).\]

    链式法则随后给出

    \[\begin{split}\begin{aligned} \mathrm d\bW^{[2]}&=(\mathrm d\bZ^{[2]})\trans\cdot\bA^{[1]}, &\mathrm d\bb^{[2]}&=(\mathrm d\bZ^{[2]})\trans\cdot\bone,\\ \mathrm d\bZ^{[1]}&=\mathrm d\bZ^{[2]}\cdot\bW^{[2]} \odot g^{[1]\prime}(\bZ^{[1]}),\\ \mathrm d\bW^{[1]}&=(\mathrm d\bZ^{[1]})\trans\cdot\bX, &\mathrm d\bb^{[1]}&=(\mathrm d\bZ^{[1]})\trans\cdot\bone. \end{aligned}\end{split}\]

    一般地,若 \(\mathrm d\bZ^{[l]}\in\mathbb R^{n\times d^{[l]}}\)⁠,则 \((\mathrm d\bZ^{[l]})\trans\cdot\bA^{[l-1]}\)\(d^{[l]}\times d^{[l-1]}\)⁠,与 \(\bW^{[l]}\) 同形;偏置梯度为 \(d^{[l]}\times1\)⁠。若在 \(\mathrm d\bZ^{[2]}\) 中已经除以 \(n\)⁠,后续参数梯度不得再次除以 \(n\)⁠。

  3. 批量前向公式的第 \((i,j)\) 个元素满足

    \[[\bA^{[l-1]}\cdot(\bW^{[l]})\trans]_{ij}+b_j^{[l]} =\sum_{k=1}^{d^{[l-1]}}a_{ik}^{[l-1]}W_{jk}^{[l]}+b_j^{[l]},\]

    这正是第 \(i\) 个样本、第 \(j\) 个神经元的逐样本公式。对参数梯度同样有

    \[[\mathrm d\bW^{[l]}]_{jk} =\frac1n\sum_{i=1}^n\delta_{ij}^{[l]}a_{ik}^{[l-1]},\]

    即逐样本外积 \(\boldsymbol\delta_i^{[l]}\cdot(\ba_i^{[l-1]})\trans\) 的平均值。这里把 \(1/n\) 写在式中;若它已包含在 \(\boldsymbol\delta_i^{[l]}\) 中,外侧不再出现。若循环计算期间更新了参数,不同样本便使用了不同模型,此时其平均值不再等于同一组参数处的批量梯度。

  4. 直接计算得到

    \[\begin{split}\bZ= \begin{pmatrix}-0.5&3.5\\-0.5&-2.5\end{pmatrix}, \qquad \mathrm d\bW= \begin{pmatrix}-2&2\\-2&4\end{pmatrix}, \qquad \mathrm d\bb=\begin{pmatrix}4\\6\end{pmatrix},\end{split}\]

    以及

    \[\begin{split}\mathrm d\bA= \begin{pmatrix}5&1\\11&1\end{pmatrix}.\end{split}\]

    \(\bZ\)\(\mathrm d\bA\) 均为 \(2\times2\)⁠,\(\mathrm d\bW\)\(\bW\) 同为 \(2\times2\)⁠,\(\mathrm d\bb\)\(\bb\) 同为 \(2\times1\)⁠。本题给定的 \(\mathrm d\bZ\) 可视为已经包含损失中所需的缩放,因此不再额外除以样本数。

  5. 以下骨架使用 sigmoid 隐藏层和二分类输出层:

    import numpy as np
    
    def sigmoid(z):
        return np.exp(-np.logaddexp(0.0, -z))
    
    def forward_backward(X, y, W1, b1, W2, b2):
        X, y = np.asarray(X, float), np.asarray(y, float)
        W1, b1 = np.asarray(W1, float), np.asarray(b1, float)
        W2 = np.asarray(W2, float)
        assert X.ndim == 2 and y.shape == (X.shape[0],)
        assert W1.shape == (b1.size, X.shape[1])
        assert W2.shape == (1, b1.size)
    
        Z1 = X @ W1.T + b1.reshape(1, -1)
        A1 = sigmoid(Z1)
        z2 = (A1 @ W2.T).reshape(-1) + float(b2)
        p = sigmoid(z2)
        loss = np.mean(np.logaddexp(0.0, z2) - y * z2)
    
        dZ2 = (p - y) / X.shape[0]
        dW2 = dZ2[None, :] @ A1
        db2 = dZ2.sum()
        dZ1 = (dZ2[:, None] @ W2) * A1 * (1.0 - A1)
        dW1 = dZ1.T @ X
        db1 = dZ1.sum(axis=0)
        cache = {"Z1": Z1, "A1": A1, "z2": z2, "p": p}
        grads = {"W1": dW1, "b1": db1, "W2": dW2, "b2": db2}
        assert dW1.shape == W1.shape and db1.shape == b1.shape
        assert dW2.shape == W2.shape and np.ndim(db2) == 0
        return loss, cache, grads
    

    提交时还应检查所有返回值是否有限。第 4 题对应的仿射层可单独写成 A @ W.T + b.reshape(1, -1)其后向三式分别为 dZ.T @ AdZ.sum(axis=0)dZ @ W

  6. 循环版必须先在固定参数下完成全部样本的前向与后向传播,再对损失和梯度取平均。可按以下方式核验:

    loop_loss = np.mean(sample_losses)
    loop_grads = {
        name: np.mean(np.stack([g[name] for g in sample_grads]), axis=0)
        for name in sample_grads[0]
    }
    np.testing.assert_allclose(loop_loss, batch_loss,
                               rtol=1e-10, atol=1e-12)
    for name in loop_grads:
        np.testing.assert_allclose(loop_grads[name], batch_grads[name],
                                   rtol=1e-10, atol=1e-12)
    

    \(n>1\) 且梯度非零的数据可发现重复除以 \(n\)⁠:错误结果恰为正确梯度的 \(1/n\)⁠。偏置广播测试应选择 \(n\ne d^{[l]}\)⁠,并断言中间量维度;若恰有 \(n=d^{[l]}\)⁠,错误列向量可能在不报错的情况下产生错误结果。

  7. 单层仿射计算的主要运算量约为 \(O(ndh)\)⁠。循环版与向量化版的渐近运算量相同,但向量化版可调用高度优化的矩阵乘法并减少解释器循环开销。计时应先预先运行,使用同一输入和数据类型重复多次,以中位数或分位数概括,并分开记录纯计算时间与数据准备时间。

    向量化会保存大小约为 \(n\times h\) 的中间矩阵,批量很大时峰值内存可能高于逐样本处理。小问题的单次时间接近计时器分辨率,线程启动、缓存和系统调度会主导结果,因此不能依据一次计时判断优劣。只有在数值和维度核验通过后,效率比较才有意义。