撰文 | zzk

1

从卷积层说起

熟悉CNN的小伙伴应该知道卷积是一个很常用也很重要的操作,CNN里的卷积和信号处理的卷积并不是一回事,CNN的卷积是做一种二维的互相关运算,以《动手学深度学习》5.1章为示例:

《动手学深度学习》5.1.1. 二维互相关运算

窗口内的元素和卷积核相乘,求和得到输出元素,一份naive的代码如下(同来自《动手学深度学习》

from mxnet import autograd, nd
from mxnet.gluon import nndef corr2d(X, K):  # 本函数已保存在d2lzh包中方便以后使用h, w = K.shapeY = nd.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))for i in range(Y.shape[0]):for j in range(Y.shape[1]):Y[i, j] = (X[i: i + h, j: j + w] * K).sum()return Y

这里是借助了numpy array的索引特性来写的,如果在c++里写,需要的循环层数更多(会在后面进行展示)。而这种循环计算的写法效率并不高,会极大拖慢卷积运算的速度。

2

初见img2col

为了提高卷积运算的速度,img2col算法被发明了出来的,它的本质是用矩阵乘法来等价卷积运算,示例图如下:

图源来自微软AI-system仓库

https://github.com/microsoft/AI-System/blob/main/docs/SystemforAI-4-Computer%20architecture%20for%20Matrix%20computation.pdf 这是微软的AISystem仓库对应的章节,强烈推荐大家去学习(本人鸽了好久没看完)

可以看到img2col把输入特征图进一步展开,然后把filter展平,两者做矩阵乘,得到了相同的结果。

3

理解img2col

看了上面的图可能还是不能理解这是怎么展开的,这里我们会介绍下:

  • 假设输入特征图为(N, Cin, H, W),卷积核参数为(K, K, Cin, Cout), 输出特征图的长宽为(Oh, Ow)

经过img2col后,输入特征图会变换为 (N, Cin*K*K, Oh*Ow) 这么一个三维向量。

此外卷积核我们会reshape成一个二维向量:(Cout, K*K*Cin)

而这两个向量可以做一个矩阵乘,输出向量为(N, Cout, Oh*Ow),这也是我们预期的卷积结果。

img2col算法是一种用空间换时间的做法,将输入进行变换后,显然它所占用的显存空间变大了,好处则是可以借助矩阵乘,快速地完成卷积运算。

下面我会结合darknet的原生img2col和一篇博客来进行相关讲解

代码:

https://github.com/pjreddie/darknet/blob/master/src/im2col.c

博客:

https://blog.csdn.net/caicaiatnbu/article/details/100515321

4

img2col源码

darknet的img2col其实是搬运自caffe的,我们这里为了方便理解,以简单的CPU版本为例子,且不考虑batch维度。

为了让读者能够快速运行上代码,这里讲解我以自己写的一版darknet img2col来作为示例。

def darknet_img2col(data, channels, height, width, ksize, stride, pad):out_h = int((height + 2*pad - ksize) / stride) + 1out_w = int((width + 2*pad - ksize) / stride) + 1channels_cols = channels*ksize*ksizeout_shape = (channels_cols, out_h*out_w)elem_cnt = out_shape[0] * out_shape[1]out_array = np.zeros(shape=elem_cnt, dtype=np.float32)

首先我们可以确定输出tensor的各个维度,其中out_hout_w就是输出的高,宽,采用的是卷积输出的公式:

PyTorch的Unfold文档

channel_cols则是之前我们提到的,img2col会把第二个维度变换为C_in*K*K

然后进入到次数为channel_cols的for循环

for c in range(channels_cols):# 分别计算一个k*k的窗口内,h,w的偏移量kh_offset = (c // ksize) % ksizekw_offset = c % ksize# 计算当前处理的通道indexc_im = c // ksize // ksize

然后我们需要根据当前处理的输出元素index,来获取对应输入的元素

for h in range(out_h):for w in range(out_w):im_row = kh_offset + h * strideim_col = kw_offset + w * strideindex = (c * out_h + h) * out_w + w

im_row的计算方式逻辑是:当前处理的输入元素窗口起始点:即h*stride,然后加上窗口内的kh_offset偏移量

取元素逻辑

而index的计算比较容易,因为输出是(C, H, W),对应的一维index那就是

当前channel*Oh*Ow + 当前h*Ow + 当前w

最后我们再将元素取出来,赋给out数组。然后再将一维的out数组reshape成我们前面推导得到的out_shape

out_array[index] = im2col_get_pixel(data, height, width, c_im, im_row, im_col,  pad)out_array = np.reshape(out_array, out_shape)return out_array

img2col_get_pixel是一个合法取元素的函数,如果出现越界范围(比如小于0,或者大于Oh),那么就是取到padding的部分,此时我们返回0。

def im2col_get_pixel(im, height, width, channel, row, col, pad):row -= padcol -= padif row < 0 or col < 0 or row >= height or col >= width:return 0return im[int(col + width * (row + height * channel))] # batch*w*h*c + width*height*channel + width*row + col

我们可以简单构造一个数组来验证结果(以微软AI-System课程的示例作为输入)

x = np.arange(1, 10).astype(np.float32)
out = darknet_img2col(x, channels=1, height=3, width=3, ksize=2, stride=1, pad=0)

输出结果符合预期:

[[1. 2. 4. 5.][2. 3. 5. 6.][4. 5. 7. 8.][5. 6. 8. 9.]]

col2img

col2img则是img2col的逆过程,有兴趣的读者可以参考下这个博客:

https://blog.csdn.net/caicaiatnbu/article/details/102626135

在后面的oneflow实现里会有更完整的图例用于理解

OneFlow对应的实现

PR地址:

https://github.com/Oneflow-Inc/oneflow/pull/5675

5

OneFlow版本的Unfold

在深度学习框架里,img2col和col2img在Pytorch里还有另外的名字,也就是Unfold和Fold。通常想自己自定义一些跟卷积相关的骚操作时候,就经常会用到这两个Op。

我们假设输入是一个(1, 2, 4, 4)的张量,但在框架内部,我们通常都是以一个一维数组来存储的,如下图所示:

输入内存排布

然而我们需要对应的高维数组索引,OneFlow内部有一个NdIndexHelper类,构造的时候我们可以传入高维shape,然后调用OffsetToNdIndex来进行一维offset到高维索引的转换。

这里我们分别给输入,输出Tensor构造一个NdIndexHelper

in_index_helper = NdIndexOffsetHelper<INDEX_T, kInputNDim>(input_dims); // 格式为(N, C, H, W)
out_index_helper = NdIndexOffsetHelper<INDEX_T, kOutputNDim>(output_dims); // 格式为(N, C, Kh, Kw, Oh, Ow)

比较特别的是,我们把输出构造成一个6维的形式

接着就是从输出的视角来推导应该取哪一个输入元素

// index_a format: (N, C, di, hi, wi, db, hb, wb) or (N, di, hi, wi, db, hb, wb, C)
// index_b format: (N, C, D, H, W) or (N, D, H, W, C)
// return: true indicates out-of-bound, otherwise in-bound
template<typename INDEX_T, int NDIM, int SDIM>
OF_DEVICE_FUNC bool UnfoldIndexTransform(const UnfoldParams<INDEX_T, NDIM, SDIM>& params,const INDEX_T* index_a, INDEX_T* index_b) {// batch dim index transformindex_b[0] = index_a[0];// channel dim index transformusing ParamType = UnfoldParams<INDEX_T, NDIM, SDIM>;index_b[ParamType::kInputChannelDim] = index_a[ParamType::kOutputChannelDim];
// spatial dim index transform// D,H,W spatial dim index transformfor (int64_t d = 0; d < NDIM; ++d) {INDEX_T idx = index_a[SDIM + NDIM + d] * params.stride[d]+ index_a[SDIM + d] * params.dilation[d] - params.padding[d];if (idx < 0 || idx >= params.dims[d]) return true;index_b[SDIM + d] = idx;}return false;
}
  • 模板参数 INDEX_T表示Index的数据类型(可以有int32_t, int64_t),NDIM表示处理几维数据(这里我们是2维),SDIM则是决定通道维所在位置,SDIM=1是NHWC格式,SDIM=2则是NCHW格式(这里我们取2)

  • 输入参数 index_a表示输出的NdIndexHelper,index_b则表示的是输入的NdIndexHelper

从前面我们可以看到N,C这两个维度的index是不变的,所以我们直接给过去

然后进入一个次数为NDIM==2的循环

这里index的计算是从输出往输入推,公式是(以H为例):

Oh*stride_h + kh*dilation_h - padding_h

计算得到输入的index,如果小于0或者大于输入的宽高,则说明是到了padding的地方,我们直接return true,以表示越界。如果能取到元素,我们则将这个index赋给index_b即输入的NdIndexHelper,且return false。

这部分的分解操作可参考下图:

从输出推导的好处就是整个运算是一个elementwise的操作,我们可以用输出tensor的元素个数做一个循环完成整个unfold操作。

template<typename T, typename INDEX_T, int NDIM, int SDIM>
__global__ void CudaUnfoldForward(UnfoldParams<INDEX_T, NDIM, SDIM> params, const T* in, T* out) {CUDA_1D_KERNEL_LOOP_T(INDEX_T, out_offset, params.out_elem_cnt) {using ParamType = UnfoldParams<INDEX_T, NDIM, SDIM>;INDEX_T in_index[ParamType::kInputNDim] = {0};INDEX_T out_index[ParamType::kOutputNDim] = {0};params.out_index_helper.OffsetToNdIndex(out_offset, out_index);if (!UnfoldIndexTransform<INDEX_T, NDIM, SDIM>(params, out_index, in_index)) {INDEX_T in_offset = params.in_index_helper.NdIndexToOffset(in_index);out[out_offset] = in[in_offset];} else {out[out_offset] = static_cast<T>(kUnfoldPaddingValue);}}
}
  • 首先根据offset来计算当前处理输出元素的NdIndex

  • 然后判断UnfoldIndexTransform该方法的返回值

  • 如果为false,则说明我们可以取到输入元素,将其index转换为1d的offset,并赋值给输出

  • 如果为true,则越界,我们填充先前设定好的一个padding_value(0)

至此整个img2col流程已经完成,整体操作可参考下图:

6

OneFlow版本的Fold

Fold就是将每一列填充回到kxk的地方

如果能理解前面的Unfold,那么这里的Fold也能很容易的理解。只是操作的元素反过来而已。

template<typename T, typename INDEX_T, int NDIM, int SDIM>
__global__ void CudaFoldForward(FoldParams<INDEX_T, NDIM, SDIM> params, const T* input_ptr,T* output_ptr) {CUDA_1D_KERNEL_LOOP_T(INDEX_T, in_offset, params.in_elem_cnt) {using ParamType = FoldParams<INDEX_T, NDIM, SDIM>;INDEX_T in_index[ParamType::kInputNDim] = {0};INDEX_T out_index[ParamType::kOutputNDim] = {0};params.in_index_helper.OffsetToNdIndex(in_offset, in_index);if (!FoldIndexTransform<INDEX_T, NDIM, SDIM>(params, in_index, out_index)) {INDEX_T out_offset = params.out_index_helper.NdIndexToOffset(out_index);XPUAdd<T>::Invoke(&input_ptr[in_offset], &output_ptr[out_offset]);} else {continue;}}
}

沿用前面的索引映射逻辑,我们进入一个循环次数为输入元素个数的循环体,计算得到当前offset对应的输入NdIndex。

如果FoldIndexTransform返回的是false,则计算输出的offset,并使用原子加atomic add,把输入元素累加到该输出位置。

7

小结

这部分代码是接手同事写一半的代码完成的,不得不说同事的构思真的很巧妙,通过模板参数能够拓展1d 2d 3d,nchw, nhwc各种格式,尽管直观上不太好理解。

darknet版本(Caffe同款)是比较直观的帮助新手入门的img2col算法,可以结合那两篇CSDN博客来理解整个过程。

其他人都在看

  • OneFlow v0.5.0正式上线

  • GPU架构演进十年,从费米到安培

  • 玩转大模型,是时候展示你真正的技术了

  • 动态调度的“诅咒”| 原有深度学习框架的缺陷③

  • OneFlow框架添加算子实践:expand和repeat

  • 计算机架构史上的一次伟大失败,多数人都不知道

点击“阅读原文”,欢迎下载体验OneFlow新一代开源深度学习框架

基于OneFlow实现Unfold、Fold算子相关推荐

  1. 拉普拉斯噪声公式_一个基于四方向的拉普拉斯算子的四阶偏微分去噪方法(精)...

    一个基于四方向的拉普拉斯算子的四阶偏微分去噪方法 曾超,王美清 ( 福州大学数学与计算机科学学院,福建 福州 350002) 摘要:本文将 " 四方向 " 引入拉普拉斯算子 ( 这 ...

  2. 岁末年初,为你打包了一份技术合订本

    "Show me the code"有时并不足够,你还需要注释辅助去深入理解代码背后的思想和逻辑. 作为一个开源社区,OneFlow不止希望通过公开的源码与开发者和用户交流,还希望 ...

  3. OneFlow 如何做静态图的算子对齐任务

    文章目录 1 前言 2 OneFlow 的 Graph 算子对齐概述 3 Graph 模式下自动测试实现原理 3.1 AutoTest 流程介绍 3.2 Graph 模式如何伴随 Eager 模式做算 ...

  4. 基于深度强化学习的进化多目标优化自适应算子选择

    进化算法(EA)已经成为多目标优化的最有效技术之一,其中已经开发了许多变异算子来处理具有各种困难的问题. 虽然大多数EA始终使用固定的运算符,但 为新问题确定最佳EA 是一个劳动密集型过程. 因此,最 ...

  5. 深度学习框架量化感知训练的思考及OneFlow的解决方案

    作者 | BBuf 原文首发于公众号GiantPandaCV 0x0.总览 相信不少小伙伴都了解或者使用了一些深度学习框架比如PyTorch,TensorFlow,OneFlow(也是笔者目前正在参与 ...

  6. 深度学习框架量化感知训练的思考及OneFlow的一种解决方案

    [GiantPandaCV导语]这篇文章分享的是笔者最近在OneFlow做的一个项目,将Pytorch FX移植到OneFlow之后实现了自动量化感知训练动态图模型(在Pytorch和OneFlow中 ...

  7. OpenCV(十五)边缘检测1 -- Sobel算子(一阶微分算子,X、Y方向边缘检测)

    目录 一.边缘检测基础理论 1.作用: 2.分类 1.基于搜索 2.基于零穿越 3.算子比较 二.Sobel算子基础理论 1.作用 2.原理及推导 3.更详细推导 4.Sobel函数 二.实战 1.对 ...

  8. UA MATH565C 随机微分方程V Markov Family的算子

    UA MATH565C 随机微分方程V Markov Family的算子 函数的算子 测度的算子 Homogeneous Markov Family 函数的算子 这一讲正式介绍Markov Famil ...

  9. Spark算子总结版

    Spark的算子的分类 从大方向来说,Spark 算子大致可以分为以下两类: 1)Transformation 变换/转换算子:这种变换并不触发提交作业,完成作业中间过程处理. Transformat ...

最新文章

  1. 2021年大数据HBase(二):HBase集群安装操作
  2. 关于oracle的基础增删改查操作总结
  3. PacBio RS系列已被淘汰,PacBio Sequel成为三代测序最新起跑线。
  4. linux之SQL语句简明教程---IN
  5. 2011年c语言二级计算机考试,2011年9月全国计算机等级考试二级C语言机试
  6. sklearn模型预测性能评估(二)
  7. [C++STL]set容器用法介绍
  8. AngularJS中$timeout和$interval的用法详解
  9. 手把手教你0基础C语言速通
  10. WebView学习笔记
  11. Atitit 泛型原理与理解attilax总结
  12. 重装系统大法—WePE or 老毛桃
  13. DLL入口函数DllMain
  14. 【EduCoder答案】HTML——表单类的标签
  15. Vue使用Swiper看这一篇就够了
  16. 类似ftp文件服务器有哪些,FTP的替代品有哪些,你知道吗?
  17. DSSD : Deconvolutional Single Shot Detector论文阅读笔记
  18. 聚观早报 | iPhone接口将与安卓统一;《三体》动画定档12月3日
  19. 06_04_任务二:SSM拉勾教育后台管理系统(广告模块与用户模块)
  20. A non-optional actual argument must be present when invoking a procedure with an explicit interface

热门文章

  1. CSS的鼠标为手形: cursor:pointer;
  2. 在线新闻推荐网 Python+Django+Mysql开发技术 基于用户、物品的协同过滤推荐算法 个性化新闻推荐系统 协同过滤推荐算法在新闻网站中的运用 个性化推荐算法、机器学习、分布式大数据、人工智
  3. 怎样理解优秀代码的三个标准:可读性,可重用性,可扩展性?
  4. C#windows人事信息管理系统,员工评价系统
  5. python神经网络训练玩游戏_python – 如何训练神经网络来玩2048游戏?
  6. ICCV 2021 | High-Fidelity Pluralistic Image Completion with Transformers 阅读笔记(部分翻译)
  7. Python -- 让程序运行后不立即关闭窗口
  8. 如何用网页打开ipynb文件
  9. 关于CPU C-States 省电模式
  10. 阿里云——手把手教你搭建个人网站(上云良心品,细致到想哭)