卷积加速之 im2col + GEMM
im2col + GEMM 优缺点以及适用的场景
优点:
易于理解和实现。
将卷积转换为矩阵乘法后,可以利用高度优化的 BLAS 库(如 cuBLAS)进行计算。
缺点:
增加了内存的使用量,因为需要构造额外的矩阵。
适用性有限,对于较大的核大小或较深的网络,内存占用可能成为瓶颈。
cuDNN 实现: cuDNN 提供了基于 im2col 和 GEMM 的卷积实现,特别是在处理较小的核和较浅的网络时效果很好。
下边给到一个 python 的 demo 程序,帮助理解下 im2col 的过程
import numpy as np
def im2col(image, kernel_shape):
"""
Transform the input image into a matrix suitable for matrix multiplication.
"""
# Extracting the dimensions
img_rows, img_cols = image.shape
kernel_rows, kernel_cols = kernel_shape
# Output size
# 当卷积核从图像的最上方开始滑动时,它可以在垂直方向上移动的位置数为 img_rows - kernel_rows
# 加上初始位置,总共能够产生 img_rows - kernel_rows + 1 个不同的垂直位置,output_cols 同理
output_rows = img_rows - kernel_rows + 1
output_cols = img_cols - kernel_cols + 1
# Create the im2col matrix
# 行数是 kernel_rows * kernel_cols 的原因:
# 1.这代表了卷积核中的元素数量
# 2.滑动卷积核的过程中,每一个卷积核元素和输入图像的一个相应元素相乘,我们会将卷积核展开成一个 1 x kernel_rows * kernel_cols 的一维数组,所以对应的 im2col 矩阵的行数就是 kernel_rows * kernel_cols
# 列数是 output_rows * output_cols 的原因:
# 1. 这代表了卷积操作产生的输出元素的数量,即卷积输出的尺寸
# 2. 每一列代表了对应于输入图像中的一个唯一的卷积窗口
im2col_matrix = np.zeros((kernel_rows * kernel_cols, output_rows * output_cols))
col = 0
for i in range(output_rows):
for j in range(output_cols):
# 选取当前的卷积窗口,i 和 j 是当前迭代行和列的索引,i + kernel_rows 指定了范围,j + kernal_cols 同理
# ravel 函数用于展平成一维数组
window = image[i:i + kernel_rows, j:j + kernel_cols].ravel()
im2col_matrix[:, col] = window
# 追踪和更新正在填充的列
col += 1
return im2col_matrix
def conv_to_matrix_mult(image, kernel):
"""
Perform convolution by converting it to a matrix multiplication.
"""
kernel_matrix = kernel.ravel()
im2col_matrix = im2col(image, kernel.shape)
# Perform matrix multiplication and reshape the result
conv_result = kernel_matrix @ im2col_matrix
output_shape = (image.shape[0] - kernel.shape[0] + 1, image.shape[1] - kernel.shape[1] + 1)
conv_result = conv_result.reshape(output_shape)
return conv_result
# Example input image and kernel
input_image = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
])
kernel = np.array([
[1, 0, -1],
[1, 0, -1],
[1, 0, -1]
])
# Perform convolution using matrix multiplication
convolution_result = conv_to_matrix_mult(input_image, kernel)
convolution_result
到底优化了多少,有没有一个量化的公式来反映快了多少呢?
速度提升程度取决于多种因素,包括硬件特性、卷积参数(如卷积核大小、步长、填充)、以及输入数据的大小
硬件加速:使用
im2col
加上高效的矩阵乘法(如 GPU 上的 cuBLAS 或 CPU 上的优化 BLAS 实现)可以显著加速卷积运算,特别是在并行处理大型数据集时。内存使用和带宽:
im2col
方法会增加内存使用量,因为它创建了一个较大的矩阵来存储重排的输入数据。在内存带宽有限的情况下,这可能成为瓶颈。卷积核大小:较小的卷积核(例如 3x3)通常能从
im2col
方法中获得更大的速度提升,因为这种情况下矩阵乘法更高效。输入和输出尺寸:输入图像的大小和卷积核的数量也会影响性能。较大的输入图像或更多的卷积核意味着更多的计算,但同时也意味着更高的并行度,这在使用 GPU 时特别有利。
算法优化:不同的库和框架对
im2col
和矩阵乘法的实现方式可能不同,这会影响性能。一些实现可能包括额外的优化,比如更有效地利用缓存或减少内存占用。
优化的点在什么地方呢 ?
改变了卷积运算的方式,使其能够更有效地利用现代计算硬件的优势,尤其是在并行处理方面。
并行处理:传统的卷积操作是通过在输入图像上逐个位置滑动卷积核来实现的,这通常是一个顺序处理过程。而将卷积转换为矩阵乘法后,可以一次性处理多个卷积窗口。现代计算硬件(特别是 GPU)非常擅长同时执行大量的矩阵乘法运算,因此这种方法可以充分利用硬件的并行处理能力。
高效的矩阵乘法库:现代计算环境中,矩阵乘法是高度优化的操作,特别是在使用诸如 cuBLAS(在 NVIDIA GPU 上)或 Intel MKL(在 Intel CPU 上)这样的库时。通过将卷积转换为矩阵乘法,可以利用这些库的优化来加速运算。
增加的内存使用:虽然
im2col
方法可以提高计算效率,但它会增加内存的使用量。这是因为它需要创建一个额外的矩阵来存储重排后的输入数据。在某些情况下,这可能导致内存带宽成为性能瓶颈。适用性问题:对于小型卷积核和大型输入数据,
im2col
方法通常更有效。但对于大型卷积核或小型输入,这种方法可能不会带来太大的性能提升,甚至可能因为额外的内存需求而减慢速度。