[时序异常检测]Anomaly Transformer算法详解

算法理论

  • 算法数据类型:长度为 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} xiRd
  • 算法目标:检测时序中的异常样本数据
  • 算法前置学习:Transformer
  • 论文地址:论文地址

总体架构

Anomaly 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(Xl1)+Xl1)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}} XlRN×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}} ZlRN×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 Xl1WQl,Xl1WKl,Xl1WVl,Xl1Wσ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,VRN×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,WVlRdmodel×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σi2ji2)]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} p11p21pN1p12p22pN2p1Np2NpNN 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σi2ji2)]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(dmodel QKT)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 SlPl均是 N × N N\times N N×N维矩阵且均是行归一化的,即关于行都是离散分布,但 P l \mathcal{P}^l Pl以Gauss核计算点之间关联,而 S l \mathcal{S}^l Sl用注意力机制计算每个time point与整体的关系

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=1L(KL(Pi,:l∣∣Si,:l)+KL(Si,:l∣∣Pi,:l)]i=1,...,N= L1l=1L(KL(P1,:l∣∣S1,:l)+KL(S1,:l∣∣P1,:l)L1l=1L(KL(P2,:l∣∣S2,:l)+KL(S2,:l∣∣P2,:l)L1l=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)=∣∣XX^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最大或最小化

以上有很多个人理解,如有错误请指出,感激不尽。

代码,训练个人数据集

若要加载个人数据集训练与预测,可根据以下步骤

资源管理结构

在这里插入图片描述

  • 原代码中是没有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
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值