在面试某公司的时候,面试官在考察完基本的算法知识后,开始进入手撕代码环节,递给我一摞纸以及一根笔,然后给了我这样一个要求:写一个卷积网络,步长和是否填充0自己考虑,但是要保证卷积后的feature map和卷积前的图像大小一致。。
def Conv2(img,W,H,kernel,3):
#img:输入图片;W,H:图片的宽和高;kernel:卷积核;3:代表3*3卷积。
#return:和输入图像尺寸大小相同的feature map
真贴心,框架都给写好了。
平常添加一个卷积都是直接调用tensorflow的,这下确实把我整懵逼了,总不能先:import tensorflow as tf
吧(重要的是自己万一祭出这个大杀器之后忽然发现忘了tensorflow添加卷积的函数调用格式岂不是更惨!)。
反正原理自己都知道,就打算还是不引入TensorFlow框架了,老老实实用底层的python语言配合常用的numpy包手撸一个吧。
首先分析了一下,要想卷积前后尺寸不变,那么就需要外层加一圈0。【这里是面试官的一个小坑,一定要保持卷积前后尺寸不变,就要自己计算出具体的padding数值】
于是先写了填充0的代码:
col = np.zeros(H)
raw = np.zeros(W + 2)
img = np.insert(img, W, values=col, axis=1)
img = np.insert(img, 0, values=col, axis=1)
img = np.insert(img, H, values=raw, axis=0)
img = np.insert(img, 0, values=raw, axis=0)
接着,记得本科时候用matlab写过各种和卷积原理完全相同的滤波器,那么,一个一个来呗。但是感觉这个知识点貌似已经还给老师了,此时内心已经祈祷本科教我数字图像处理的长龙欧巴附体。就在这个时候面试官已经问我写完了没,纳尼?这才刚开始好不好。只能硬着头皮靠自己了。
for i in range(H):
for j in range(W):
temp = img[i][0]*kernel[0][0]+img[i][1]*kernel[0][1]+img[i][2]*kernel[0][2]+img[i+1][0]*kernel[1][0]……
唉,等等,写到这里,我已经开始觉得自己后背一阵发凉。这玩意,小小的一张A4纸容不下我“伟大”的思想啊。如果是在我自己的编译器里,老子一行代码写成100行又如何?但是现在不行啊,就算不是我错了,是这个世界错了,但现在是在人家的地盘上写代码,咱得认怂不是?于是迅速抽出下边一张纸,用python切片简化了一下步骤。
temp = img[i:i + 3, j:j + 3]
刚写了这么一句,面试官已经开始不耐烦了,让我给他讲讲思路,大概看了看,说了句:我懂了。就过去了(恕我直言,我自己都不懂唉,可能内心已经开始鄙视我了,但是看我也写不出来啥了,给我个面子吧)。面试回来觉得这么简单的代码都写不出来太丢人了。于是在自家的地盘上写了一下,啊,自家的编译器真香……完整代码如下:
#手写卷积网络
import numpy as np
def Conv2(img, H, W, kernel, n):
# img:输入图片;W,H:图片的宽和高;kernel:卷积核。
# return:和输入图像尺寸大小相同的feature map;
# 卷积大小固定为3*3卷积,这里因为固定了卷积大小,所以写代码前可以直接确定:卷积步长为1,四周个填充一排0
col = np.zeros(H)
raw = np.zeros(W + 2)
img = np.insert(img, W, values=col, axis=1)
img = np.insert(img, 0, values=col, axis=1)
img = np.insert(img, H, values=raw, axis=0)
img = np.insert(img, 0, values=raw, axis=0)
res = np.zeros([H,W])##直接新建一个全零数组,省去了后边逐步填充数组的麻烦
for i in range(H):
for j in range(W):
temp = img[i:i + 3, j:j + 3]
temp = np.multiply(temp,kernel)
res[i][j] = temp.sum()
return (res)
if __name__ == '__main__':
A = np.array([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1]]) # 4行5列
ken = np.array([[2, 2, 2], [2, 2, 2], [2, 2, 2]])
print(Conv2(A, 4, 5, ken, 3))
最后输出:
[[ 8. 12. 12. 12. 8.]
[ 12. 18. 18. 18. 12.]
[ 12. 18. 18. 18. 12.]
[ 8. 12. 12. 12. 8.]]
代码还有几个不足:
最后,真心觉得编程真是个最容易犯眼高手低错误的工作,或许还是自己太菜了,本来觉得很简单的一个算法,包含写这篇文章,查各种函数,写代码等足足花了三个小时左右。
np.dot(A, B)
或@
对于二维矩阵,这就是线性代数中的矩阵相乘。而对于一维矩阵,会直接计算出两者的内积。但是,需要注意的是:如果一个列向量(本质上是一个维数为(1,2)的向量)和一个一维数组相乘,使用dot函数是会报错的。如下Python代码:
A = np.array([[1,2],[3,4]])
B = np.array([[5,6],[7,8]])
print("矩阵相乘:",np.dot(A,B))
print("一维向量内积:",np.dot(A[0],B[0]))
B1 = np.array([[5],[6]])
print("行乘列:",np.dot(A[0],B1))
print("列乘行:",np.dot(B1,A[0]))
输出结果如下:
矩阵相乘:array([[19, 22],
[43, 50]])
一维向量内积:17
列乘行:17
ValueError: shapes (2,1) and (2,) not aligned: 1 (dim 1) != 2 (dim 0)
np.multiply()
或 *
这个很好理解,实现对应元素相乘。还是以上边的A,B数组为例:
print("元素相乘1:",np.multiply(A,B))
print("元素相乘2:",A*B)
输出都是相同的:
array([[ 5, 12],
[21, 32]])
array()
和 matrix
的区别以上是针对array()数据类型来说的,另外,还有一个matrix的数据类型,在乘法方面表现并不一样。具体总结如下:
array()函数的相乘中:*代表点乘(对应元素相乘),dot()代表矩阵乘积。
mat()函数的乘法中:*代表矩阵乘,multiply()代表点乘。
这两个数据类型还有其他的一些区别,有时间再总结一波。这里强烈呼吁把这两个数据类型统一一下。我平常习惯使用array()数据类型,其实日常使用中也没有必要具体的区分,只需要在使用乘法的时候稍微麻烦一点:点乘用 np.multiply(A,B)
矩阵相乘用:np.dot(A,B)
。别用*
就好。
array.sum()
计算矩阵所有元素相加之和。A.sum(axis=0)
:计算矩阵每一列元素相加之和。A.Sum(axis=1)
:计算矩阵的每一行元素相加之和。使用np.c_[A,B]和np.r_[A,B]分别添加行和列
使用
np.insert(被添加数组, 插入位置, values=插入数组, axis=方向)
使用column_stack((A,B))
添加一列或者raw_stack((A,B))
添加一行
这个题还是挺考底层代码的实现功底的,对于面试计算机视觉岗位的同学,个人觉得这种题比较新奇,比手撕那些青蛙跳台阶啦等算法更能考察出对卷积的理解和代码实现能力,一石二鸟。以后在面试中应该会经常出现,需要引起注意。