正文
既然卷积由相邻元素的顺序定义,那么靠近数组结尾的输出元素自然存在边界条件。为了避免这个问题,一钟很常见的做法是在输入序列x[n]的两端添加足够的元素(称为鬼元素)。如果你添加0,这个操作被称为零填充。其他方法也可以。在实现卷积时,你需要解决填充问题。
Swift的卷积
让我们来看看如何用Swift实现卷积。假设我们有以下的输入数组x和核数组w:
let x: [Float] = [1, 2, 3, 4, 5], M = x.count
let w: [Float] = [1, 2, 3], N = w.count
let T = N+M-1 // 这个之后需要
在我们开始之前,如上所述让我们添加N-1个0到序列x,和M-1个0到核来容纳计算。你可以使用以下函数:
func pad(sequence x: [Float], other sequence: [Float]) -> [Float] {
return x + [Float](repeatElement(0, count: sequence.count-1))
}
所以,填充过的新序列是:
let paddedX: [Float] = pad(sequence: x, other: kernel)
let paddedK: [Float] = pad(sequence: kernel, other: x)
现在,我们可以建立paddedX和paddedK之间的一个卷积:
最后,卷积的结果是:
// y = [1, 4, 10, 16, 22]
Accelerate的卷积
如果你想加速卷积处理,你可以使用Accelerate框架提供的vDSP_conv函数。同样,我需要处理边界条件和核反转。这一次,我对输入数组和核换个零填充的方式。另外,我需要反转核(文档里有解释),否则我得到的是两个序列的相关性。
以下是用Accelerate的实现:
import Accelerate
let x: [Float] = [1, 2, 3, 4, 5], M = x.count
let kernel: [Float] = [1, 2, 3], N = kernel.count
let T = N+M-1
var res = [Float](repeatElement(0, count: T))
let zeros = [Float](repeatElement(0, count: N-1))
let newXin = zeros + x + zeros
vDSP_conv(newXin, 1, kernel.reverse(), 1, &res, 1, vDSP_Length(T), vDSP_Length(N))
对于这个很短的输入序列,你不会感激Accelerate框架带来的加速。但如果我创建了100,000个元素的输入数组,并用和之前示例相同的w内核进行卷积。在我的MacBook Pro上,Swift的实现需要318 ms,而Accelerate的vDSP_conv方法只要159 ns。
Metal的卷积
让我们看一下如何用Metal实现相同的例子。看
这篇文章
学习如何配置一个GPU计算的Metal项目。
在这个特殊的例子中,我们需要创建3个Metal纹理(遵守MTLTexture协议的对象):第一个纹理存储输入序列,第二个纹理存储核,第三个纹理存储最终结果。
以下是创建这些纹理的源代码:
import Metal
let paddedX: [Float] = input + [Float](repeatElement(0, count: N-1))
let paddedK: [Float] = kernel + [Float](repeatElement(0, count: M-1))
let inputTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(with: .r32Float, width: paddedX.count, height: 1, mipmapped: false)
inputTextureDescriptor.usage = .shaderRead
inTexture = metalContext.device.newTexture(with: inputTextureDescriptor)
let region = MTLRegionMake2D(0, 0, paddedX.count, 1)
inTexture?.replace(region, mipmapLevel: 0, withBytes: paddedX, bytesPerRow: paddedX.count * sizeof(Float32.self))
let kernelTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(with: .r32Float, width: paddedK.count, height: 1, mipmapped: false)
kernelTexture = metalContext.device.newTexture(with: kernelTextureDescriptor)
let kernelRegion = MTLRegionMake2D(0, 0, paddedK.count, 1)
kernelTexture?.replace(kernelRegion, mipmapLevel: 0, withBytes: paddedK, bytesPerRow: paddedK.count * sizeof(Float32.self))