紧致卷积网络设计——Shift卷积算子
前言
最近笔者在阅读关于骨骼点数据动作识别的文献Shift-GCN[2]的时候,发现了原来还有Shift卷积算子[1]这种东西,该算子是一种可供作为空间卷积的替代品,其理论上不需要增添额外的计算量和参数量,就可以通过1x1卷积实现空间域和通道域的卷积,是一种做紧致模型设计的好工具。本文作为笔记纪录笔者的论文阅读思考, 如有谬误请联系指出,转载请联系作者并注明出处,谢谢。
∇\nabla∇ 联系方式:
e-mail: FesianXu@gmail.com
QQ: 973926198
github: https://github.com/FesianXu
知乎专栏: 计算机视觉/计算机图形理论与应用
微信公众号:
卷积计算及其优化
为了讨论的连续性,我们先简单回顾下传统的深度学习卷积计算。给定一个输入张量,如Fig 1.1中的蓝色块所示,其尺寸为F∈RDF×DF×M\mathbf{F} \in \mathbb{R}^{D_F \times D_F \times M}F∈RDF×DF×M;给定卷积核K∈RDK×DK×M×N\mathbf{K} \in \mathbb{R}^{D_K \times D_K \times M \times N}K∈RDK×DK×M×N,如Fig 1.1中的蓝色虚线框所示,为了方便起见,假定步进stride = 1
,padding = 1
,那么最终得到输出结果为G∈RDF×DF×N\mathbf{G} \in \mathbb{R}^{D_F \times D_F \times N}G∈RDF×DF×N,计算过程如式子(1.1)所示:
Gk,l,n=∑i,j,mKi,j,m,nFk+i^,l+j^,m(1.1)G_{k,l,n} = \sum_{i,j,m} K_{i,j,m,n}F_{k+\hat{i},l+\hat{j},m} \tag{1.1} Gk,l,n=i,j,m∑Ki,j,m,nFk+i^,l+j^,m(1.1)
其中(k,l)(k,l)(k,l)为卷积中心,而i^=i−⌊DK/2⌋\hat{i} = i-\lfloor D_K/2\rfloori^=i−⌊DK/2⌋,j^=j−⌊DK/2⌋\hat{j} = j-\lfloor D_K /2 \rfloorj^=j−⌊DK/2⌋是卷积计算半径的索引。不难知道,该卷积操作的参数量为M×N×DK2M \times N \times D_K^2M×N×DK2。计算量也容易计算,考虑到每个卷积操作都需要对每个卷积核中的参数进行乘法计算,那么有乘法因子DK2D_K^2DK2,而考虑到stride = 1
而且存在填充,那么容易知道计算量为M×N×DF2×DK2M \times N \times D_F^2 \times D_K^2M×N×DF2×DK2 FLOPs。容易发现,卷积的计算量和参数量与卷积核大小DKD_KDK呈现着二次增长的关系,这使得卷积的计算量和参数量增长都随着网络设计的加深变得难以控制。
在进一步对传统卷积计算进行优化之前,我们先分析一下卷积计算到底提取了什么类型的信息。以二维卷积为例子,卷积计算主要在两个维度提取信息,空间域和通道域,不过从本质上说,通道域的信息可以看成是原始输入(比如RGB图片输入)的层次化特征/信息的层叠,因此本质上二维卷积还是提取空间域信息,只不过在层叠卷积过程中,使得空间域信息按照层次的特点,散布在了通道域中。
知道了这一点,我们就可以把卷积过程中的空间卷积和通道卷积分离开了,从而得到了所谓的 通道可分离卷积[4,5]。如Fig 1.2所示,这类型的卷积将空间域和通道域卷积完全分开,在第一步只考虑空间域卷积,因此对于每个输入张量的通道,只会有唯一一个对应的卷积核进行卷积。数学表达为:
G^k,l,m=∑i,jK^i,j,mFk+i^,l+j^,m(1.2)\hat{G}_{k,l,m} = \sum_{i,j} \hat{K}_{i,j,m} F_{k+\hat{i},l+\hat{j},m} \tag{1.2} G^k,l,m=i,j∑K^i,j,mFk+i^,l+j^,m(1.2)
对比式子(1.1)和(1.2),我们发现区别在于对卷积核的索引上,通过式子(1.2)输出的张量形状为G^∈RDF×DF×M\hat{\mathbf{G}} \in \mathbb{R}^{D_F \times D_F \times M}G^∈RDF×DF×M,为了接下来在通道域进行卷积,需要进一步应用1x1卷积,将通道数从MMM变为NNN,如式子(1.3)所示。
Gk,l,n=∑mPm,nG^k,l,m(1.3)G_{k,l,n} = \sum_{m} P_{m,n} \hat{G}_{k,l,m} \tag{1.3} Gk,l,n=m∑Pm,nG^k,l,m(1.3)
其中P∈RM×NP \in \mathbb{R}^{M \times N}P∈RM×N为1x1卷积核。通道可分离卷积就是将传统卷积(1.1)分解为了(1.2)(1.3)两个步骤。
通过这种优化,可以知道卷积核参数量变为M×DK2M \times D_K^2M×DK2,而计算量变为M×DK2×DF2M \times D_K^2 \times D_F^2M×DK2×DF2 FLOPs。虽然理论上,深度可分离网络的确减少了计算量和参数量,但是实际上,因为深度可分离网络的实现使得访存1(memory access)过程占据了主导,使得实际计算占用率过小,限制了硬件的并行计算能力。
我们用传统卷积和深度可分离卷积的计算/访存
系数进行比较(仅考虑最基本的访存,即是将每个操作数都从内存中获取,而不考虑由于局部性原理[6],而对重复的操作数进行访问导致的消耗):
传统卷积:M×N×DF2×DK2DF2×(M+N)+DF2×M×N(1.4)传统卷积:\dfrac{M \times N \times D_F^2 \times D_K^2}{D_F^2 \times (M+N) + D_F^2 \times M \times N} \tag{1.4} 传统卷积:DF2×(M+N)+DF2×M×NM×N×DF2×DK2(1.4)
深度可分离卷积:M×DF2×DK2DF2×2M+DK2×M(1.5)深度可分离卷积: \dfrac{M \times D_F^2 \times D_K^2}{D_F^2 \times 2M + D_K^2 \times M} \tag{1.5} 深度可分离卷积:DF2×2M+DK2×MM×DF2×DK2(1.5)
式子(1.4)和(1.5)的比较最终会化简为比较(M+N)/N(M+N)/N(M+N)/N和2M2M2M的大小,越小意味着计算效率越高。我们发现,传统的卷积反而比深度可分离卷积的计算效率高得多。这是不利于程序并行计算的。
为此,文章[1]提出了Shift卷积算子,尝试解决这种问题。
Shift卷积算子
在Shift卷积算子中,其基本思路也是类似于深度可分离卷积的设计,将卷积分为空间域和通道域的卷积,通道域的卷积同样是通过1x1卷积实现的,而在空间域卷积中,引入了shift操作。我们接下来会详细地探讨shift操作的设计启发,细节和推导。
shift卷积算子的数学形式表达如式子(2.1)所示,如图Fig 2.1所示,shift卷积的每一个卷积核都是一个“独热”的算子,其卷积核只有一个元素为1,其他全部为0,如式子(2.2)所示。类似于深度可分离卷积,对于输入的MMM个通道的张量,分别对应了MMM个Shift卷积核,如Fig 2.1的不同颜色的卷积核所示。
G~k,l,m=∑i,jK~i,j,mFk+i^,l+j^,m(2.1)\tilde{G}_{k,l,m} = \sum_{i,j} \tilde{K}_{i,j,m} F_{k+\hat{i},l+\hat{j},m} \tag{2.1} G~k,l,m=i,j∑K~i,j,mFk+i^,l+j^,m(2.1)
K~i,j,m={1,当i=im,j=jm0,其他(2.2)\tilde{K}_{i,j,m} = \left\{ \begin{aligned} 1, & & 当 i = i_m,j=j_m \\ 0, & & 其他 \end{aligned} \right. \tag{2.2} K~i,j,m={1,0,当i=im,j=jm其他(2.2)
我们把其中一个通道的shift卷积操作拿出来分析,如Fig 2.2所示。我们发现,shift卷积过程相当于将原输入的矩阵在某个方向进行平移,这也是为什么该操作称之为shift的原因。虽然简单的平移操作似乎没有提取到空间信息,但是考虑到我们之前说到的,通道域是空间域信息的层次化扩散。因此通过设置不同方向的shift卷积核,可以将输入张量不同通道进行平移,随后配合1x1卷积实现跨通道的信息融合,即可实现空间域和通道域的信息提取。
我们发现shift卷积的本质是特定内存的访问,可学习参数只是集中在1x1卷积操作中。因此如果实现得当,shift卷积是不占用额外的计算量和参数量的,结合shift卷积,只使用1x1卷积即可提取到结构化层次化的空间域信息,因此大大减少了卷积网络设计的参数量和计算量。
然而我们注意到,对于一个卷积核大小为DKD_KDK,通道数为MMM的卷积核而言,其可能的搜索空间为(DK2)M(D_K^2)^M(DK2)M,在学习过程中穷尽这个搜索空间是不太现实的。为了减少搜索空间,[1]采用了一种简单的启发式设计:将MMM个通道均匀地分成DK2D_K^2DK2个组,我们将每个组称之为 平移组(shift group)。每个组有⌊M/DK2⌋\lfloor M/D_K^2\rfloor⌊M/DK2⌋个通道,这些通道都采用相同的平移方向。当然,有可能存在除不尽的情况,这个时候将会有一些通道不能被划分到任意一个组内,这些剩下的通道都称之为“居中”组,如Fig 2.3所示,其中心元素为1,其他为0,也即是对原输入不进行任何处理。
虽然通过这种手段大大缩小了搜索空间,但是仍然需要让模型学出如何将第mmm个通道映射到第n,n∈[0,⌊M/DK2⌋−1]n, n\in[0,\lfloor M/D_K^2\rfloor-1]n,n∈[0,⌊M/DK2⌋−1]个平移组的最佳排列规则,这仍然是一个很大的搜索空间。为了解决这个问题,以下需要提出一种方法,其能够使得shift卷积层的输出和输入是关于通道排序无关的。假设Kπ(⋅)\mathcal{K}_{\pi}(\cdot)Kπ(⋅)表示是在以π\piπ为通道排序的shift卷积操作,那么公式(2.1)可以表示为G~=Kπ(F)\tilde{G} = \mathcal{K}_{\pi}(F)G~=Kπ(F),如果我们在进行该卷积之前,先后进行两次通道排序,分别是Pπ1\mathcal{P}_{\pi_1}Pπ1和Pπ2\mathcal{P}_{\pi_2}Pπ2,那么我们有:
G~=Pπ2(Kπ(Pπ1(F)))=(Pπ2∘Kπ∘Pπ1)(F)(2.3)\tilde{G} = \mathcal{P}_{\pi_2}(\mathcal{K}_{\pi}(\mathcal{P}_{\pi_1}(F))) = (\mathcal{P}_{\pi_2} \circ \mathcal{K}_{\pi} \circ \mathcal{P}_{\pi_1})(F) \tag{2.3} G~=Pπ2(Kπ(Pπ1(F)))=(Pπ2∘Kπ∘Pπ1)(F)(2.3)
其中∘\circ∘表示算子组合。令P1(⋅)\mathcal{P}_1(\cdot)P1(⋅)和P2(⋅)\mathcal{P}_2(\cdot)P2(⋅)分别表示1x1卷积操作,我们有式子(2.4)
P^1=P1∘Pπ1P^2=P2∘Pπ2(2.4)\begin{aligned} \hat{P}_1 &= \mathcal{P}_1 \circ \mathcal{P}_{\pi_1} \\ \hat{P}_2 &= \mathcal{P}_2 \circ \mathcal{P}_{\pi_2} \end{aligned} \tag{2.4} P^1P^2=P1∘Pπ1=P2∘Pπ2(2.4)
这一点不难理解,即便对1x1卷积的输入进行通道排序重组,在学习过程中,通过算法去调整1x1卷积的参数的顺序,就可以通过构造的方式,实现P^x\hat{\mathcal{P}}_{x}P^x和Px\mathcal{P}_{x}Px之间的双射(bijective)。如式子(2.5)所示,就结论而言,不需要考虑通道的排序,比如只需要依次按着顺序赋值某个平移组,使得其不重复即可。通过用1x1卷积“三明治”夹着shift卷积的操作,从理论上可以等价于其他任何形式的通道排序后的结果。这点比较绕,有疑问的读者请在评论区留言。
G=(P2∘Pπ2∘Kπ∘Pπ1∘P1)(F)=((P2∘Pπ2)∘Kπ∘(Pπ1∘P1))(F)=(P^2∘Kπ∘P^1)(F)(2.5)\begin{aligned} G &= (\mathcal{P}_{2} \circ \mathcal{P}_{\pi_2} \circ \mathcal{K}_{\pi} \circ \mathcal{P}_{\pi_1} \circ \mathcal{P}_1)(F) \\ &= ((\mathcal{P}_{2} \circ \mathcal{P}_{\pi_2}) \circ \mathcal{K}_{\pi} \circ (\mathcal{P}_{\pi_1} \circ \mathcal{P}_1))(F) \\ &= (\hat{\mathcal{P}}_2 \circ \mathcal{K}_{\pi} \circ \hat{\mathcal{P}}_1)(F) \end{aligned} \tag{2.5} G=(P2∘Pπ2∘Kπ∘Pπ1∘P1)(F)=((P2∘Pπ2)∘Kπ∘(Pπ1∘P1))(F)=(P^2∘Kπ∘P^1)(F)(2.5)
根据以上讨论,根据shift算子构建出来的卷积模块类似于Fig 2.4所示,注意到蓝色实线块的1x1 conv -> shift kernel -> 1x1 conv
正是和我们的讨论一样的结构,而Identity
块则是考虑到仿照ResNet的设计补充的short cut
链路。蓝色虚线块的shift
块是实验补充的一个设计,存在虚线部分的shift块的设计称之为SC2SC^2SC2结构,只存在实线部分的设计则称之为CSCCSCCSC结构。
shift卷积算子的有效性在文章[1]设置了很多实验进行对比,这里只给出证实其在分类任务上精度和计算量/参数量的一个比较,如Fig 2.5所示,我们发现shift算子的确在计算量和参数量上有着比较大的优势。
在[7]中有shift卷积算子前向和反向计算的cuda代码,其主要操作就是进行卷积输入张量的访存选择。有兴趣的读者可以自行移步去阅读。
Update 20201115:
在这里回答一位来自于知乎朋友的问题,链接来源:https://zhuanlan.zhihu.com/p/272732283
ID:fighting
shift操作理论上就是点乘一个one-hot矩阵(只有一个位置为1,其余位置为0),M个channel的shift kernel为D_K2M,为了降低搜索空间,对M个channel分组为M/D_K^2,每个组内采用一个one-shot矩阵。也即是说仅需要将M个channel中的每一个channel给出它属于这些组中的一个组。为了使得这么多group channel在输入输出时排序无关(仅对每一个channel考虑将其映射为哪一个shift 矩阵,但是与这个shift矩阵所在的group channel的位置没有关系,也就是所有的group channel地位一样,仅仅考虑一种排序方式即可,比如按照划窗的顺序从上到下的group channel对应的shift矩阵),就人为的shuffle channel,但shuffle channel不可导,将shuffle操作和11卷积相结合。所以最终推导出来的式子含义:先对输入进行11卷积和channel shuffle,然后执行某种特定排序的shift 移动,再对输出执行shuffle channel和11卷积。
两个疑问:1,对输出还要在执行shuffle和11卷积,是为了啥呢?2,shift 操作是操作内存实现的,怎么实现的呢?
→\rightarrow→ 回答:你好,我认为这里其实并不需要对通道进行shuffle打乱,因为既然根据式子(2.3-2.5)的推导可以得知
这么多group channel在输入输出时排序无关
那么我只需要固定某个特定的索引顺序即可,最简单的方式如Fig a1所示,按行列排列的顺序遍历设置即可,并不需要对其进行shuffle,因为可以证实其本身都是可以通过结合1x1卷积的方式学习出来的(不同的索引顺序学习出来的卷积参数不同,但是如果看成整体的话,它们是等价的)。因此文章里面应该是不需要进行shift组的排序shuffle的。
说到如何实现shift的访存优化机制,我们可以先看看shift-gcn是怎么做实现的,具体见文章[8]。当然,本文并不是采用shift-gcn定义的那种shift图卷积,我们回到开源的[7]中的具体代码段进行分析。我不是很熟悉cuda编程,只能作初步的分析,比如代码段[9]:
__global__ void shiftnet_cuda_moduloshift3x3_nchw_float32_kernel_tilein16x16_tileout14x14(float *src,float *dst,int num_h_tiles,int num_w_tiles,int batch_sz,int channels,int height,int width)
{__shared__ float cache[256];const int num_blocks = batch_sz * channels * num_h_tiles * num_w_tiles;const int num_threads = blockDim.x * num_blocks;const int rd_chans = (channels / 9) * 9;for (int idx = threadIdx.x + blockDim.x * blockIdx.x;idx < num_threads; idx += blockDim.x * gridDim.x){const int w_tile_idx = (idx / 256) % num_w_tiles;const int h_tile_idx = ((idx / 256) / num_w_tiles) % num_h_tiles;const int tile_ch = (((idx / 256) / num_w_tiles) / num_h_tiles) % channels;const int tile_batch_idx = ((((idx / 256) / num_w_tiles) / num_h_tiles) / channels) % batch_sz;const int w_shift = ((tile_ch % 3) - 1) * (tile_ch < rd_chans);const int h_shift = (((tile_ch / 3) % 3) - 1) * (tile_ch < rd_chans);const int w_tile_off = threadIdx.x % 16;const int h_tile_off = threadIdx.x / 16;const int w_idx = w_tile_off - 1 + 14 * w_tile_idx;const int h_idx = h_tile_off - 1 + 14 * h_tile_idx;const int buf_idx = w_idx + width * (h_idx + height * (tile_ch + channels * tile_batch_idx));if (w_idx >= 0 && w_idx < width && h_idx >= 0 && h_idx < height) {cache[threadIdx.x] = src[buf_idx];} else {cache[threadIdx.x] = 0.0f;}__syncthreads();if (w_tile_off >= 1 && w_tile_off < 15 && h_tile_off >= 1 && h_tile_off < 15) {if (w_idx >= 0 && w_idx < width && h_idx >= 0 && h_idx < height) {const int cache_idx = (w_tile_off + w_shift) + 16 * (h_tile_off + h_shift);dst[buf_idx] = cache[cache_idx];}}__syncthreads();}
}
它的实现并没有完全优化完全,因为他没有结合后续的1x1卷积进行优化,他只是进行了将某行(或列)置位0,代码是:
if (w_idx >= 0 && w_idx < width && h_idx >= 0 && h_idx < height) {cache[threadIdx.x] = src[buf_idx];} else {cache[threadIdx.x] = 0.0f;}}
然后进行移位:
if (w_tile_off >= 1 && w_tile_off < 15 && h_tile_off >= 1 && h_tile_off < 15) {if (w_idx >= 0 && w_idx < width && h_idx >= 0 && h_idx < height) {const int cache_idx = (w_tile_off + w_shift) + 16 * (h_tile_off + h_shift);dst[buf_idx] = cache[cache_idx];}}
我觉得这个并不是最优化后的结果,最优化应该是指定某个方块(比如不包括第一列的其他所有数据),与后续的1x1卷积联合起来,只对这些方块进行卷积,这才是真正的访存优化,显然这样难度太大,因此它的实现并没有这样做。(正如我所说的,我不是很熟悉cuda,有谬误请指出。)
最后需要指出的是,它并不是one-hot矩阵点乘,而是卷积。
以上。
Reference
[1]. Wu, B., Wan, A., Yue, X., Jin, P., Zhao, S., Golmant, N., … & Keutzer, K. (2018). Shift: A zero flop, zero parameter alternative to spatial convolutions. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (pp. 9127-9135).
[2]. Cheng, K., Zhang, Y., He, X., Chen, W., Cheng, J., & Lu, H. (2020). Skeleton-Based Action Recognition With Shift Graph Convolutional Network. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 183-192).
[3]. https://github.com/peterhj/shiftnet_cuda_v2
[4]. Howard, Andrew G., Menglong Zhu, Bo Chen, Dmitry Kalenichenko, Weijun Wang, Tobias Weyand, Marco Andreetto, and Hartwig Adam. “Mobilenets: Efficient convolutional neural networks for mobile vision applications.” arXiv preprint arXiv:1704.04861 (2017).
[5]. Chollet, F. (2017). Xception: Deep learning with depthwise separable convolutions. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 1251-1258).
[6]. https://baike.baidu.com/item/%E5%B1%80%E9%83%A8%E6%80%A7%E5%8E%9F%E7%90%86
[7]. https://github.com/peterhj/shiftnet_cuda_v2/blob/master/src/shiftnet_cuda_kernels.cu
[8]. https://fesian.blog.csdn.net/article/details/109644297
[9]. https://github.com/peterhj/shiftnet_cuda_v2/blob/4d471bd744751ff0fd6cf5acd518e9484cc70a98/src/shiftnet_cuda_kernels.cu#L25
访存指的是从内存中取出操作数加载到寄存器中,通常访存时间远比计算时间长,大概是数量级上的差别。 ↩︎
紧致卷积网络设计——Shift卷积算子相关推荐
- An Empirical Evaluation of Generic Convolutional and Recurrent Networks(中文版+注释)(时序预测)(TCN时域卷积网络)
TCN (An Empirical Evaluation of Generic Convolutional and Recurrent Networks 中文版+注释)百度网盘链接一般卷积和递归网络的 ...
- 卷积网络的通用设计原则
卷积网络的通用设计原则 卷积网络的通用设计原则 卷积网络的通用设计原则 避免表示瓶颈(representational bottlenecks),尤其在网络早期. 前馈神经网络可以用输入到输出的无环图 ...
- sift论文_卷积神经网络设计相关论文
最近梳理了一下卷积神经网络设计相关的论文(这个repo现在只列出了最重要的一些论文,后面会持续补充): Neural network architecture designgithub.com 1. ...
- Survey | 基于图卷积网络的药物发现方法
本期介绍2019年6月发表在Briefings in Bioinformatics的综述,该综述由康奈尔大学等机构的研究人员撰写,系统总结了GCN及其在药物发现方面的最新进展,重点是与药物相关的应用: ...
- 北大图灵班本科生带来动画CG福音,「最懂骨骼的卷积网络」,无需配对样本实现动作迁移 | SIGGRAPH...
鱼羊 金磊 发自 凹非寺 量子位 报道 | 公众号 QbitAI 我有一个动画形象,我有一套人体动作,可想要把它们组合成真正的动画,可不是 1+1 这么简单. 别看这体型迥异的三位动作整齐划一,支撑动 ...
- 论文翻译:搜索人脸活体检测的中心差异卷积网络及实现代码
搜索人脸活体检测的中心差异卷积网络 摘要 1. 绪论 2. 相关工作 人脸活体检测 卷积运算符 神经架构搜索 3. 方法论 3.1 中心差分卷积 基本卷积 基本卷积结合中心差分操作 中心差分卷积的实现 ...
- 图卷积网络 GCN Graph Convolutional Network(谱域GCN)的理解和详细推导
文章目录 1. 为什么会出现图卷积神经网络? 2. 图卷积网络的两种理解方式 2.1 vertex domain(spatial domain):顶点域(空间域) 2.2 spectral domai ...
- STGCN时空图卷积网络:用于交通预测的深度学习框架
时空图卷积网络:用于交通预测的深度学习框架 及时准确的交通预测对城市交通控制和引导至关重要.由于交通流的高度非线性和复杂性,传统的方法不能满足中长期预测任务的要求,往往忽略了空间和时间的相关性.本文提 ...
- 【CV】膨胀卷积详解以及时间卷积网络TCN论文笔记和源码实现
这篇博文分为两部分.第一部分详细讲解了TCN模型(Temporal Convolutional Network)中涉及的1D卷积,因果卷积,膨胀卷积中设计的计算,非常值得一看,有醍醐灌顶的作用.第二部 ...
最新文章
- javassist学习笔记
- POJ 2749 Building roads
- redis伪集群脚本
- Java 垃圾回收算法之G1
- django后台多页面分页逻辑python代码
- linux内核中打开文件 及属性控制
- Java基础学习总结(181)——Nacos、Apollo、Config配置中心如何选型?
- uniapp 日期选择器_uniapp实现横向滚动选择日期
- 更新T1表,要添加一个条件A,但T1表没有A字段
- 关于Lasso回归的一个例子
- Atitit 减少财政支出----获取商家商业机构的补贴措施 attilax大总结.docx
- 主板检测卡常见错误代码:0D~0F
- 中国电信物联网平台物理模型创建
- SAP HR(一、模块基础概念介绍)
- windows server winrm介绍
- 细胞穿膜肽( CPPs)偶联肽核酸H region-PNA|Arg-PNA|Lys-PNA|Cationic-PNA|47Tat57-PNA的特性
- Java 笔试强训 牛客网选择编程题 02
- 4-17 定义一个长方形类,定义 求周长和面积的方法,然后定义一个测试类,进行测试。
- 紫薇星“Jigsaw Puzzle”
- 推荐一些实用的谷歌浏览器翻译插件