算法理论
- 算法数据类型:长度为 N N N的时间序列 X = { x 1 , x 2 , . . . , x N } \mathcal{X}=\{x_1, x_2, ..., x_N\} X={x1,x2,...,xN},其中 x i ∈ R d x_i\in R^{d} xi∈Rd
- 算法目标:检测时序中的异常样本数据
- 算法前置学习:Transformer
- 论文地址:论文地址
总体架构
- 从图上看到,Anomaly Transformer(以下简称AT)结构上类似Transformer的Encoder,用Anomaly Attention模块替换了Transformer的Encoder的self-Attention模块,其它没有什么实质变化,模型可公式化为:
Z l = Layer-Norm ( Anomaly-Attention ( X l − 1 ) + X l − 1 ) X l = Layer-Norm ( Feed-Forward ( Z l ) + Z l ) \mathcal{Z}^l=\text{Layer-Norm}(\text{Anomaly-Attention}(\mathcal{X}^{l-1}) + \mathcal{X}^{l-1})\\ \mathcal{X}^l=\text{Layer-Norm}(\text{Feed-Forward}(\mathcal{Z}^l) + \mathcal{Z}^l) Zl=Layer-Norm(Anomaly-Attention(Xl−1)+Xl−1)Xl=Layer-Norm(Feed-Forward(Zl)+Zl)- Layer-Norm( ⋅ \cdot ⋅)可参看Transformer
- X l ∈ R N × d m o d e l \mathcal{X}^l\in R^{N\times d_{model}} Xl∈RN×dmodel为第 l l l层的输出,通道数为 d m o d e l d_{model} dmodel,此处的通道数据为数据的特征数量,与原始数据维数 d d d含义相同。
- Z l ∈ R N × d m o d e l \mathcal{Z}^l \in R^{N\times d_{model}} Zl∈RN×dmodel为第 l l l层的隐藏表示
下面介绍论文的核心–Anomaly Attention
Anomaly Attention
对于先验关联,作者采用可学习的高斯核来计算相对时间距离的先验。利用高斯核的特性,对每个时间点可以在更多地关注相邻时间点。对于高斯核还用了一个可学习的尺度参数
σ
\sigma
σ,使先验关联适应不同的时间序列模式,主要用于匹配不同长度的异常时间段。
序列关联从原始序列中学习,能够自适应地找到最有效的associations(关联)。这两种关联有种局部信息与全局信息的味道。
- 类似Transformer,对第
l
l
l层定义:
Q
,
K
,
V
,
σ
\mathcal{Q}, \mathcal{K}, \mathcal{V}, \sigma
Q,K,V,σ =
X
l
−
1
W
Q
l
,
X
l
−
1
W
K
l
,
X
l
−
1
W
V
l
,
X
l
−
1
W
σ
l
\mathcal{X}^{l-1}W_{\mathcal{Q}}^l, \mathcal{X}^{l-1}W_{\mathcal{K}}^l, \mathcal{X}^{l-1}W_{\mathcal{V}}^l, \mathcal{X}^{l-1}W_{\mathcal{\sigma}}^l
Xl−1WQl,Xl−1WKl,Xl−1WVl,Xl−1Wσl
- Q , K , V ∈ R N × d m o d e l \mathcal{Q}, \mathcal{K}, \mathcal{V}\in R^{N\times d_{model}} Q,K,V∈RN×dmodel
- σ ∈ R N × 1 \sigma \in R^{N\times 1} σ∈RN×1与时间序列长度相匹配
- W Q l , W K l , W V l ∈ R d m o d e l × d m o d e l \mathcal{W_{\mathcal{Q}}^l}, \mathcal{W_{\mathcal{K}}^l}, \mathcal{W_{\mathcal{V}}^l}\in R^{d_{model}\times d_{model}} WQl,WKl,WVl∈Rdmodel×dmodel
- 则第
l
l
l层的Anomaly-Attention可以公式化为:
- Prior-Association:
P l = R e s c a l e ( [ 1 2 π σ i e x p ( − ∣ j − i ∣ 2 2 σ i 2 ) ] i , j ∈ { 1 , 2 , . . . , N } ) ] ) = \mathcal{P}^l=Rescale([\frac{1}{\sqrt{2\pi}\sigma_i}exp(-\frac{|j - i|^2}{2\sigma_i^2})]_{i, j\in\{1, 2, ...,N\}})])= Pl=Rescale([2πσi1exp(−2σi2∣j−i∣2)]i,j∈{1,2,...,N})])=
[ p 11 p 12 ⋯ p 1 N p 21 p 22 ⋯ p 2 N ⋮ ⋮ ⋱ ⋮ p N 1 p N 2 ⋯ p N N ] N × N \begin{bmatrix} p_{11} & p_{12} & \cdots & p_{1N} \\ p_{21} & p_{22} & \cdots & p_{2N}\\ \vdots & \vdots& \ddots & \vdots\\ p_{N1} & p_{N2} & \cdots & p_{NN} \end{bmatrix}_{N\times N} p11p21⋮pN1p12p22⋮pN2⋯⋯⋱⋯p1Np2N⋮pNN N×N
其中 p i j = g i j ∑ j = 1 N g i j p_{ij}=\frac{g_{ij}}{\sum_{j=1}^Ng_{ij}} pij=∑j=1Ngijgij, g i j = 1 2 π σ i e x p ( − ∣ j − i ∣ 2 2 σ i 2 ) ] i , j ∈ { 1 , 2 , . . . , N } ) g_{ij}=\frac{1}{\sqrt{2\pi}\sigma_i}exp(-\frac{|j - i|^2}{2\sigma_i^2})]_{i, j\in\{1, 2, ...,N\}}) gij=2πσi1exp(−2σi2∣j−i∣2)]i,j∈{1,2,...,N}) - Series-Association: S l = S o f t m a x ( Q K T d m o d e l ) ∈ R N × N \mathcal{S}^l=Softmax(\frac{\mathcal{Q}\mathcal{K}^T}{\sqrt{d_{model}}})\in R^{N\times N} Sl=Softmax(dmodelQKT)∈RN×N
- Reconstruction: Z l ^ = S l V \hat{\mathcal{Z}^l}=\mathcal{S}^l\mathcal{V} Zl^=SlV
- 可以看出 S l 与 P l \mathcal{S}^l与\mathcal{P}^l Sl与Pl均是 N × N N\times N N×N维矩阵且均是行归一化的,即关于行都是离散分布,但 P l \mathcal{P}^l Pl以Gauss核计算点之间关联,而 S l \mathcal{S}^l Sl用注意力机制计算每个time point与整体的关系
- Prior-Association:
Association Discrepancy
作者采用对称KL散度形式化先验关联和序列关联之间的关联差异,表示这两个分布之间的信息增益。对所有的
L
L
L层的关联差异进行平均,将多层特征的关联组合成一个更有信息量的度量:
A
s
s
D
i
s
(
P
,
S
,
X
)
=
[
1
L
∑
l
=
1
L
(
K
L
(
P
i
,
:
l
∣
∣
S
i
,
:
l
)
+
K
L
(
S
i
,
:
l
∣
∣
P
i
,
:
l
)
]
i
=
1
,
.
.
.
,
N
=
[
1
L
∑
l
=
1
L
(
K
L
(
P
1
,
:
l
∣
∣
S
1
,
:
l
)
+
K
L
(
S
1
,
:
l
∣
∣
P
1
,
:
l
)
1
L
∑
l
=
1
L
(
K
L
(
P
2
,
:
l
∣
∣
S
2
,
:
l
)
+
K
L
(
S
2
,
:
l
∣
∣
P
2
,
:
l
)
⋮
1
L
∑
l
=
1
L
(
K
L
(
P
N
,
:
l
∣
∣
S
N
,
:
l
)
+
K
L
(
S
N
,
:
l
∣
∣
P
N
,
:
l
)
]
∈
R
N
×
1
AssDis(\mathcal{P}, \mathcal{S}, \mathcal{X})=[\frac{1}{L}\sum_{l=1}^L(KL(\mathcal{P}_{i, :}^l||\mathcal{S}_{i, :}^l) + KL(\mathcal{S}_{i, :}^l||\mathcal{P}_{i, :}^l)]_{i=1,...,N}=\\ \begin{bmatrix} \frac{1}{L}\sum_{l=1}^L(KL(\mathcal{P}_{1, :}^l||\mathcal{S}_{1, :}^l) + KL(\mathcal{S}_{1, :}^l||\mathcal{P}_{1, :}^l)\\ \frac{1}{L}\sum_{l=1}^L(KL(\mathcal{P}_{2, :}^l||\mathcal{S}_{2, :}^l) + KL(\mathcal{S}_{2, :}^l||\mathcal{P}_{2, :}^l)\\ \vdots\\ \frac{1}{L}\sum_{l=1}^L(KL(\mathcal{P}_{N, :}^l||\mathcal{S}_{N, :}^l) + KL(\mathcal{S}_{N, :}^l||\mathcal{P}_{N, :}^l) \end{bmatrix}\in R^{N\times 1}
AssDis(P,S,X)=[L1l=1∑L(KL(Pi,:l∣∣Si,:l)+KL(Si,:l∣∣Pi,:l)]i=1,...,N=
L1∑l=1L(KL(P1,:l∣∣S1,:l)+KL(S1,:l∣∣P1,:l)L1∑l=1L(KL(P2,:l∣∣S2,:l)+KL(S2,:l∣∣P2,:l)⋮L1∑l=1L(KL(PN,:l∣∣SN,:l)+KL(SN,:l∣∣PN,:l)
∈RN×1
- AssDis的计算即将每层的 P \mathcal{P} P与 S \mathcal{S} S按行平均,形成一个 N N N维向量。
- 对于某个异常点 x i x_i xi, P \mathcal{P} P的第 i i i行表示与其它的时间点的Gauss距离, x j x_j xj与 x i x_i xi越近,对应的值越大,而由于此点异常,注意力机制对于 x i x_i xi将只会关注 x i x_i xi附近的点,这导致 P \mathcal{P} P与 S \mathcal{S} S更加相似,则KL散度值更小,所以相对于正常的点AssDis更小,使得正常点与异常点有了区分性。
最小最大策略
作为一个无监督任务,作者使用重构损失优化模型。重构损失将引导序列关联找到最具信息量的关联。为了进一步放大正常和异常点的差异,还使用额外的损失来放大关联差异。由于先验关联的单峰性,差异损失会引导序列关联更多地关注非相邻区域(解释:对于异常点,Gauss距离会关注附近的点,注意力也会如此,导致AssDis很小, 如果放大AssDis,则会使其关注较远的点),这使得异常重建更加困难(解释:无法重构所以就很容易的判断为异常),异常的可识别性更强。损失函数为:
L
(
X
^
,
P
,
S
,
λ
;
X
)
=
∣
∣
X
−
X
^
∣
∣
F
2
−
λ
×
∣
∣
AssDis
(
P
,
S
;
X
)
∣
∣
1
\mathcal{L}(\hat{\mathcal{X}}, \mathcal{P}, \mathcal{S}, \lambda; \mathcal{X}) = ||\mathcal{X}-\hat{\mathcal{X}}||^2_F - \lambda \times ||\text{AssDis}(\mathcal{P}, \mathcal{S}; \mathcal{X})||_1
L(X^,P,S,λ;X)=∣∣X−X^∣∣F2−λ×∣∣AssDis(P,S;X)∣∣1
- 其中 X ^ ∈ R N × d \hat{\mathcal{X}}\in R^{N\times d} X^∈RN×d
- λ \lambda λ是为了平衡损失
注意,直接最大化关联差异将极大地降低高斯核的尺度参数,使先验关联变得毫无意义。
为了更好地控制关联学习,作者提出了一个极大极小策略:具体来说,对于最小化阶段,用先验关联
P
l
\mathcal{P}^l
Pl近似从原始序列中学习到的序列关联
S
l
\mathcal{S}^l
Sl,使
P
\mathcal{P}
P适应不同的时间模式。在最大化阶段,我们对序列关联进行优化,扩大关联差异。这个过程迫使序列关联更多地关注非相邻的区域。即存在两个阶段的损失函数:
Minimize Phase:
L
T
o
t
a
l
(
X
^
,
P
,
S
d
e
t
a
c
h
,
−
λ
;
X
)
Maxmize Phase:
L
T
o
t
a
l
(
X
^
,
P
d
e
t
a
c
h
,
S
,
λ
;
X
)
\text{Minimize Phase:} \mathcal{L}_{Total}(\hat{\mathcal{X}}, \mathcal{P}, \mathcal{S}_{detach}, -\lambda; \mathcal{X})\\ \text{Maxmize Phase:} \mathcal{L}_{Total}(\hat{\mathcal{X}}, \mathcal{P_{detach}}, \mathcal{S}, \lambda; \mathcal{X})
Minimize Phase:LTotal(X^,P,Sdetach,−λ;X)Maxmize Phase:LTotal(X^,Pdetach,S,λ;X)
由于
P
\mathcal{P}
P在最小阶段近似于
S
d
e
t
a
c
h
\mathcal{S}_{detach}
Sdetach,因此最大阶段将对序列关联进行更强的约束,迫使时间点更加关注非相邻区域。在重构损失下,异常比正常时间点更难达到这一点,从而放大了关联差异的正常-异常可分辨性。
- 从代码上可以看到,最大最小只是对 ∣ ∣ A s s D i s ( P , S ; X ) ∣ ∣ 1 ||AssDis(\mathcal{P}, \mathcal{S}; \mathcal{X})||_1 ∣∣AssDis(P,S;X)∣∣1进行的,并不是对损失函数,此处容易有误解
- 即两个阶段都是最小化 L ( X ^ , P , S , λ ; X ) \mathcal{L}(\hat{\mathcal{X}},\mathcal{P}, \mathcal{S}, \lambda; \mathcal{X}) L(X^,P,S,λ;X),只是通过改变 λ \lambda λ的正负对 ∣ ∣ A s s D i s ( P , S ; X ) ∣ ∣ 1 ||AssDis(\mathcal{P}, \mathcal{S}; \mathcal{X})||_1 ∣∣AssDis(P,S;X)∣∣1最大或最小化
以上有很多个人理解,如有错误请指出,感激不尽。
代码,训练个人数据集
- 代码链接为https://github.com/thuml/Anomaly-Transformer
- 代码中没有提供数据集,但提供了数据集的下载路径~要梯子
若要加载个人数据集训练与预测,可根据以下步骤
资源管理结构
- 原代码中是没有dataset文件夹的,手动创建一个即可
- 将个人数据集放入dataset文件夹中,结构如下,没必要写两层(我是为了统一才写的,后面文件路径写对就好)
- 下图中的MSL是原论文的数据集,MY_TEST是个人数据集,train.csv与test.csv分别是训练与测试集,test_label.csv只能为了评估时算后面的指标,完全不参与模型训练,如果没有的话可以不用,数据格式任意,npy/csv/excel等都行,后面有处理的方式,只用仿照作者写一个dataloader的类就好
- 个人建议增加一个test_label.csv,不增加后面在测试的时候要删除一些代码,增加很简单,只用创建一个与test.csv高度一样的序列即可,例如test.csv的shape=(10000, d),则test_label.csv中只用创建一列np.zeros(10000)即可,后面测试的指标完全不用看,只用看预测的结果即可
要更改的部分代码
- 在main.py中,需要更改部分代码首先是参数,win_size是每次输入的序列的长度,根据自己的任务改,input_c是每个样本的维数,即dataframe的特征个数,output_c是输出的维数,因为要重构,这里保持与输入一样,dataset是数据集的名称,mode是要训练还是要测试,data_path为数据集的存放路径,写到train.csv的文件夹那一级
- batchsize一定要调低一点,默认的1024会爆显存
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--lr', type=float, default=1e-4)
parser.add_argument('--num_epochs', type=int, default=10)
parser.add_argument('--k', type=int, default=3)
parser.add_argument('--win_size', type=int, default=100)
parser.add_argument('--input_c', type=int, default=34)
parser.add_argument('--output_c', type=int, default=34)
parser.add_argument('--batch_size', type=int, default=32)
parser.add_argument('--pretrained_model', type=str, default=None)
parser.add_argument('--dataset', type=str, default='MY_TEST')
parser.add_argument('--mode', type=str, default='test', choices=['train', 'test'])
parser.add_argument('--data_path', type=str, default=r'个人存放test.csv/train.csv的路径')
parser.add_argument('--model_save_path', type=str, default='checkpoints')
parser.add_argument('--anormly_ratio', type=float, default=4.00)
config = parser.parse_args()
args = vars(config)
print('------------ Options -------------')
for k, v in sorted(args.items()):
print('%s: %s' % (str(k), str(v)))
print('-------------- End ----------------')
main(config)
- 数据加载,在data_loader.py中可以依照作者已有的类创建个人数据集的读取类,并在get_loader_segment添加加载代码
class MY_TESTSegLoader(object):
def __init__(self, data_path, win_size, step, mode="train"):
self.mode = mode
self.step = step
self.win_size = win_size
self.scaler = StandardScaler()
data = pd.read_csv(data_path + '/train.csv')
data = data.values[:, 1:]
data = np.nan_to_num(data)
self.scaler.fit(data)
data = self.scaler.transform(data)
test_data = pd.read_csv(data_path + '/test.csv')
test_data = test_data.values[:, 1:]
test_data = np.nan_to_num(test_data)
self.test = self.scaler.transform(test_data)
self.train = data
self.val = self.test
self.test_labels = pd.read_csv(data_path + '/test_label.csv').values[:, 1:]
print("test:", self.test.shape)
print("train:", self.train.shape)
def __len__(self):
"""
Number of images in the object dataset.
"""
if self.mode == "train":
return (self.train.shape[0] - self.win_size) // self.step + 1
elif (self.mode == 'val'):
return (self.val.shape[0] - self.win_size) // self.step + 1
elif (self.mode == 'test'):
return (self.test.shape[0] - self.win_size) // self.step + 1
else:
return (self.test.shape[0] - self.win_size) // self.win_size + 1
def __getitem__(self, index):
index = index * self.step
if self.mode == "train":
return np.float32(self.train[index:index + self.win_size]), np.float32(self.test_labels[0:self.win_size])
elif (self.mode == 'val'):
return np.float32(self.val[index:index + self.win_size]), np.float32(self.test_labels[0:self.win_size])
elif (self.mode == 'test'):
return np.float32(self.test[index:index + self.win_size]), np.float32(
self.test_labels[index:index + self.win_size])
else:
return np.float32(self.test[
index // self.step * self.win_size:index // self.step * self.win_size + self.win_size]), np.float32(
self.test_labels[index // self.step * self.win_size:index // self.step * self.win_size + self.win_size])
def get_loader_segment(data_path, batch_size, win_size=100, step=100, mode='train', dataset='KDD'):
if (dataset == 'SMD'):
dataset = SMDSegLoader(data_path, win_size, step, mode)
elif (dataset == 'MSL'):
dataset = MSLSegLoader(data_path, win_size, 1, mode)
elif (dataset == 'SMAP'):
dataset = SMAPSegLoader(data_path, win_size, 1, mode)
elif (dataset == 'PSM'):
dataset = PSMSegLoader(data_path, win_size, 1, mode)
elif dataset == 'MY_TEST':
dataset = MY_TESTSegLoader(data_path, win_size, 1, mode)
shuffle = False
if mode == 'train':
shuffle = True
data_loader = DataLoader(dataset=dataset,
batch_size=batch_size,
shuffle=shuffle,
num_workers=0)
return data_loader
测试
- 训练后生成checkpoints文件夹,保存训练后的模型
- 在main.py中将mode参数改为test即可测试
- 在solver.py中将test()函数的返回值加一个pred并在main.py中接受,保存成dataFrame即可,可以用于可视化
def main(config):
cudnn.benchmark = True
if (not os.path.exists(config.model_save_path)):
mkdir(config.model_save_path)
solver = Solver(vars(config))
if config.mode == 'train':
solver.train()
elif config.mode == 'test':
_, _, _, _, pred_1, pred = solver.test()
pd.DataFrame(np.concatenate([pred, pred_1]), columns=['pred_after', 'pred']).to_csv(r'pred_res.csv')
return solver