前言
这次我们来搓一个一维卷积,用到的方法是滑动窗口。
功能
基础功能,未实现扩张(dilation)卷积和分组(groups)卷积。
原理
对于每个(out_channel)卷积核,它的各个(in_channel)卷积核需要与输入x的各个(in_channel)通道对应做卷积:kernel与对应的输入x的切片(x[:,:,i * self.stride: i * self.stride + self.ker_size])相乘再相加。
相乘指的是:比如1×3的kernel和1×3的kernel对应元素相乘
相加指的是:完成上述相乘之后相加,得到一个标量,然后各个(in_channel)通道的标量再相加,得到一个标量。
unfold函数
unfold是处理二维卷积的一个函数,这里我们把一维数据(batch_size,in_channel,in_length)当作二维数据(batch_size,in_channel,1,in_length)处理,这样卷积核的大小就是(1,kernel_size),步长就是(1, stride)。unfold之后的形状变成(batch_size,in_channel * ker_size,out_length)。
广播机制
在做乘法时,卷积核的形状变成(out_channel,in_channel * ker_size),样本x的形状是(batch_size,in_channel * ker_size,out_length)。
根据广播机制,这两个矩阵有一个维度相同都是in_channel * ker_size,二者可以相乘。且卷积核新增2个轴变为(1,out_channel,in_channel * ker_size,1)、样本x新增1个轴变为(batch_size,1,in_channel * ker_size,out_length)。
在第三个维度对应元素相乘再相加,得到一个标量也就是降维,所以可以得到形状为(batch_size,out_channel,out_length)。
代码
class Conv1d(nn.Module):
def __init__(self, in_channels:int, out_channels:int, kernel_size:int=3,
stride:int=1, padding:int=0, bias:bool=True) -> None:
super(Conv1d, self).__init__()
self.stride = stride
self.pad = padding
self.ker_size = kernel_size
self.out_ch = out_channels
self.kernel = nn.Parameter(nn.init.xavier_normal_(pt.randn(out_channels, in_channels, kernel_size)))
self.bias = None
if bias:
self.bias = nn.Parameter(pt.zeros(out_channels))
def forward(self, x):
# x:(batch_size, in_channel, in_len)
# out_len = (x.shape[2] + 2*self.pad - self.ker_size)//self.stride + 1
x = nn.functional.pad(x, (self.pad, self.pad), "constant", 0)
# (batch_size, in_ch, 1, in_len) -> (batch_size, in_ch*ker_size, out_len)
x_unfold = nn.functional.unfold(x[:,:,None,:],
kernel_size=(1, self.ker_size), stride=(1, self.stride))
out = self.kernel.view(self.out_ch, -1).matmul(x_unfold)
if self.bias is not None:
out += self.bias[None,:,None]
return out
结语
有问题欢迎在评论区讨论!