转载自知乎
感谢 @Sebastian 提供的完整实现https://github.com/sebgao/cTensor
在之前的一篇笔记中推导了卷积运算的im2col方法的实现:https://zhuanlan.zhihu.com/p/63974249
numpy.lib.stride_tricks.as_strided(x, shape=None, strides=None, subok=False, writeable=True)
X = np.arange(9, dtype=np.int32).reshape(3,3)
print(X)
'''
[[0 1 2]
[3 4 5]
[6 7 8]]
'''
A = np.lib.stride_tricks.as_strided(X, shape=(2,2,2,2), strides=(12,4,12,4))
print(A)
'''
[[[[0 1]
[3 4]]
[[1 2]
[4 5]]]
[[[3 4]
[6 7]]
[[4 5]
[7 8]]]]
'''
X = np.arange(16, dtype=np.int32).reshape(4,4)
print(X)
'''
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]]
'''
A = np.lib.stride_tricks.as_strided(X, shape=(2,2,3,3), strides=(16,4,16,4))
print(A)
'''
[[[[ 0 1 2]
[ 4 5 6]
[ 8 9 10]]
[[ 1 2 3]
[ 5 6 7]
[ 9 10 11]]]
[[[ 4 5 6]
[ 8 9 10]
[12 13 14]]
[[ 5 6 7]
[ 9 10 11]
[13 14 15]]]]
'''
X1 = np.arange(1,19, dtype=np.int32).reshape(2,3,3)
print(X1)
'''
[[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]]
[[10 11 12]
[13 14 15]
[16 17 18]]]
'''
A1 = as_strided(X1, shape=(2,2,2,2,2), strides=(*X1.strides, *X1.strides[-2:]))
print(A1)
'''
[[[[[ 1 2]
[ 4 5]]
[[ 2 3]
[ 5 6]]]
[[[ 4 5]
[ 7 8]]
[[ 5 6]
[ 8 9]]]]
[[[[10 11]
[13 14]]
[[11 12]
[14 15]]]
[[[13 14]
[16 17]]
[[14 15]
[17 18]]]]]
'''
完全正确
桶里,当X是四维数组时(X.shape = (N, C, H, W)),卷积核的宽高为:kh,kw
那么可以确定A的shape为:(N,C,?,?,kh,kw),至于中间的两个数值怎么确定?还是用以前的公式:
def split_by_strides(X, kh, kw, s):
N, C, H, W = X.shape
oh = (H - kh) // s + 1
ow = (W - kw) // s + 1
strides = (*X.strides[:-2], X.strides[-2]*s, X.strides[-1]*s, *X.strides[-2:])
A = as_strided(X, shape=(N,C,oh,ow,kh,kw), strides=strides)
这里没有padding,因为在这之前就X就已经认为是pad过了
还有个小问题,如果X.shape不是上面那个样子怎么弄?这里举个其他的例子:
def split_by_strides(X, kh, kw, s):
N, H, W, C = X.shape
oh = (H - kh) // s + 1
ow = (W - kw) // s + 1
shape = (N, oh, ow, kh, kw, C)
strides = (X.strides[0], X.strides[1]*s, X.strides[2]*s, *X.strides[1:])
A = as_strided(X, shape=shape, strides=strides)
return A
其实就是将shape改一下,strides参数调整一下就行了
现在蛋糕已经切好了,那么是直接吃吗?不过还有卷积核的问题没有解决,如果把A经过transpose和reshape转换成二维矩阵,再把卷积核如法炮制,那么直接用np.dot就可以实现了,得到结果之后再reshape回来也就是想要的feature map了
可是,这么一顿操作下来,好不容易去掉for循环提高的效率,不是又被拉回来了?有没有一行代码的解决方案?这时候就需要用到numpy的另一个函数tensordot了
不过这样就说来话长了,足够另起一篇了,而且单独一篇篇幅太长也不利于阅读,这里先把代码贴出来,然后回头再介绍tensordot
设卷积核的shape为:(kh,kw,C,kn),X.shape:(N,H,W,C),计算feature_map的代码如下:
kh, kw, C, kn = self.filters.shape
X_split = split_by_strides(X, kh, kw, s) # X_split.shape: (N, oh, ow, kh, kw, C)
feature_map = np.tensordot(X_split, self.filters, axes=[(3,4,5), (0,1,2)]) + self.bias
可以检验一下,featrue_map的shape为:(N,oh,ow,kn)
最后就是上面代码中用到的np.tensordot函数了,这个和np.dot类似,不过np.dot是矩阵乘法,tensordot是张量的点乘,简单的写一下用法,先来看看官方手册上tensordot的介绍:
numpy.tensordot
( a, b, axes=2)
其中参数a,b都是np数组;axes是给定的轴,axes赋整数的情况不在讨论范围之内,这里讨论的是其为数组的情况,先看最简单的例子:
>>> A = np.random.randint(0,9,(3,4))
>>> B = np.random.randint(0,9,(4,5))
>>> C = np.tensordot(A, B, [1, 0])
>>> C.shape
(3, 5)
>>> C
array([[ 37, 23, 77, 19, 16],
[ 83, 32, 116, 40, 51],
[ 86, 43, 127, 47, 47]])
>>> np.dot(A, B)
array([[ 37, 23, 77, 19, 16],
[ 83, 32, 116, 40, 51],
[ 86, 43, 127, 47, 47]])
这个和np.dot的结果虽然没有区别,不过为了说明其中的不同,还是用另一种角度来进行抽象,先将上面的两个二维数组分别抽象成一个列向量和一个行向量:
说了这么多,主要是为了接下来的内容做铺垫,再来看一个复杂点的例子:
>>> A = np.random.randint(0,9,(3,4,5))
>>> B = np.random.randint(0,9,(4,5,2))
>>> np.tensordot(A, B, [(1,2), (0,1)])
array([[290, 146],
[308, 164],
[316, 205]])
经过上面的解释,想必你肯定知道这些数值是怎么来的了,来验证一下:
>>> np.sum(A[0] * B[:,:,0])
290
>>> np.sum(A[2] * B[:,:,1])
205
上面分别验证了(0,0)和(2,1)位置的值,都完全正确
所以,tensordot的功能就是对于A, B两个多维数组,按给定的axes得到分别得到子数组subA和subB,然后这两个子数组进行点乘运算
官方手册上有个例子,这里搬过来,应该能更好的理解:
>>> a = np.arange(60.).reshape(3,4,5)
>>> b = np.arange(24.).reshape(4,3,2)
>>> c = np.tensordot(a,b, axes=([1,0],[0,1]))
>>> c.shape
(5, 2)
>>> c
array([[ 4400., 4730.],
[ 4532., 4874.],
[ 4664., 5018.],
[ 4796., 5162.],
[ 4928., 5306.]])
>>> # A slower but equivalent way of computing the same... 一个更慢但效果一样的方法
>>> d = np.zeros((5,2))
>>> for i in range(5):
... for j in range(2):
... for k in range(3):
... for n in range(4):
... d[i,j] += a[k,n,i] * b[n,k,j]