真实面经题目 · 原创解析
如何手写实现一维卷积算子?给定输入序列 [1,2,3,4] 和卷积核 [1,2,3] 时,如何约定 kernel 翻转、valid/full 输出、padding 和 stride?
这题考的是能否先把卷积约定说清楚再写代码:深度学习里的 Conv1D 通常实际做 cross-correlation 不翻转 kernel;数学卷积会翻转 kernel;valid/full、padding 和 stride 会直接改变输出长度和数值。
真实面经题目 · 原创解析
这题考的是能否先把卷积约定说清楚再写代码:深度学习里的 Conv1D 通常实际做 cross-correlation 不翻转 kernel;数学卷积会翻转 kernel;valid/full、padding 和 stride 会直接改变输出长度和数值。
手写一维卷积前我会先明确约定,否则同一个输入会有多个合理输出。深度学习框架里的 Conv1D 通常实现的是互相关,也就是不翻转 kernel;数学意义上的卷积会先把 kernel 翻转。其次要明确 padding:valid 表示不补零,只在 kernel 完全落在输入范围内计算;full 可以理解为两侧各补 kernel_size-1 个零,让 kernel 与输入有任意重叠都输出;same 通常补到输出长度接近输入长度,但偶数 kernel 和 stride 大于 1 时还要约定左右补零。最后是 stride:每次窗口起点移动 stride 个位置,所以输出长度一般是 floor((N + 2P - K) / S) + 1,前提是 N + 2P >= K。按深度学习互相关、valid、stride=1、padding=0 的约定,输入 [1,2,3,4] 和 kernel [1,2,3] 的输出是 [1*1+2*2+3*3, 2*1+3*2+4*3] = [14,20]。如果按数学卷积先翻转 kernel,valid 输出是 [10,16];如果按 full 数学卷积,输出是 [1,4,10,16,17,12]。写代码时我会把 flip_kernel、padding 和 stride 做成显式参数,并用小样例测试 valid/full、stride、kernel 大于输入和空输出等边界。
一维卷积最容易错在没有先说明语义。深度学习里的 Conv1D 多数使用 cross-correlation,即窗口和 kernel 同向点乘,不翻转 kernel;数学卷积则使用翻转后的 kernel。面试中只要先声明约定,并能给出另一种约定下的结果,通常比直接报一个数字更严谨。
输入长度为 N,kernel 长度为 K,两侧各补 P 个零,stride 为 S 时,valid 风格的输出长度是 floor((N + 2P - K) / S) + 1,若 N + 2P < K 则没有完整窗口。valid 对应 P=0;full 对应 P=K-1;same 需要按框架约定决定左右补零,尤其在偶数 kernel 或 stride 大于 1 时不能含糊。
对输入 [1,2,3,4] 和 kernel [1,2,3],若按深度学习互相关、valid、stride=1,两个窗口分别是 [1,2,3] 和 [2,3,4],输出为 [14,20]。若按数学卷积,kernel 翻转为 [3,2,1],valid 输出为 [10,16]。若按 full 数学卷积,相当于两侧补 2 个零,输出为 [1,4,10,16,17,12]。
最小实现可以先把输入两侧补零,然后根据输出长度枚举窗口起点,每次取长度为 K 的窗口和 kernel 点乘。stride 只影响窗口起点的步长;flip_kernel 决定点乘前是否反转 kernel。这个版本时间复杂度是 O(output_len * K),空间上除输出和补零数组外没有复杂状态,适合作为正确性基准。
正确性测试应覆盖 valid、full、自定义 padding、stride 大于 1、kernel 长于输入、空输入、负数、浮点数和 kernel 翻转。常见失败包括输出长度 off-by-one、full padding 只补一侧、把 convolution 和 correlation 混用、same padding 左右不一致、stride 后最后一个不完整窗口被错误使用。
手写基准实现强调清晰正确;部署到高性能推理库时才考虑 SIMD、循环展开、cache 友好布局、多通道输入、batch、dilation、group、im2col 或直接卷积 kernel。性能指标可以看单次耗时、吞吐、内存访问和数值一致性。对于小 kernel 和短音频帧,简单直接卷积可能比复杂变换更合适。
def conv1d(x, kernel, padding='valid', stride=1, flip_kernel=False):
if stride <= 0:
raise ValueError('stride must be positive')
if not x or not kernel:
return []
if padding == 'valid':
pad = 0
elif padding == 'full':
pad = len(kernel) - 1
elif isinstance(padding, int) and padding >= 0:
pad = padding
else:
raise ValueError('padding must be valid, full, or a non-negative int')
weights = list(reversed(kernel)) if flip_kernel else list(kernel)
padded = [0] * pad + list(x) + [0] * pad
window = len(weights)
out_len = (len(padded) - window) // stride + 1
if out_len <= 0:
return []
out = []
for t in range(out_len):
start = t * stride
total = 0
for j in range(window):
total += padded[start + j] * weights[j]
out.append(total)
return out
x = [1, 2, 3, 4]
k = [1, 2, 3]
print(conv1d(x, k, padding='valid', stride=1, flip_kernel=False)) # [14, 20]
print(conv1d(x, k, padding='valid', stride=1, flip_kernel=True)) # [10, 16]
print(conv1d(x, k, padding='full', stride=1, flip_kernel=True)) # [1, 4, 10, 16, 17, 12] 训练时 kernel 是可学习参数,使用 cross-correlation 或数学卷积只差一个参数翻转的约定,不影响模型表达能力。框架通常采用不翻转实现,代码和内存访问更直接。
valid 不补零,只输出完整窗口;full 两侧补 kernel_size-1 个零,让任意重叠都输出;same 希望输出长度接近输入长度,但左右补零和 stride 处理需要按具体约定说明。
在两侧 padding 都是 P 的情况下,输出长度是 floor((N + 2P - K) / S) + 1;最后不足 kernel 长度的窗口通常丢弃,除非额外定义 ceil 模式。
valid 且不补零时没有完整窗口,输出为空;full 或显式 padding 时仍可计算,因为补零后 kernel 可以与输入部分重叠。实现里要明确返回空还是报错。
多通道时每个输出通道有一组输入通道 kernel,对每个时间窗口先在输入通道和 kernel 维度上求和,再加 bias。单通道滑窗点乘是多通道实现的最小基础。