Skip to content

AvgPool1d 与 permute:DLinear moving_avg 的滑动平均

Abstract

这篇接在 moving_avg.forward 的边界 padding 注解之后。

它只讲清楚一件事:为什么 AvgPool1d 前要先 permute(0, 2, 1),以及滑动平均怎样把 (2, 8, 3) 变回 (2, 6, 3)

0. 文件索引

项目内容
源文件ts_benchmark/baselines/time_series_library/layers/Autoformer_EncDec.py
源类moving_avg
源方法forward(self, x)
父文档zdocs/modelread/DLinear/03-Layer2-series_decomp.md5.2 AvgPool1d
前置文档model-order/01-DLinear/01-Autoformer_EncDec-moving_avg-forward-基础语法注解.md
本文输入padding 后的 x: (B, T+2*pad, C) = (2, 8, 3)
本文输出moving average 后的 x: (B, T, C) = (2, 6, 3)

1. 源码对象

完整类:

python
class moving_avg(nn.Module):
    """
    Moving average block to highlight the trend of time series
    """

    def __init__(self, kernel_size, stride):
        super(moving_avg, self).__init__()
        self.kernel_size = kernel_size
        self.avg = nn.AvgPool1d(kernel_size=kernel_size, stride=stride, padding=0)

    def forward(self, x):
        # padding on the both ends of time series
        front = x[:, 0:1, :].repeat(1, (self.kernel_size - 1) // 2, 1)
        end = x[:, -1:, :].repeat(1, (self.kernel_size - 1) // 2, 1)
        x = torch.cat([front, x, end], dim=1)
        x = self.avg(x.permute(0, 2, 1))
        x = x.permute(0, 2, 1)
        return x

本文只看后半段:

python
x = self.avg(x.permute(0, 2, 1))
x = x.permute(0, 2, 1)

2. Level 1:AvgPool1d 要求什么输入

nn.AvgPool1d 是一维平均池化。

它要求输入格式是:

text
(N, C, L)

在时间序列里可以理解成:

维度含义在本文 toy 中
Nbatch sizeB = 2
Cchannel / feature 数C = 3
L一维序列长度T_padded = 8

但是 DLinear / Autoformer 里大多数时间序列张量是:

text
(B, T, C)

所以 padding 之后:

text
x.shape = (2, 8, 3)

这还不能直接喂给 AvgPool1d

必须先换成:

text
(B, C, T_padded) = (2, 3, 8)

这就是:

python
x.permute(0, 2, 1)

3. Level 2:permute(0, 2, 1) 做了什么

原张量:

text
x.shape = (B, T, C) = (2, 8, 3)

维度编号:

编号原含义
0B
1T
2C

执行:

python
x.permute(0, 2, 1)

意思是新维度顺序变成:

text
原 dim 0 -> 新 dim 0
原 dim 2 -> 新 dim 1
原 dim 1 -> 新 dim 2

所以:

text
(B, T, C) -> (B, C, T)
(2, 8, 3) -> (2, 3, 8)

这里没有改变数据值,只是改变了“按哪个维度解释数据”。

4. Level 3:AvgPool1d 的参数在这里是什么意思

源码:

python
self.avg = nn.AvgPool1d(kernel_size=kernel_size, stride=stride, padding=0)

toy 参数:

text
kernel_size = 3
stride = 1
padding = 0

解释:

参数含义toy 值在本文里控制什么
kernel_size窗口大小3每次取连续 3 个时间步求平均
stride窗口移动步长1每次向右移动 1 步
padding池化层内部补边0不再补边,因为前面已经手动复制 padding

注意:

python
padding=0

不是说整个算法不 padding,而是说:

AvgPool1d 自己不再做 padding,因为前面已经用 repeat + cat 手动补好了边界。

5. Level 4:输出长度怎么算

AvgPool1d 的输出长度公式:

text
L_out = floor((L_in + 2 * padding - kernel_size) / stride) + 1

代入 toy:

text
L_in = 8
padding = 0
kernel_size = 3
stride = 1

L_out = floor((8 + 2*0 - 3) / 1) + 1
      = floor(5) + 1
      = 6

所以:

text
AvgPool1d 输入:  (B, C, L_in)  = (2, 3, 8)
AvgPool1d 输出:  (B, C, L_out) = (2, 3, 6)

最后再执行:

python
x = x.permute(0, 2, 1)

把它换回:

text
(B, C, T) -> (B, T, C)
(2, 3, 6) -> (2, 6, 3)

6. Level 5:完整 shape 流

从原始输入开始:

text
原始 x:
  (B, T, C) = (2, 6, 3)

边界复制 padding:
  front: (2, 1, 3)
  end:   (2, 1, 3)
  cat 后: (2, 8, 3)

换成 AvgPool1d 格式:
  x.permute(0, 2, 1)
  (2, 8, 3) -> (2, 3, 8)

滑动平均:
  AvgPool1d(kernel=3, stride=1, padding=0)
  (2, 3, 8) -> (2, 3, 6)

换回模型主线格式:
  x.permute(0, 2, 1)
  (2, 3, 6) -> (2, 6, 3)

7. Level 6:可算 toy 小例子

只看第 0 个 batch、第 0 个变量:

text
原始序列:
[1, 2, 3, 4, 5, 6]

边界复制 padding 后:

text
[1, 1, 2, 3, 4, 5, 6, 6]

kernel_size=3stride=1,窗口逐步滑动:

text
窗口 0: [1, 1, 2] -> (1 + 1 + 2) / 3 = 1.333
窗口 1: [1, 2, 3] -> (1 + 2 + 3) / 3 = 2.000
窗口 2: [2, 3, 4] -> (2 + 3 + 4) / 3 = 3.000
窗口 3: [3, 4, 5] -> (3 + 4 + 5) / 3 = 4.000
窗口 4: [4, 5, 6] -> (4 + 5 + 6) / 3 = 5.000
窗口 5: [5, 6, 6] -> (5 + 6 + 6) / 3 = 5.667

输出:

text
moving_mean = [1.333, 2.000, 3.000, 4.000, 5.000, 5.667]

这就是 DLinear 里 trend 分量的来源。

8. 常见错误

8.1 忘记 permute

错误理解:

python
self.avg(x)  # x 是 (B, T, C)

问题是 AvgPool1d 会把中间维当成 channel,把最后一维当成长度。

也就是说它会把:

text
(2, 8, 3)

错误解释成:

text
B=2, C=8, L=3

这完全不是我们想要的时间方向滑动平均。

8.2 把 padding=0 理解成没有 padding

本代码里有 padding,只是 padding 不在 AvgPool1d 内部做。

真正的 padding 是这三行完成的:

python
front = x[:, 0:1, :].repeat(1, (self.kernel_size - 1) // 2, 1)
end = x[:, -1:, :].repeat(1, (self.kernel_size - 1) // 2, 1)
x = torch.cat([front, x, end], dim=1)

AvgPool1d(padding=0) 只是表示:

进入池化层时,边界已经处理完了。

9. 一句话总结

这两行:

python
x = self.avg(x.permute(0, 2, 1))
x = x.permute(0, 2, 1)

可以翻译成:

先把时间序列从 (B,T,C) 换成 AvgPool1d 需要的 (B,C,T),沿时间轴做滑动平均,再换回模型主线使用的 (B,T,C)

*记录并在线阅读我的笔记*