深度学习计算框架实现(代码片段)

xytpai xytpai     2022-12-29     565

关键词:

参考与评述

参考书目《Deep Learning》Lan Goodfellow.
经典的深度学习框架是以计算图&梯度下降方法实现对前馈网络的有监督学习。
这里复现了前馈计算图的梯度计算实现。


一、前馈计算图实现

1. 前向与梯度计算

  • 结果数组 (保存输入节点与计算节点的输出值,能够反映节点在计算方向的拓扑排序)
  • 梯度数组 (保存输入节点与计算节点的梯度,能够反映节点在计算方向的拓扑排序)
  • 连接图 (反映每个节点的父节点)
  • 输出函数集合 (反映每个计算节点如何根据其输入得到输出)
  • 梯度函数集合 (反映每个计算节点如何根据输入和它的梯度计算对其任意父节点的梯度)

即可组成一个完整前馈计算图。
我们以下图所示全连接神经网络为例构建计算图(我们将每个神经元看作一个节点)。

(1)---/(3)---/(5)     X       X      (7)--(8)-->
(2)/---(4)/---(6)/

其中(1)(2)为输入点,(3)(4)(5)(6)为隐层,(7)为输出层,(8)为计算Loss的输出。这样,除了(1)(2)外其它都是计算节点。我们可以为该网络定义一个包含8个元素的数组,以及计算关系。

数组及连接图如下:

//结果数组与梯度数组,我们把输入节点放到计算节点之前
list=[1] [2] [3] [4] [5] [6] [7] [8]
grad=[1] [2] [3] [4] [5] [6] [7] [8]
//父节点集合
Par[1]=null  Par[2]=null
Par[3]=1,2   Par[4]=1,2   //注意由于是前馈网络,Pa(i)=j则i>j
Par[5]=3,4   Par[6]=3,4   
Par[7]=5,6   Par[8]=7

计算节点输出函数集合如下:(以下输出均为标量,输入(x)为向量)

[ Fun(i,x)=sigmoid(b_i+sum_j w_ij*x_j),iin [3,7] Fun(8,x)=0.5 * (x-E)^2 ]

计算节点梯度函数集合如下:
注: dcFun(i,x,p) ,即求节点i在输入x下的输出对其父节点p的输出的偏导数。

[ dcFun(i,x,p)=sigmoid‘(b_i+sum_j w_ij*x_j)*w_ip, i in [3,7] dcFun(8,x,p)=(x-E) ]

先解释下下面所用到的一些方法。

Par[i]->get_output_array(); //返回节点i的所有父节点的输出列表,即i节点输入向量
Par[i]->get_index_array();  //返回节点i的所有父节点的索引列表
L.has_element();            //列表中还有值存在
L.get_next_element();       //返回列表下一个值
put_input_in(list,i,j);     //向list的i到j索引处输入值

前馈实现:

//前馈计算
put_input_in(list,1,2);
for(i=3;i<=8;i++)
    list[i]=Fun(i,Par[i]->get_output_array());

前馈传播之后,为了最小化最终输出list[8] ,即由(8)定义的损失函数输出,我们需要计算每一节点的梯度。
反馈实现:

//反馈梯度计算
for(i=1;i<8;i++) grad[i]=0; //清空梯度数组
grad[8]=1;
for(i=8;i>=3;i--) //迭代每个计算节点,累加其各个父节点梯度

    input=Par[i]->get_output_array();
    par_array=Par[i]->get_index_array();
    while(par_array.has_element()) //迭代本节点的所有父节点
    
        par_index=par_array.get_next_element();
        grad[par_index]+=grad[i]*dcFun(i,input,par_index); 
        //这里dcFun(c,x,p)即求节点c在输入x下的输出对其父节点p的输出的偏导数
    

以计算(4)节点的输出梯度为例。我们先得到其子节点(5)和(6)的梯度。则
(grad[4] = grad[5]*(5对4输出的偏导) + grad[6]*(6对4输出的偏导))
如何求5对4输出的偏导?我们由公式推导。
(out5=sigmoid(out3*w_51+out4*w_52+b_5))
(我们令a5=out3*w_51+out4*w_52+b_5)
(fracpartialout5partialout4=sigmoid‘(a5)*w_52)
这样我们可以得到grad[4]的表达式。
(grad[4] = grad[5]*(sigmoid‘(a5)*w_52) + grad[6]*(sigmoid‘(a6)*w_62))
在反馈计算迭代到(5)(6)节点时,都会累加grad[4]这个值。
我们根据任意节点输出的梯度,以及其输入,就能调整这个节点的一些参数 。

2. 参数更新

在上一步中,我们得到了每个节点的输出,以及Loss对每个节点输出的梯度。
参数可以放在如上所示节点的内部,也可以单独作为一个节点的输出。
如果参数在节点内部:

[ fracpartial Lpartial A_W= fracpartial Lpartial A_out fracpartial A_outpartial A_W ]

由于A的输出对其参数W的导数只由节点A决定,因此这些操作可以在计算所有梯度后并行执行。
如果参数由单个节点定义:

X  --A--->
    /     //参数放在W节点输出中,这样A就只是一个计算形式,需要计算材料X与W
   W      //这种形式在如Tensorflow框架中出现

这种形式中,我们将W当作一个输入节点,这样,在梯度计算时我们的A将会直接算出Loss对于W输出的梯度。


二、全连接神经网络

1. MLP前向计算

一个全连接神经网络(MLP)可以当作一个整体作为计算图中的一个计算节点,它有它的依赖,输出方法,以及求父节点梯度的计算方法,权值更新方法。为了方便易用,也可以每一层当作一个计算节点。(PyTorch)
我们还可以将权值放到某个输入节点中,为了区分它和输入,把它定义成变量节点。(Tensorflow)

Require: 网络深度l

Require: (W_i , iin1,...,l (W_i的每一列表示i层某个神经元的全部权值))

Require: (b_i , iin1,...,l (b_i表示i层各个神经元的偏置))

Require: X,程序输入 (X每一行为一个输入样本,行数为多少批样本,应用SGD)

Require: Y,目标输出 (Y每一行为一个样本对应标签,行数为多少批样本标签)

H_0=X

for k=1:l do

[ A_k=b_k+H_k-1W_k (A_k行数为批次数目,列数为k层神经元数目,这里加法为行行加)H_k=f(A_k) (对A_k逐元素计算非线性函数) ]

end for

$E=H_l $
(L=Loss(E,Y)+lambdaOmega( heta) (lambdaOmega( heta)为正则项,用于优化学习))

2. 对向量偏导的定义:

[ fracpartial y_mpartial x_n= eginpmatrix fracpartial y_1partial x_1 & fracpartial y_1partial x_2 & ... & fracpartial y_1partial x_n fracpartial y_2partial x_1 & fracpartial y_2partial x_2 & ... & fracpartial y_2partial x_n ...&...&...&... fracpartial y_mpartial x_1 & fracpartial y_mpartial x_2 & ... & fracpartial y_mpartial x_n\endpmatrix_m imes n ]

[ fracpartial zpartial y_m=(fracpartial zpartial y_1,fracpartial zpartial y_2,...,fracpartial zpartial y_m)^T ]

[ fracpartial zpartial x_n= (fracpartial y_mpartial x_n)^T(fracpartial zpartial y_m)= eginpmatrix fracpartial y_1partial x_1fracpartial zpartial y_1 +fracpartial y_2partial x_1fracpartial zpartial y_2 +... fracpartial y_1partial x_2fracpartial zpartial y_1 +fracpartial y_2partial x_2fracpartial zpartial y_2 +... ... fracpartial y_1partial x_nfracpartial zpartial y_1 +fracpartial y_2partial x_nfracpartial zpartial y_2 +... endpmatrix_n= eginpmatrix sum_k=1^mfracpartial zpartial y_kfracpartial y_kpartial x_1 \sum_k=1^mfracpartial zpartial y_kfracpartial y_kpartial x_2 ... \sum_k=1^mfracpartial zpartial y_kfracpartial y_kpartial x_n \endpmatrix_n ]

3. 线性单元梯度计算:

已知 (AB=C)(fracpartial Lpartial C=G),求 (fracpartial Lpartial A与fracpartial Lpartial B) :( (L)对某个矩阵(X)的偏导(G)的形式与(X)一模一样)

[ eginpmatrix a_11 & a_12 & a_13 a_21 & a_22 & a_23 \endpmatrix_2 imes 3 imes eginpmatrix b_11 & b_12 b_21 & b_22 b_31 & b_32 \endpmatrix_3 imes 2= eginpmatrix a_11 b_11+a_12 b_21+a_13 b_31 & a_11 b_12+a_12 b_22+a_13 b_32 a_21 b_11+a_22 b_21+a_23 b_31 & a_21 b_12+a_22 b_22+a_23 b_32 \endpmatrix_2 imes 2 ]

[ fracpartial Lpartial A= eginpmatrix g_11 b_11+g_12 b_12 & g_11 b_21+g_12 b_22 & g_11 b_31+g_12 b_32 g_21 b_11+g_22 b_12 & g_21 b_21+g_22 b_22 & g_21 b_31+g_22 b_32 \endpmatrix_2 imes 3=G imes B^T ]

[ fracpartial Lpartial B= eginpmatrix g_11 a_11+g_21 a_21 & g_12 a_11+g_22 a_21 g_11 a_12+g_21 a_22 & g_12 a_12+g_22 a_22 g_11 a_13+g_21 a_23 & g_12 a_13+g_22 a_23 \endpmatrix_3 imes 2=A^T imes G ]

对于偏置:已知 (A+b=C,fracpartial Lpartial C=G)(fracpartial Lpartial A,fracpartial Lpartial b)

[ eginpmatrix a_11 & a_12 & a_13 a_21 & a_22 & a_23 \endpmatrix_2 imes 3+ eginpmatrix b_1 & b_2 & b_3 \endpmatrix = eginpmatrix a_11+b_1 & a_12+b_2 & a_13+b_3 a_21+b_1 & a_22+b_2 & a_23+b_3 \endpmatrix_2 imes 3 ]

[ fracpartial Lpartial A= eginpmatrix g_11 & g_12 & g_13 g_21 & g_22 & g_23 \endpmatrix_2 imes 3=G ]

[ fracpartial Lpartial b= eginpmatrix g_11+g_21 & g_12+ g_22 & g_13+ g_23 \endpmatrix=G的行和 ]

4. 非线性单元梯度计算:

已知 (f(A)=H,fracpartial Lpartial H=G)(fracpartial Lpartial A)

[ H= eginpmatrix f(a_11) & f(a_12) f(a_21) & f(a_22) \endpmatrix_2 imes 2, fracpartial Lpartial A= eginpmatrix g_11f‘(a_11) & g_12f‘(a_12) g_21f‘(a_21) & g_22f‘(a_22) \endpmatrix_2 imes 2= G⊙f‘(A) ]

5. MLP梯度计算:

使用SGD算法,我们的输入是一次一批的,标签也是一次一批。

(Ggets abla_EL) (得到矩阵G,E为最后一层的输出)

for (k=l:1) do :

[ Ggets abla_A_kL=G⊙f‘(a_k) ,(得到对线性单元输出的梯度)\nabla_b_kL=G行和,(得到对偏置的梯度) \nabla_W_k=G imes H_k-1^T ,(得到对权值的梯度)Ggets abla_H_k-1L=W_k^T imes G,(梯度传播到父节点)\]

end for

6. 优化线性单元:

为了增加代码局部性来提高CPU运算速度,我们优化了计算方式。
与之前的矩阵乘法不同,权值矩阵行数为每个样本输出特征个数,列数与输入特征个数相同。

[ H=Linear( eginpmatrix a_00 & a_01 a_10 & a_11 a_20 & a_21 \endpmatrix_3 imes 2 , eginpmatrix w_00 & w_01 w_10 & w_11 w_20 & w_21 w_30 & w_31 \endpmatrix_4 imes 2, eginpmatrix b_0 & b_1 & b_2 & b_3 \endpmatrix ) = eginpmatrix a_00w_00+a_01w_01+b_0 & a_00w_10+a_01w_11+b_1 & a_00w_20+a_01w_21+b_2 & a_00w_30+a_01w_31+b_3 a_10w_00+a_11w_01+b_0 & a_10w_10+a_11w_11+b_1 & a_10w_20+a_11w_21+b_2 & a_10w_30+a_11w_31+b_3 a_20w_00+a_21w_01+b_0 & a_20w_10+a_21w_11+b_1 & a_20w_20+a_21w_21+b_2 & a_20w_30+a_21w_31+b_3\endpmatrix_3 imes 4 ]

[ frac partial Lpartial A= eginpmatrix g_00w_00+g_01w_10+g_02w_20+g_03w_30 &g_00w_01+g_01w_11+g_02w_21+g_03w_31 g_10w_00+g_11w_10+g_12w_20+g_13w_30 &g_10w_01+g_11w_11+g_12w_21+g_13w_31 g_20w_00+g_21w_10+g_22w_20+g_23w_30 &g_20w_01+g_21w_11+g_22w_21+g_23w_31 \endpmatrix=G imes W ]

[ frac partial Lpartial W= eginpmatrix g_00a_00+g_10a_10+g_20a_20 &g_00a_01+g_10a_11+g_20a_21 g_01a_00+g_11a_10+g_21a_20 &g_01a_01+g_11a_11+g_21a_21 g_02a_00+g_12a_10+g_22a_20 &g_02a_01+g_12a_11+g_22a_21 g_03a_00+g_13a_10+g_23a_20 &g_03a_01+g_13a_11+g_23a_21 \endpmatrix =G^T imes A ]

[ frac partial Lpartial b= eginpmatrix g_00+g_10+g_20 & g_01+g_11+g_21 & g_02+g_12+g_22 & g_03+g_13+g_23 \endpmatrix=G列和 ]

7. Batch Normalization:

引入BN层的作用是为了加速学习,试想如下网络:

x   A-->    //二维坐标输入(x,y)经过一个线性变换得到输出(二分类)
y/

如果(x,y)的整个数据集是偏离原点很远的一些点。由于在二分类问题中我们需要找到一条分割不同类点的直线,而初始化的bias(约等于0)表示的直线在靠近原点的地方,因此需要很多步迭代才能达到准确位置。同时,如果这些点非常密集,那么得到的梯度就会非常微小,造成梯度弥散。

  • 我们希望将这些点保留相对位置地移动到原点附近(也相当于将原点移动到这些点中心位置)。
    因此我们就把每个x减去x的平均值,每个y减去y的平均值。

  • 我们还希望让这些点有着归一化的缩放大小(保持相对位置缩放到一个标准窗口内)
    因此我们就在让上述处理后的每个x‘,y‘除以他们各自的均方差根(强行拉成标准正态分布)

  • 还希望在这个归一化标准基础上,增加一点灵活性,以适应非线性函数
    因此我们加上一个乘性可学习参数、一个加性可学习参数。

因此,BN层一般都是加在非线性层之前,线性层之后,且BN层之前的线性层可以不要偏置(由于BN含偏置)。
每个神经元输出都套一个BN块,这个BN块搜集一个batch内该神经元所有输出,得到均值和均方差根,并且计算出标准化后的值到下一层。

对于一个batch中神经元A输出的所有x

[ m=frac 1n sum x_i std=frac 1n sum (x_i-m)^2 x‘_i gets frac x_i-msqrt std+eps y_i gets gamma x‘_i +eta ]

其中eps为一个很小值(1e-5)来防止分母为零
以下举例对一个MLP输出矩阵求BN
(每一行一个batch每一列一个特征,每一列就是一个神经元输出)

[ A= eginpmatrix a_00 & a_01 a_10 & a_11 a_20 & a_21 \endpmatrix m_0=(a_00+a_10+a_20)/3 m_1=(a_01+a_11+a_21)/3 std_0=((a_00-m_0)^2+(a_10-m_0)^2+(a_20-m_0)^2)/3 std_1=((a_01-m_1)^2+(a_11-m_1)^2+(a_21-m_1)^2)/3 y_ij= gamma_j a‘_ij+eta_j =gamma_j frac (a_ij-m_j)sqrtstd_j+eps +eta_j ]

下面求梯度,使用链式法则:

[ frac partial Lpartial std_j=frac-gamma_j 2(std_j+eps)^-3/2 sum_i g_ij(a_ij-m_j) \frac partial Lpartial m_j=sum_i g_ij gamma_j frac -sqrtstd_j+eps-(a_ij-m_j)(1/2)(std_j+eps)^-1/2fracpartial std_jpartial m_jstd_j+eps =frac-gamma_jsqrtstd_j+epssum_i g_ij-fracpartial Lpartial std_jfrac1n sum_i 2(a_ij-m_j) \frac partial Lpartial a_ij=sum_i g_ij gamma_j frac (1-frac 1n) sqrt std_j+eps-(a_ij-m_j)frac 12(std_j+eps)^-1/2frac 2n(a_ij-m_j)(1-frac 1n)std_j+eps =frac 1n fracpartial Lpartial m_j+frac 2(a_ij-m_j)n fracpartial Lpartial std_j +frac g_ij gamma_jsqrt std_j+eps \frac partial Lpartial gamma_j=sum_i g_ija‘_ij \frac partial Lpartial eta_j=sum_i g_ij ]


三、损失函数

1. Softmax单元:

在分类问题中,神经网络的输出层属于One-Hot类型。
比如手写数字识别中只需识别0~9数字,那么神经网络的输出层一共有10个神经元(对应10分类问题)。我们需要将这些输出表示为对应概率,因此和必须=1且每个输出大于0。这就需要通过Softmax层处理。

[ S_i=frace^isum_k=0^n e^k ]

[ fracpartial S_ipartial i=frace^i sum e^k - e^i e^i(sum e^k)^2 =frace^i ( sum e^k - e^i)(sum e^k)^2=S_i(1-S_i) ]

[ fracpartial S_ipartial j_i eq j= -frace^i e^j(sum e^k)^2 = -S_iS_j ]

已知 (S=softmax(A),fracpartial Lpartial S=G)(fracpartial Lpartial A)
注意:A的每一行为一个样本,因此Softmax是对每一行进行操作

[ softmax eginpmatrix a_11 & a_12 & a_13 a_21 & a_22 & a_23 \endpmatrix_2 imes 3 = eginpmatrix frace^a11e^a11+e^a12+e^a13 &frace^a12e^a11+e^a12+e^a13 & frace^a13e^a11+e^a12+e^a13 frace^a21e^a21+e^a22+e^a23 &frace^a22e^a21+e^a22+e^a23 & frace^a23e^a21+e^a22+e^a23 \endpmatrix_2 imes 3 = eginpmatrix s_11 & s_12 & s_13 s_21 & s_22 & s_23 \endpmatrix ]

[ fracpartial Lpartial A= eginpmatrix s_11(g_11(1-s_11)-g_12s_12-g_13s_13) & s_12(g_11s_11-g_12(1-s_12)-g_13s_13) & ... s_21(g_21(1-s_21)-g_22s_22-g_23s_23) & s_21(g_21s_21-g_22(1-s_22)-g_23s_23) & ... \endpmatrix ]

2. Cross-Entropy单元:

Cross-Entropy是分类问题常用的损失函数,配合Softmax使用,其输入为一个表示不同类别识别概率的向量,以及One-Hot类型的标签向量,输出代价。

[ L_Cross-Entropy=- frac 1n sum y log a + (1-y) log (1-a) ]

比如我们得到最终Softmax输出的分类概率为 [0.8 , 0.1 , 0.1] One-Hot标签为[1 , 0 , 0]。
(Loss=-frac 13 (log 0.8+log 0.9+log 0.9))
对于一个batch中我们可以这样计算:

[ L=CrossEntropy( eginpmatrix a_11 & a_12 & a_13 a_21 & a_22 & a_23 \endpmatrix, eginpmatrix 1 & 0 & 0 0 & 1 & 0 \endpmatrix ) =-frac 16(log a_11 + log (1-a_12) + log (1-a_13) + log (1-a_21) + log a_22 + log (1-a_23)) ]

对于Cross-Entropy我们仅需对其输入求导,对标签矩阵无需求导。
求导十分方便,对矩阵A每个位置的求导,仅与该位置的原始数据和Y有关。
(fracpartial Lpartial a_ij=y_ij是否为1? 是-frac 1na_ij:否frac 1n(1-a_ij))

3. 分类问题的SCE单元:

[ L=SCE( eginpmatrix a_00 & a_01 & a_02 a_10 & a_11 & a_12 \endpmatrix, eginpmatrix 0 1 \endpmatrix ) =-frac 12 (ln frac e^a_00e^a_00+e^a_01+e^a_02 + ln frac e^a_11e^a_10+e^a_11+e^a_12) = frac 12(ln s_00+ln s_11) = -frac 12( a_00-ln (e^a_00+e^a_01+e^a_02)+a_11-ln (e^a_10+e^a_11+e^a_12) ) \]

(fracpartial Lpartial a_ij=y_i是否为j? 是frac s_ij-1n:否frac s_ijn)


四、卷积神经网络

1. 单通道图像卷积:

我们用一个例子对图A进行二维卷积运算,图像为单通道,使用一个卷积核。

[ Conv2d( eginpmatrix a_00 & a_01 & a_02 a_10 & a_11 & a_12 a_20 & a_21 & a_22 \endpmatrix, eginpmatrix w_00 & w_01 w_10 & w_11 \endpmatrix,b)= eginpmatrix a_00w_00+a_01w_01+a_10w_10+a_11w_11+b & a_01w_00+a_02w_01+a_11w_10+a_12w_11+b a_10w_00+a_11w_01+a_20w_10+a_21w_11+b & a_11w_00+a_12w_01+a_21w_10+a_22w_11+b\endpmatrix ]

如果图A高度Ah宽度Aw,卷积核高度Wh宽度Ww。
输出图高:Ah-Wh+1
输出图宽:Aw-Ww+1

for(i=0;i<Ah-Wh+1;i++)
    for(j=0;j<Aw-Ww+1;j++)
    
        sum=0;
        for(di=0;di<Wh;di++)
            for(dj=0;dj<Ww;dj++)
                sum+=A[i+di][j+dj]*W[di][dj];
        Out[i][j]=sum;
    

2. 单通道卷积梯度计算:

上面例子中,对W的偏导如下:

[ frac partial Lpartial W= eginpmatrix g_00a_00+g_01a_01+g_10a_10+g_11a_11 & g_00a_01+g_01a_02+g_10a_11+g_11a_12 g_00a_10+g_01a_11+g_10a_20+g_11a_21 & g_00a_11+g_01a_12+g_10a_21+g_11a_22 \endpmatrix =Conv2d(G,A,0),(G形状与输出一样) ]

上面例子中,对A的偏导如下:

[ frac partial Lpartial A= eginpmatrix g_00w_00 & g_00w_01+g_01w_00 & g_01w_01 g_00w_10+g_10w_00 & g_00w_11+g_01w_10+g_10w_01+g_11w_00 & g_01w_11+g_11w_01 g_10w_10 & g_10w_11+g_11w_10 & g_11w_11 \endpmatrix ]

看似十分凌乱,这里运用了一个小技巧:
先对G做padding变为G‘:
高度方向:上下各增加Wh-1行0。
宽度方向:左右各增加Ww-1列0。
再对W做元素逆置:
W矩阵的一维排列:W00,W01,W10,W11 变换成: W11,W10,W01,W00 再整合成原形状矩阵。
最终结果为:
[ frac partial Lpartial A=Conv2d(G‘, eginpmatrix w_11 & w_10 w_01 & w_00 \endpmatrix,0) ]

来验证一下尺寸是否正确:
Gw=Aw-Ww+1
G‘w=Aw-Ww+1+2(Ww-1)=Aw+Ww-1
Aw=G‘w-Ww+1

[ frac partial Lpartial b=G各元素和 ]

上面表明,我们同样可以用卷积操作来求卷积核的梯度,十分方便。

3. Conv2d-Padding:

如果需要输入图像与输出图像形状一样,可以在进行卷积运算之前将输入图周围补0。
为了尽可能将图像放到中间。
如果Ww为奇数,Whfw=Ww/2(向下取整),图像左右各加Whfw列0,行操作类似。
如果Ww为偶数,Whfw=Ww/2,图像左加Whfw-1列0,右加Whfw列0,行操作类似。
在反传梯度的时候,原来Padding过的位置不需要传梯度。

4. 多通道多图卷积:

如果一个图像是三通道的,那么每一个卷积核也应该为三通道(一个卷积核偏置只有一个)。
一个卷积核输出:该卷积核各通道分别卷积图像各通道,各通道输出图叠加,再每个位置加一个偏置。
对于100张2通道4*4图形成的batch,用6个3*3卷积核操作的形状如下:

[ Conv2d( A_100 imes 2 imes 4 imes 4,W_6 imes 2 imes 3 imes 3,b_6)= O_100 imes 6 imes 2 imes 2 ]

A1 A2 A3           K11 K12 K13      A^1 A^2 //A的3个通道分别和K1_,K2_的3个通道卷积
          Conv2d                =  
B1 B2 B3           K21 K22 K23      B^1 B^2 //B的3个通道分别和K1_,K2_的3个通道卷积
//A^1这样形成:A1卷积K11+A2卷积K12+A3卷积K13+偏置1(标量)
//A^2这样形成:A1卷积K21+A2卷积K22+A3卷积K23+偏置2(标量)  
//以上加法为矩阵对应位置相加

为了使输入与输出图形状一样,对输入图padding,高宽分别补上2*Whfh与2*Whfw个0。

5. 多通道多图卷积梯度计算:

用一个例子来说明:

//以下每一个元素都是一张图
    I00 I01 I02
I=  I10 I11 I12  //四张三通道图
    I20 I21 I22
    I30 I31 I32

W=  W00 W01 W02  //两个三通道卷积核
    W10 W11 W12
  
    H00 H01
H=  H10 H11  //输出四张两通道图(每一个通道对应一个卷积核输出)
    H20 H21
    H30 H31

    G00 G01  //该矩阵每一个元素都是一张图
G=  G10 G11  //矩阵高度为一个batch的样本数
    G20 G21  //矩阵宽度为一个样本的输出特征图多少,也即该层卷积核数目
    G30 G31

下面用类似 矩阵乘法 的操作来求多图多通道卷积的梯度:

[ Conv2dGrad\_I( eginpmatrix G_00 & G_01 G_10 & G_11 G_20 & G_21 \endpmatrix, eginpmatrix W_00 & W_01 & W_02 W_10 & W_11 & W_12 \endpmatrix) \ = eginpmatrix G_00W_00+G_01W_10 & G_00W_01+G_01W_11 & G_00W_02+G_01W_12 G_10W_00+G_11W_10 & G_10W_01+G_11W_11 & G_10W_02+G_11W_12 G_20W_00+G_21W_10 & G_20W_01+G_21W_11 & G_20W_02+G_21W_12 \endpmatrix) \ =G imes W \(乘法为求单图梯度,加法为图对应位置叠加) ]

[ Conv2dGrad\_W( eginpmatrix G_00 & G_01 G_10 & G_11 G_20 & G_21 \endpmatrix, eginpmatrix I_00 & I_01 & I_02 I_10 & I_11 & I_12 I_20 & I_21 & I_22 \endpmatrix) \ = eginpmatrix G_00I_00+G_10I_10++G_20I_20 & G_00I_01+G_10I_11++G_20I_21 & G_00I_02+G_10I_12++G_20I_22 G_01I_00+G_11I_10++G_21I_20 & G_01I_01+G_11I_11++G_21I_21 & G_01I_02+G_11I_12++G_21I_22 \endpmatrix \ = G^T imes I \(乘法为求单卷积核梯度,加法为对应位置叠加) ]

[ Conv2dGrad\_b( eginpmatrix G_00 & G_01 G_10 & G_11 G_20 & G_21 \endpmatrix)= eginpmatrix sum G_00 + sum G_10 + sum G_20 sum G_01 + sum G_11 + sum G_21 \endpmatrix (求和符号为所有元素累加) ]

6. 多步长卷积 :

为了在卷积的同时对图像进行降维,可以指定步长进行卷积。

stride=2; //要指定的步长
//得到输出图的形状
Out_h=(Ah-Wh+1)%stride==0?(Ah-Wh+1)/stride:(Ah-Wh+1)/stride+1; //(Ah-Wh+1)/stride向上取整
Out_w=(Aw-Ww+1)%stride==0?(Aw-Ww+1)/stride:(Aw-Ww+1)/stride+1;
Out_1d=(float*)Out; //变成一维数组操作
index=0;
for(i=0;i<Ah-Wh+1;i+=stride)
    for(j=0;j<Aw-Ww+1;j+=stride) 
        sum=0;
        for(di=0;di<Wh;di++)
            for(dj=0;dj<Ww;dj++)
                sum+=A[i+di][j+dj]*W[di][dj];
        Out_1d[index++]=sum;
    

7. 多步长卷积梯度计算 :

对卷积核W梯度:

//CPU优化代码如下
G_1d=(float*)G; //变成一维数组操作
for(i=0;i<Wh;i++)
    for(j=0;j<Ww;j++) 
        sum=0;
        index=0;
        for(di=0;di<Grad_h*stride;di+=stride)
            for(dj=0;dj<Grad_w*stride;dj+=stride)
                sum+=G_1d[index++]*A[i+di][j+dj]; //相当于卷积核G内间隔stride卷积
        Wgrad[i][j]+=sum;
    
/*如需要并行操作,可以沿用Conv2d操作,具体如下:
1.创造一个Ah-Wh+1行Aw-Ww+1列零矩阵G‘
2.将G‘用G间隔stride-1行stride-1列填充(G平均分散成G‘且G‘(0,0)=G(0,0))
3.对W梯度=Conv2d(A,G‘,0)*/

对输入图A梯度:

/*无明显优化算法,统一用Conv2d操作,步骤如下:
1.创造一个Ah-Wh+1行Aw-Ww+1列零矩阵G‘
2.将G‘用G间隔stride-1行stride-1列填充(G平均分散成G‘且G‘(0,0)=G(0,0))
3.对G‘进行如下Padding:高度方向上下各增加Wh-1行零,宽度方向左右各增加Ww-1列零,记G‘‘。
4.将卷积核W元素逆置变为W‘
5.对A梯度=Conv2d(G‘‘,W‘,0)*/
//对偏置b的梯度依然是G内所有元素相加

8. 池化层 :

将图像切成一块一块,求每块最大值Max或平均值Avg做为输出图的相应点。
MaxPool作用:降维,减少对识别物体的平移敏感度。
AvgPool作用:降维,保持原有平移敏感度。

MaxPool 前向计算:

kernel_size=2; //池化方框边长
stride=2;//池化步长
//得到输出图的形状
Out_h=(Ah-kernel_size+1)%stride==0?(Ah-kernel_size+1)/stride:(Ah-kernel_size+1)/stride+1; 
Out_w=(Aw-kernel_size+1)%stride==0?(Aw-kernel_size+1)/stride:(Aw-kernel_size+1)/stride+1;
Out_1d=(float*)Out; //变成一维数组操作
Out_Max_Y=(int*)malloc(Out_h*Out_w*sizeof(int)); //保存最大值坐标方便梯度计算
Out_Max_X=(int*)malloc(Out_h*Out_w*sizeof(int));
index=0;
for(i=0;i<Ah-kernel_size+1;i+=stride)
    for(j=0;j<Aw-kernel_size+1;j+=stride) 
        maxi=i;maxj=j;maxm=A[i][j];
        for(pi=i+1;pi<i+kernel_size;pi++)
            for(pj=j+1;pj<j+kernel_size;pj++)
                if(A[pi][pj]>maxm)
                    maxi=pi;
                    maxj=pj;
                    maxm=A[pi][pj];
                
        Out_Max_Y[index]=maxi;
        Out_Max_X[index]=maxj;
        Out_1d[index++]=maxm;
    

MaxPool 梯度计算:

//梯度矩阵G与输出矩阵大小相同,因此是被降维过的。
//要计算输入矩阵A的梯度
//先得到每块中哪个是最大值
//G中某个位置的值直接传给对应块最大值的位置,该块中其他的梯度不传递值
G_1d=(float*)G; //变成一维度数组操作
index=0;
for(i=0;i<Ah-kernel_size+1;i+=stride)
    for(j=0;j<Aw-kernel_size+1;j+=stride) 
        Agrad[Out_Max_Y[index]][Out_Max_X[index]]+=G_1d[index]; //注意反向梯度计算是累加的
        index++;
    
free(Out_Max_Y); //如果是动态内存需要回收
free(Out_Max_X);

AvgPool 前向计算:

kernel_size=2; //池化方框边长
stride=2;//池化步长
//得到输出图的形状
Out_h=(Ah-kernel_size+1)%stride==0?(Ah-kernel_size+1)/stride:(Ah-kernel_size+1)/stride+1; 
Out_w=(Aw-kernel_size+1)%stride==0?(Aw-kernel_size+1)/stride:(Aw-kernel_size+1)/stride+1;
Out_1d=(float*)Out; //变成一维数组操作
Out_Max_Y=(int*)malloc(Out_h*Out_w*sizeof(int)); //保存最大值坐标方便梯度计算
Out_Max_X=(int*)malloc(Out_h*Out_w*sizeof(int));
index=0;
for(i=0;i<Ah-kernel_size+1;i+=stride)
    for(j=0;j<Aw-kernel_size+1;j+=stride) 
        sum=0;
        for(pi=i;pi<i+kernel_size;pi++)
            for(pj=j;pj<j+kernel_size;pj++)
                sum+=A[pi][pj]
        Out_1d[index++]=sum/(kernel_size*kernel_size);
    

AvgPool 梯度计算:

//G中某个位置的值,平均分配给对应块各个位置
G_1d=(float*)G; //变成一维数组操作
index=0;
for(i=0;i<Ah-kernel_size+1;i+=stride)
    for(j=0;j<Aw-kernel_size+1;j+=stride) 
        for(pi=i;pi<i+kernel_size;pi++)
            for(pj=0;pj<j+kernel_size;pj++)
                Agrad[pi][pj]+=G_1d[index]/(kernel_size*kernel_size); //平均分配
        index++;
    

9. Conv2d-BN-Layer :

由于Batch Normalization是套在每个神经元输出后面的,一个卷积核即一个神经元,因此操作如下:

A1 A2 //假设输出为三张图,每张图两个通道
B1 B2 //由于A1,B1,C1都是卷积核W1的输出,A2,B2,C2都为卷积核W2的输出
C1 C2 //因此有两个BN节点,分别对应W1与W2
//1.计算(A1,B1,C1)上所有像素的平均值和均方差根,求出对应各像素BN输出
//2.计算(A2,B2,C2)上所有像素的平均值和均方差根,求出对应各像素BN输出
//3.计算梯度参考前面,操作集合确定后方法是一样的。

五、深度残差网络

如果要训练很深的网络,会面临一个微妙的问题:由于每次梯度传播都是按顺序从最高层到最底层,如果某个很低的层需要很高层的某个信息,那么在此之间所有层都应该学习去保留这些信息,这增加了学习的复杂度。ResNet为了解决低层无法充分利用高层的有用信息(梯度)这一问题。

1. 残差网络基本单元 :

X--A1--relu--A2-- add--relu-->
                /
  ---------------
//A1、A2为单层线性单元,relu非线性激活函数,add为对应元素相加
//注:可以用任意深度的非线性块替代上图中A1-relu-A2的位置(最后为线性层)

输入X经过一个旁路直接与A2输出叠加。
理论上,一个A1--relu--A2块能够逼近任意函数,姑且称它为万能块。万能块并不好用,比如让它逼近函数H(X)=X十分困难。并且如果要通过万能块传梯度到X,势必会经过一定衰减。
如果任务要求让不加残差网络的万能块逼近H(X),那么残差网络的万能块转化为逼近H(X)-X。
有如下两个优势:

  • 因为X一般都是正的元素(如图像像素,前面经过relu情况),并且H(X)中真正有用的信息必然拥有正的响应值(relu的特性使上层只把梯度传给A2输出为正的神经元,A2输出为负的成为死神经元),因此H(X)-X在有用信息的方向上更接近原点,因此更容易学习。

  • 传到X的梯度为万能块与后面层梯度之和,如果万能块贡献很小,那么其传下去的梯度几乎不影响前面各层,该万能层也会被渐渐忽略,即逼近H(X),也即网络有明显的忽略一些层的能力。如果万能块与后面层贡献相当,那么前面层能学习到两者共同的有用信息。

上述残差网络块需要输入与输出形状一致,事实上很难保证,我们可以通过线性变化让其一致。

2. 残差卷积单元 :

为了将残差结构应用到卷积神经网络上,我们可以做成一个特殊的单元,在应用时叠加这些单元,下述两个单元结构引用了论文 Deep Residual Learning for Image Recognition 中的结构。

对于较浅的层数<=34:

Node* ResidualCNNBlock(Node* X,int inchannel,int outchannel,int stride) 
    //输入图X(batch,inchannel,h,w)
    out=Conv2d(X,inchannel,outchannel,3,stride,padding=1,bias=False);//stride步3*3卷积
    out=BatchNorm2d(out);
    out=ReLU(out);
    //(batch,outchannel,h/stride,w/stride)
    out=Conv2d(out,outchannel,outchannel,3,1,padding=1,bias=False);
    out=BatchNorm2d(out);
    //万能块输出(batch,outchannel,h/stride,w/stride)
    if(stride!=1||inchannel!=outchannel)  //输入与输出形状不一致,需要线性变换
        sc_out=Conv2d(X,inchannel,outchannel,1,stride,padding=0,bias=False);
        sc_out=out=BatchNorm2d(sc_out);
        out=Add(out,sc_out);
     else 
        out=Add(out,X);
    
    out=ReLU(out); //最后一个非线性映射
    return out; //输出(batch,outchannel,h/stride,w/stride)

对于较深的层数:

//即便输入通道比较多,也可以用比较少的卷积核卷积,来节省计算
Node* ResidualCNNBlock(Node* X,int inchannel,int outchannel,int stride,int knum) 
    //(batch,inchannel,h,w)
    out=Conv2d(X,inchannel,knum,1,1,padding=0,bias=False);//1步1*1卷积降维
    out=BatchNorm2d(out);
    out=ReLU(out);
    //(batch,knum,h,w)
    out=Conv2d(out,knum,knum,3,stride,padding=1,bias=False);//stride步3*3卷积
    out=BatchNorm2d(out);
    out=ReLU(out);
    //(batch,knum,h/stride,w/stride)
    out=Conv2d(out,knum,outchannel,1,1,padding=0,bias=False);//1步1*1卷积升维
    out=BatchNorm2d(out);
    //(batch,outchannel,h/stride,w/stride)
    if(stride!=1||inchannel!=outchannel)  //输入与输出形状不一致,需要线性变换
        sc_out=Conv2d(X,inchannel,outchannel,1,stride,padding=0,bias=False);
        sc_out=out=BatchNorm2d(sc_out);
        out=Add(out,sc_out);
     else 
        out=Add(out,X);
    
    out=ReLU(out); //最后一个非线性映射
    return out; //输出(batch,outchannel,h/stride,w/stride)

3. 深度残差卷积网络案例 :

由于当图像尺寸减半时卷积核数目加倍,我们将相同卷积核数目的残差网络层绑定在一起,设计成一个块:

Node* ResNetBlocks(Node* X,int inchannel,int outchannel,int stride,int depth,int knum) 
    //(batch,inchannel,h,w)
    out=ResidualCNNBlock(X,inchannel,outchannel,stride,knum);
    //(batch,outchannel,h/stride,w/stride)
    for(i=1;i<depth;i++)
        out=ResidualCNNBlock(out,outchannel,outchannel,1,knum);
    return out; //(batch,outchannel,h/stride,w/stride)

以下为官方的几种设计案例:

//34-Layer:
out=Conv2d_Padding_BN_ReLU(X,Xchannel,kernel_size=7,outchannel=cnl);//预处理
out=ResNetBlocks(out,inchannel=cnl,outchannel=64,stride=2,depth=3);
out=ResNetBlocks(out,inchannel=64,outchannel=128,stride=2,depth=4);
out=ResNetBlocks(out,inchannel=128,outchannel=256,stride=2,depth=6);
out=ResNetBlocks(out,inchannel=256,outchannel=512,stride=2,depth=3);
out=GlobalAvgPool(out); //每张图像尺寸降成1
out=MLP(infeature=512,outfeature=分类数目);
//50-layer,101-layer,152-layer:   (差别仅关注depth参数)
out=Conv2d_Padding_BN_ReLU(X,Xchannel,kernel_size=7,outchannel=cnl);//预处理
out=ResNetBlocks(out,inchannel=cnl,outchannel=256,stride=2,depth=3,knum=64);
out=ResNetBlocks(out,inchannel=256,outchannel=512,stride=2,depth=4,knum=128);//4,4,8
out=ResNetBlocks(out,inchannel=512,outchannel=1024,stride=2,depth=6,knum=256);//6,23,56
out=ResNetBlocks(out,inchannel=1024,outchannel=2048,stride=2,depth=3,knum=512);
out=GlobalAvgPool(out); //每张图像尺寸降成1
out=MLP(infeature=2048,outfeature=分类数目);























































pytorch深度学习框架:优雅而简洁的代码实现(代码片段)

 PyTorch是由Facebook发布的深度学习框架,旨在为研究人员和工程师提供快速、灵活和简单的实验平台。与其他框架相比,PyTorch具有简洁的API和灵活的动态计算图,使得构建和训练深度神经网络变得更加优雅和简洁。本... 查看详情

计算机视觉pytorch实现(代码片段)

...识别、图像分割、目标跟踪、图像分类、姿态估计等。在深度学习中人们开发了很多的学习框架,如Caffe、MXNet、Pytorch和TensorFlow等。这些框架可以极大简化了构建深度学习神经网络的过程。在计算机视觉应用中通过pytorch 查看详情

tensorflow实战google深度学习框架(代码片段)

第3章TensorFlow入门3.1TensorFlow计算模型-计算图3.1.1计算图的概念Tensorflow中所有计算都会被转化成计算图的一个节点,计算图上的边表示了他们之间的相互依赖关系。3.1.2计算图的使用Tensorflow的程序可以分成两个阶段:定义计算、... 查看详情

深度学习框架tensorflow模型分析(代码片段)

目录1快速入门模型2相关的库的导入3数据展示和划分4sklearn实现5tf.keras实现6总结1快速入门模型机器学习鸢尾花数据集分析:https://blog.csdn.net/ZGL_cyy/article/details/126924746机器学习k近邻算法鸢尾花种类预测:https://blog.csdn.net/ZGL_cyy/article... 查看详情

深度学习框架-tensorflow(代码片段)

...操作知道tf.keras中的相关模块及常用方法 1.1TensorFlow介绍深度学习框架TensorFlow一经发布,就受到了广泛的关注,并在计算机视觉、音频处理、推荐系统和自然语言处理等场景下都被大面积推广使用,现在已发布2.3.0版... 查看详情

深度学习和计算机视觉(cv)介绍(代码片段)

目录1深度学习概述1.1什么是深度学习1.2发展历史2计算机视觉(CV)2.1计算机视觉定义2.2常见任务2.3应用场景2.3.1人脸识别2.3.2视频监控2.3.3图片识别分析2.3.4辅助驾驶2.4发展历史1深度学习概述1.1什么是深度学习在介绍深度... 查看详情

tensorflow实战google深度学习框架笔记codepart(代码片段)

深层神经网络线性模型的局限性激活函数实现去线性化a=tf.nn.relu(tf.matmul(x,w1)+biases1)y=tf.nn.relu(tf.matmul(a,w2)+biases2)经典损失函数#计算交叉熵cross_entropy=-tf.reduce_mean(y_*tf.log(tf.clip_by_value(y,le-10,1.0)))#tf.clip_by_value样例 查看详情

pytorch深度学习框架介绍+简单回归问题(代码片段)

深度学习框架id="深度学习框架">深度学习框架pytorchpytorch使用C和C++的后端,是站在torch框架基础上开发pytorch和tensorflow的基本区别是使用了动态图和静态图的区别机器学习中的常见的静态图和动态图静态图必须事先定义好静态... 查看详情

从零实现深度学习框架——seq2seq从理论到实战理论(代码片段)

...,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不使用外部完备... 查看详情

深度学习框架tensorflow快速入门(代码片段)

...nsorFlow的安装3张量及其操作4tf.keras介绍5总结1TensorFlow介绍深度学习框架TensorFlow一经发布,就受到了广泛的关注,并在计算机视觉、音频处理、推荐系统和自然语言处理等场景下都被大面积推广使用,接下来我们深入... 查看详情

深度学习框架中的动态shape问题(代码片段)

前言最近这段时间实现了公司内部深度学习框架的dynamicshape的功能。在完成这个功能之后,我进一步深刻体会到了乔布斯说过的一段话:Whenyoustartlookingataproblemanditseemsreallysimple,youdon’treallyunderstandthecomplexityoftheproblem.Thenyo... 查看详情

从零开始实现一个深度学习框架|激活函数,损失函数与卷积层(代码片段)

往期回顾super(Sigmoid,self).__init__()x=/(+np.exp(-x))xp=self.forward(self.storage[gradient=p*(-p)accumulated_gradient*gradientsuper(Tanh,self).__init__()x=/(+np.exp(*x))-xp=self.forward(self.storage[grad 查看详情

前端如何开始深度学习,那不妨试试jax(代码片段)

一、简介在深度学习方面,TensorFlow和PyTorch是绝对的王者。但是,但除了这两个框架之外,一些新生的框架也不容小觑,比如谷歌推出的JAX深度学习框架。1.1、快速发展的JAXJAX是一个用于高性能数值计算的Python库&#... 查看详情

从零实现深度学习框架——计算图运算补充

...,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不适用外部完备... 查看详情

深度学习入门2022最新版深度学习简介(代码片段)

【深度学习入门2022最新版】第一课深度学习简介概述深度学习vs机器学习机器学习是什么深度学习是什么机器学习和深度学习的区别神经网络机器学习实现二分类神经网络实现二分类TensorFlowPyTorch神经网络的原理张量张量最小值(... 查看详情

深度学习框架torch7解析--tensor篇(代码片段)

Tensor类型多维矩阵数据的内部表示内存管理常用函数Tensor的构造函数常用操作函数复制和初始化Rsizing提取子tensor未完待续TensorTensor类可以说是Torch中最重要的类了,因为几乎所有的包都依赖于这个类实现.它是整个Torch实现的数据... 查看详情

深度学习框架介绍(代码片段)

深度学习框架介绍1.常见深度学习框架对比2.TensorFlow的特点3.TensorFlow的安装4.Tenssorlfow使用技巧1.常见深度学习框架对比tensorflow的github:2.TensorFlow的特点官网:https://www.tensorflow.org/语言多样(LanguageOptions)TensorFlo 查看详情

从零实现深度学习框架收藏

...,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不适用外部完备... 查看详情