DeepPoly中卷积的纯矩阵实现
DeepPoly是一种证明神经网络在特定输入域上稳定的方法,对于普通的线性层可以达到完全的准确性,对于ReLU层使用线性边界近似。卷积层可以看成一种特殊的线性层,但是在实际实现中,为了性能考虑,需要额外处理。本文借用PyTorch的矩阵操作实现DeepPoly卷积。
线性层
我们先从相对简单的线性层出发,来解释一下DeepPoly的回传过程。
问题定义
对于线性层 $\vec x^{m+1}=W^m \vec x^m+\vec b^m$ ,已知
\[W_{l}^{m,m-1} \vec x^{m-1} + \vec b_{l}^{m,m-1} \leq \vec x^{m} \leq W_{u}^{m,m-1} \vec x^{m-1} + \vec b_{u}^{m,m-1}\]求 $\vec x^{m+1}$ 的范围。
理论分析
$\vec x^{m+1}$ 可以表示成
\[W_{L}^{m+1,m-1} \vec x^{m-1} + \vec b_{L}^{m+1,m-1} \leq \vec x^{m+1} \leq W_{U}^{m+1,m-1} \vec x^{m-1} + \vec b_{U}^{m+1,m-1}\]的形式,与输入域的形式类似,因此,只需要求出参数 $W_{l}^{i+1,i-1}, \vec b_{l}^{i+1,i-1} , W_{u}^{i+1,i-1}, b_{u}^{i+1,i-1}$ 就可以合并两层。通过代入,可以得到参数的计算公式:
\[W_{L}^{m+1,m-1}[x, z] = \sum_y W^{m}[x, y] (W_L^{m, m-1}[y, z] [W^{m}[x, y] > 0] + W_U^{m, m-1}[y, z] [W^{m}[x, y] < 0])\] \[W_{U}^{m+1,m-1}[x, z] = \sum_y W^{m}[x, y] (W_U^{m, m-1}[y, z] [W^{m}[x, y] > 0] + W_L^{m, m-1}[y, z] [W^{m}[x, y] < 0])\]直观理解上,就是如果该层的权值为正,下限就取上一层的下限,上限就取上一层的上限;如果为负,就反之。
具体实现
在具体实现上,为了批量化,需要使用gather
函数来根据正负选取权值:
def backsubsitute(self, lowerW: Tensor, lowerB: Tensor, upperW: Tensor, upperB: Tensor):
new_sz = lowerW.shape[0]
pre_sz, base_sz = self.lowerW.shape
W = torch.stack([self.lowerW, self.upperW], dim=1) # [p,2,q]
W = W.expand((new_sz, -1, -1, -1)) # [o,p,2,q]
B = torch.stack([self.lowerB, self.upperB], dim=1) # [p,2]
B = B.expand((new_sz, -1, -1)) # [o,p,2]
idx_neg = (lowerW < 0).long() # [o,p]
idxW = idx_neg.unsqueeze(-1).unsqueeze(-1).expand((-1, -1, -1, base_sz)) # [o,p,1,q]
idxB = idx_neg.unsqueeze(-1) # [o,p,1]
lW = torch.gather(W, dim=2, index=idxW).squeeze(2) # [o,p,1,q] -> [o,p,q]
lB = torch.gather(B, dim=2, index=idxB) # [o,p,1]
lowerW = lowerW.unsqueeze(1) # [o,p] -> [o,1,p]
newLowerW = torch.bmm(lowerW, lW).squeeze(1) # [o,1,p]x[o,p,q]=[o,1,q] -> [o,q]
newLowerB = torch.bmm(lowerW, lB).view(-1) # [o,1,p]x[o,p,1]=[o,1,1] -> [o]
newLowerB += lowerB
idx_pos = (upperW > 0).long()
idxW = idx_pos.unsqueeze(-1).unsqueeze(-1).expand((-1, -1, -1, base_sz))
idxB = idx_pos.unsqueeze(-1)
uW = torch.gather(W, dim=2, index=idxW).squeeze(2)
uB = torch.gather(B, dim=2, index=idxB)
upperW = upperW.unsqueeze(1)
newUpperW = torch.bmm(upperW, uW).squeeze(1)
newUpperB = torch.bmm(upperW, uB).view(-1)
newUpperB += upperB
return self.preLayer.backsubsitute(newLowerW, newLowerB, newUpperW, newUpperB)
ReLU层也是用线性上下限证明的,因此可以直接复用,区别在于层本身的上下限要改成DeepPoly中的
\[\lambda \vec x^{m-1} \leq \vec x^m \leq A \vec x^{m-1} + B\]卷积层
DeepPoly中卷积层的主要矛盾在于,普通的卷积是共享权值的,但是对于DeepPoly的卷积ReLU层,每个像素的 $\lambda$ 不一定相等,导致每一层的卷积核不一定相等,因此需要分别存储处理才能达到最高的准确度。
常见的卷积操作为
\[X^{m+1}_{uvw} = \sum_{xyz} W^{m+1, m}_{uxyz} X^m_{x,v+y,w+z}\]权值只有在通道上是分离的,而在长宽上都是共享的。在这里,我们就需要把长宽也分离出来:
\[X^{m+1}_{uvw} = \sum_{xyz} W^{m+1, m}_{uvwxyz} X^m_{x,v+y,w+z}\]代入 $X^m_{xyz}$ ,得到 \(X^{m+1}_{uvw},X^{m-1}_{uvw}\) 之间的关系:
\[X^{m+1}_{ijk} = \sum_{uvw} W^{m+1, m}_{ijkuvw} \sum_{xyz} W^{m, m-1}_{uvwxyz} X^{m-1}_{x,j+v+y,k+w+z}\]同时我们知道, \(X^{m+1}_{uvw},X^{m-1}_{uvw}\) 之间的形式应该为:
\[X^{m+1}_{ijk} = \sum_{abc} W^{m+1,m-1}_{ijkabc} X^{m-1}_{a,j+b,k+c}\]和线性层不同,这里的 \(X^{m-1}_{a,j+b,k+c}\) 与 \(X^{m-1}_{x,j+v+y,k+w+z}\) 并不是一一对应的,而是多对一的关系,因此无法直接得到新参数,不过可以通过对应关系,得到
\[W^{m+1,m-1}_{ijkabc} = \sum \{ W^{m+1, m}_{ijkuvw} W^{m, m-1}_{uvwxyz} | \forall a=x, b=v+y, c=w+z \}\]在代码实现中,为了避免计算每组 $(a,b,c)$ 对应的 $(x,y,z,v,w)$ ,我们从 $(i,j,k,u,v,w)$ 的角度循环,把每次的结果累加到 $a=x,b=v+y,c=w+z$ 的位置。这种方法的缺点在于,循环 $v,w$ 具有依赖性,因此我们只批量化了循环 $i,j,k,u$ 。好在 $v,w$ 一般是很小的数,因此对于性能的影响并不大。
def backsubsitute_conv(self, lowerW: Tensor, lowerB: Tensor, upperW: Tensor, upperB: Tensor, stride, padding):
if self.preLayer is None:
return self.getBoundUnderWB(
lowerW, lowerB, upperW, upperB, stride, padding)
out_ch_1, in_ch_1, k_h_1, k_w_1 = self.lowerW.shape
out_ch_2, out_h_2, out_w_2, in_ch_2, k_h_2, k_w_2 = lowerW.shape
s1 = 2
p1 = 1
s2 = stride
p2 = padding
s = s1 * s2
p = p1 + s1*p2
k_h_merge = k_h_1 + s1*k_h_2 - s1
k_w_merge = k_w_1 + s1*k_w_2 - s1
assert(out_ch_1 == in_ch_2)
newLowerW = torch.zeros((out_ch_2, out_h_2, out_w_2,
in_ch_1, k_h_merge, k_w_merge))
newUpperW = torch.zeros((out_ch_2, out_h_2, out_w_2,
in_ch_1, k_h_merge, k_w_merge))
newLowerB = torch.zeros((out_ch_2, out_h_2, out_w_2))
newUpperB = torch.zeros((out_ch_2, out_h_2, out_w_2))
W1 = torch.stack([self.lowerW, self.upperW], dim=1)
B1 = torch.stack([self.lowerB, self.upperB], dim=1)
i, j, k, u = torch.meshgrid(
torch.arange(out_ch_2),
torch.arange(out_h_2),
torch.arange(out_w_2),
torch.arange(in_ch_2)
)
i, j, k = i.flatten(), j.flatten(), k.flatten()
u = u.flatten()
for v in range(k_h_2):
for w in range(k_w_2):
coef = lowerW[:,:,:,:,v,w] # [i,j,k,u]
idx_neg = (coef < 0).long().flatten()
lW = W1[u, idx_neg].reshape((out_ch_2, out_h_2, out_w_2, in_ch_2, in_ch_1, k_h_1, k_w_1))
lB = B1[u, idx_neg].reshape((out_ch_2, out_h_2, out_w_2, in_ch_2))
coef_ex = coef.unsqueeze(4).unsqueeze(5).unsqueeze(6)
newLowerW[:,:,:,:,s1*v:s1*v+k_h_1, s1*w:s1*w+k_w_1] += (coef_ex * lW).sum(3)
newLowerB += (coef * lB).sum(3)
coef = upperW[:,:,:,:,v,w]
idx_pos = (coef > 0).long().flatten()
uW = W1[u, idx_pos].reshape((out_ch_2, out_h_2, out_w_2, in_ch_2, in_ch_1, k_h_1, k_w_1))
uB = B1[u, idx_pos].reshape((out_ch_2, out_h_2, out_w_2, in_ch_2))
coef_ex = coef.unsqueeze(4).unsqueeze(5).unsqueeze(6)
newUpperW[:,:,:,:,s1*v:s1*v+k_h_1, s1*w:s1*w+k_w_1] += (coef_ex * uW).sum(3)
newUpperB += (coef * uB).sum(3)
newLowerB += lowerB
newUpperB += upperB
return self.preLayer.backsubsitute(newLowerW, newLowerB, newUpperW, newUpperB, s, p)
具体代码中,我们还增加了步长和填充的影响。对于卷积ReLU层,和线性层类似,只需要对每个像素取 $\lambda$ 即可。