Appearance
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.md 的 5.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 中 |
|---|---|---|
N | batch size | B = 2 |
C | channel / 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)维度编号:
| 编号 | 原含义 |
|---|---|
0 | B |
1 | T |
2 | C |
执行:
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=3,stride=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)。