Pytorch深度学习实战(1)—— 使用LSTM 自动编码器进行时间序列异常检测

news2025/7/20 9:45:04

 🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

文章目录

数据

探索性数据分析

LSTM 自动编码器

重建损失

ECG 数据中的异常检测

数据预处理

训练

保存模型

选择阈值

评估

正常听力节拍

异常情况

概括


TL;DR 使用真实世界的心电图 (ECG) 数据来检测患者心跳中的异常情况。我们将构建一个 LSTM 自动编码器,在一组正常的心跳上对其进行训练,并将看不见的示例分类为正常或异常

在本教程中,您将学习如何使用 LSTM 自动编码器检测时间序列数据中的异常。您将使用来自单个心脏病患者的真实心电图数据来检测异常心跳。

  • 在浏览器中运行完整的笔记本 (Google Colab)
  • 阅读使用 Pytorch 完成工作这本书

在本教程结束时,您将学习如何:

  • 准备数据集以从时间序列数据中进行异常检测
  • 使用 PyTorch 构建 LSTM 自动编码器
  • 训练和评估您的模型
  • 选择异常检测的阈值
  • 将看不见的示例分类为正常或异常

数据

该数据集包含 5,000 个时间序列示例(通过 ECG 获得),具有 140 个时间步长。每个序列对应于单个充血性心力衰竭患者的单个心跳。

心电图(ECG 或 EKG)是一种通过测量心脏的电活动来检查心脏功能的测试。每次心跳时,都会有一个电脉冲(或电波)穿过您的心脏。这种波会导致肌肉挤压并从心脏泵出血液。资源

我们有 5 种类型的心跳(类):

  • 正常 (N)
  • R-on-T 室性早搏 (R-on-T PVC)
  • 室性早搏 (PVC)
  • 室上性早搏或异位搏动(SP 或 EB)
  • 未分类节拍 (UB)。

假设一个健康的心脏和每分钟 70 到 75 次的典型心率,每个心动周期或心跳大约需要 0.8 秒才能完成一个周期。频率:每分钟 60-100 次(人类) 持续时间:0.6-1 秒(人类)来源

该数据集在我的 Google Drive 上可用。我们去取得它:

# BASH
!gdown --id 16MIleqoIr1vYxlGk4GKnGmrsCPuWkkpT
!unzip -qq ECG5000.zip
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

数据有多种格式。我们将arff文件加载到 Pandas 数据帧中:

with open('ECG5000_TRAIN.arff') as f:
    train = a2p.load(f)
with open('ECG5000_TEST.arff') as f:
    test = a2p.load(f)

我们将把训练和测试数据合并到一个数据框中。这将为我们提供更多数据来训练我们的自动编码器。我们还将对其进行洗牌:

df = train.append(test)
df = df.sample(frac=1.0)
df.shape

(5000, 141)

 我们有 5,000 个示例。每行代表一个心跳记录。让我们命名可能的类:

CLASS_NORMAL = 1
class_names = ['Normal','R on T','PVC','SP','UB']

 接下来,我们将最后一列重命名为target,以便更容易引用它:

new_columns = list(df.columns)
new_columns[-1] = 'target'
df.columns = new_columns

探索性数据分析

让我们检查一下每个心跳类有多少个示例:

df.target.value_counts()

 Output:

1    2919
2    1767
4     194
3      96
5      24
Name: target, dtype: int64

让我们绘制结果: 

 

     到目前为止,普通班的例子最多。这很棒,因为我们将使用它来训练我们的模型。

让我们看一下每个类的平均时间序列(在顶部和底部用一个标准偏差平滑):

正常类与所有其他类具有明显不同的模式,这是非常好的。也许我们的模型将能够检测到异常?

LSTM 自动编码器

自动编码器的工作是获取一些输入数据,将其传递给模型,并获得输入的重构。重建应该尽可能匹配输入。诀窍是使用少量参数,以便您的模型学习数据的压缩表示。

从某种意义上说,自动编码器试图只学习数据最重要的特征(压缩版本)。在这里,我们将了解如何将时间序列数据提供给自动编码器。我们将使用几个 LSTM 层(因此是 LSTM 自动编码器)来捕获数据的时间依赖性。

要将序列分类为正常或异常,我们将选择一个阈值,超过该阈值时心跳被视为异常。

重建损失

在训练自动编码器时,目标是尽可能地重构输入。这是通过最小化损失函数来完成的(就像在监督学习中一样)。这个函数被称为重建损失。交叉熵损失和均方误差是常见的例子。

ECG 数据中的异常检测

我们将使用正常的心跳作为模型的训练数据并记录重建损失。但首先,我们需要准备数据:

数据预处理

让我们获取所有正常的心跳并删除目标(类)列:

normal_df = df[df.target == str(CLASS_NORMAL)].drop(labels='target', axis=1)
normal_df.shape

(2919, 140)

 我们将合并所有其他类并将它们标记为异常:

anomaly_df = df[df.target != str(CLASS_NORMAL)].drop(labels='target', axis=1)
anomaly_df.shape

 (2081, 140)

我们将正常示例拆分为训练集、验证集和测试集:

train_df, val_df = train_test_split(
    normal_df,
    test_size=0.15,
    random_state=RANDOM_SEED
)
val_df, test_df = train_test_split(
    val_df,
    test_size=0.33,
    random_state=RANDOM_SEED
)

我们需要将我们的示例转换为张量,以便我们可以使用它们来训练我们的自动编码器。让我们为此编写一个辅助函数:

def create_dataset(df):
    sequences = df.astype(np.float32).to_numpy().tolist()
    dataset = [torch.tensor(s).unsqueeze(1).float() for s in sequences]
    n_seq, seq_len, n_features = torch.stack(dataset).shape
    return dataset, seq_len, n_features

每个时间序列都将转换为形状序列长度x特征数(在我们的例子中为 140x1)的 2D 张量。

让我们创建一些数据集:

train_dataset, seq_len, n_features = create_dataset(train_df)
val_dataset, _, _ = create_dataset(val_df)
test_normal_dataset, _, _ = create_dataset(test_df)
test_anomaly_dataset, _, _ = create_dataset(anomaly_df)

 LSTM 自动编码器

                                                                        自动编码器 

示例自动编码器架构图像源

通用的自动编码器架构由两个组件组成。一个压缩输入的编码器和一个尝试重建它的解码器。

我们将使用来自这个GitHub存储库的 LSTM 自动编码器,并进行一些小的调整。我们模型的工作是重建时间序列数据。让我们从编码器开始:

class Encoder(nn.Module):
    def __init__(self, seq_len, n_features, embedding_dim=64):
        super(Encoder, self).__init__()
        self.seq_len, self.n_features = seq_len, n_features
        self.embedding_dim, self.hidden_dim = embedding_dim, 2 * embedding_dim
        self.rnn1 = nn.LSTM(
            input_size=n_features,
            hidden_size=self.hidden_dim,
            num_layers=1,
            batch_first=True
        )
        self.rnn2 = nn.LSTM(
            input_size=self.hidden_dim,
            hidden_size=embedding_dim,
            num_layers=1,
            batch_first=True
        )
    def forward(self, x):
        x = x.reshape((1, self.seq_len, self.n_features))
        x, (_, _) = self.rnn1(x)
        x, (hidden_n, _) = self.rnn2(x)
        return hidden_n.reshape((self.n_features, self.embedding_dim))

编码器使用两个 LSTM 层来压缩时间序列数据输入。

接下来,我们将使用解码器对压缩表示进行解码

class Decoder(nn.Module):
    def __init__(self, seq_len, input_dim=64, n_features=1):
        super(Decoder, self).__init__()
        self.seq_len, self.input_dim = seq_len, input_dim
        self.hidden_dim, self.n_features = 2 * input_dim, n_features
        self.rnn1 = nn.LSTM(
            input_size=input_dim,
            hidden_size=input_dim,
            num_layers=1,
            batch_first=True
        )
        self.rnn2 = nn.LSTM(
            input_size=input_dim,
            hidden_size=self.hidden_dim,
            num_layers=1,
            batch_first=True
        )
        self.output_layer = nn.Linear(self.hidden_dim, n_features)
    def forward(self, x):
        x = x.repeat(self.seq_len, self.n_features)
        x = x.reshape((self.n_features, self.seq_len, self.input_dim))
        x, (hidden_n, cell_n) = self.rnn1(x)
        x, (hidden_n, cell_n) = self.rnn2(x)
        x = x.reshape((self.seq_len, self.hidden_dim))
        return self.output_layer(x)

我们的解码器包含两个 LSTM 层和一个提供最终重建的输出层。

是时候将所有内容包装到一个易于使用的模块中了:

class RecurrentAutoencoder(nn.Module):
    def __init__(self, seq_len, n_features, embedding_dim=64):
        super(RecurrentAutoencoder, self).__init__()
        self.encoder = Encoder(seq_len, n_features, embedding_dim).to(device)
        self.decoder = Decoder(seq_len, embedding_dim, n_features).to(device)
    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x

我们的自动编码器通过编码器和解码器传递输入。让我们创建它的一个实例:

model = RecurrentAutoencoder(seq_len, n_features, 128)
model = model.to(device)

训练

让我们为我们的训练过程编写一个辅助函数:

def train_model(model, train_dataset, val_dataset, n_epochs):
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
    criterion = nn.L1Loss(reduction='sum').to(device)
    history = dict(train=[], val=[])
    best_model_wts = copy.deepcopy(model.state_dict())
    best_loss = 10000.0
    for epoch in range(1, n_epochs + 1):
        model = model.train()
        train_losses = []
        for seq_true in train_dataset:
            optimizer.zero_grad()
            seq_true = seq_true.to(device)
            seq_pred = model(seq_true)
            loss = criterion(seq_pred, seq_true)
            loss.backward()
            optimizer.step()
            train_losses.append(loss.item())
        val_losses = []
        model = model.eval()
        with torch.no_grad():
            for seq_true in val_dataset:
                seq_true = seq_true.to(device)
                seq_pred = model(seq_true)
                loss = criterion(seq_pred, seq_true)
                val_losses.append(loss.item())
        train_loss = np.mean(train_losses)
        val_loss = np.mean(val_losses)
        history['train'].append(train_loss)
        history['val'].append(val_loss)
        if val_loss < best_loss:
            best_loss = val_loss
            best_model_wts = copy.deepcopy(model.state_dict())
        print(f'Epoch {epoch}: train loss {train_loss} val loss {val_loss}')
    model.load_state_dict(best_model_wts)
    return model.eval(), history

 在每个时期,训练过程都会为我们的模型提供所有训练示例,并评估验证集的性能。请注意,我们使用的批量大小为 1(我们的模型一次只能看到 1 个序列)。我们还记录了过程中的训练和验证集损失。

请注意,我们正在最小化L1Loss,它测量 MAE(平均绝对误差)。为什么?重建似乎比 MSE(均方误差)更好。

我们将获得具有最小验证错误的模型版本。让我们做一些训练:

model, history = train_model(
    model,
    train_dataset,
    val_dataset,
    n_epochs=150
)

 我们的模型收敛得很好。似乎我们可能需要一个更大的验证集来平滑结果,但现在就可以了。

保存模型

让我们存储模型以备后用:

MODEL_PATH = 'model.pth'
torch.save(model, MODEL_PATH)

 如果要下载并加载预训练模型,请取消注释下一行:

# !gdown --id 1jEYx5wGsb7Ix8cZAw3l5p5pOwHs3_I9A
# model = torch.load('model.pth')
# model = model.to(device)

选择阈值

有了我们手头的模型,我们可以看看训练集上的重建误差。让我们首先编写一个辅助函数来从我们的模型中获取预测:

def predict(model, dataset):
    predictions, losses = [], []
    criterion = nn.L1Loss(reduction='sum').to(device)
    with torch.no_grad():
        model = model.eval()
        for seq_true in dataset:
            seq_true = seq_true.to(device)
            seq_pred = model(seq_true)
            loss = criterion(seq_pred, seq_true)
            predictions.append(seq_pred.cpu().numpy().flatten())
            losses.append(loss.item())
    return predictions, losses

我们的函数遍历数据集中的每个示例并记录预测和损失。让我们得到损失并看看它们:

_, losses = predict(model, train_dataset)
sns.distplot(losses, bins=50, kde=True);
THRESHOLD = 26

评估

使用阈值,我们可以将问题转化为简单的二元分类任务:

  • 如果示例的重建损失低于阈值,我们会将其归类为正常心跳
  • 或者,如果损失高于阈值,我们会将其归类为异常

正常听力节拍

让我们检查一下我们的模型在正常心跳上的表现如何。我们将使用测试集中的正常心跳(我们的模型没有看到这些):

predictions, pred_losses = predict(model, test_normal_dataset)
sns.distplot(pred_losses, bins=50, kde=True);


 我们将计算正确的预测:

correct = sum(l <= THRESHOLD for l in pred_losses)
print(f'Correct normal predictions: {correct}/{len(test_normal_dataset)}')

Correct normal predictions: 142/145 

异常情况

我们将对异常示例执行相同的操作,但它们的数量要高得多。我们将得到一个与正常心跳大小相同的子集:

anomaly_dataset = test_anomaly_dataset[:len(test_normal_dataset)]

 现在我们可以对异常子集的模型进行预测:

predictions, pred_losses = predict(model, anomaly_dataset)
sns.distplot(pred_losses, bins=50, kde=True);

 最后,我们可以统计超过阈值的示例数量(视为异常): 

correct = sum(l > THRESHOLD for l in pred_losses)
print(f'Correct anomaly predictions: {correct}/{len(anomaly_dataset)}')

 Correct anomaly predictions: 142/145 

我们取得了非常好的结果。在现实世界中,您可以根据要容忍的错误类型来调整阈值。在这种情况下,您可能希望误报(正常心跳被视为异常)多于误报(异常被视为正常)。

看例子

我们可以叠加真实的和重建的时间序列值,看看它们有多接近。我们将针对一些正常和异常情况执行此操作:

 

概括

在本教程中,您学习了如何使用 PyTorch 创建 LSTM 自动编码器,并使用它来检测 ECG 数据中的心跳异常。

  • 在浏览器中运行完整的笔记本 (Google Colab)
  • 阅读使用 Pytorch 完成工作这本书

你学会了如何:

  • 准备数据集以从时间序列数据中进行异常检测
  • 使用 PyTorch 构建 LSTM 自动编码器
  • 训练和评估您的模型
  • 选择异常检测的阈值
  • 将看不见的示例分类为正常或异常

虽然我们的时间序列数据是单变量的(我们只有 1 个特征),但代码应该适用于多变量数据集(多个特征),几乎不需要修改。随意尝试!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/17282.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

并发编程面试题

并发 为什么要使用并发编程&#xff08;并发编程的优点&#xff09; 并发编程可以提升 CPU 的计算能力的利用率&#xff0c;通过并发编程的形式可以将多核CPU 的计算能力发挥到极致提升程序的性能&#xff0c;如&#xff1a;响应时间、吞吐量、计算机资源使用率等。并发程序可…

Golang 数组基础

数组内部实现和基础功能 了解Go的数据结构&#xff0c;一般会从数组开始&#xff0c;因为数组是切片和映射的基础数据结构。 内部实现 在Go语言中&#xff0c;数组是一个长度固定的数据类型&#xff0c;用于存储一段具有相同的类型的元素连续块。数组的类型是固定统一的&…

Vue框架插槽(第八课)

案例 组件信息的通信 自己思考一下 答案在本文章的后面插槽 v-slot 这个时候我们就可以来定义插槽slot&#xff1a; 插槽的使用过程其实是抽取共性、预留不同&#xff1b;我们会将共同的元素、内容依然在组件内进行封装&#xff1b;同时会将不同的元素使用slot作为占位&#xf…

CTC 技术介绍概述——啃论文系列

CTC 技术介绍概述——啃论文系列 文章目录CTC 技术介绍概述——啃论文系列自我介绍摘要前言知识导图1. 定义2. 诞生背景2.1 频谱紧张例子&#xff0c;wifi的5GHz2.2 通信干扰——CTI2.3 管理困难2.4 异构通信传统实现——网关桥接2.5 CTC——异构直接通信3. 包级CTC3.1 基于RSS…

个人设计web前端大作业 基于html5制作美食菜谱网页设计作业代码

&#x1f380; 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

基于PHP+MySQL长途客用汽车票订票系统的设计与实现

随着时代的变迁汽车已经成为了人们日常生活中不可或缺的一部分,虽然很多人已经拥有了私家车,但是很多时候因为离家较远等原因,很多时候人们还是会通过客用汽车来进行一些出行,但是通常情况下客用车票都需要到客用站进行购买,这极其的不便利。 为了能够让用户足不出户就可以进行…

拿稳这24道JVM面试题,要价30k都有底气

1.什么是JVM? JVM 的全称是 「Java Virtual Machine」&#xff0c;也就是我们耳熟能详的 Java 虚拟机。它能识别 .class后缀的文件&#xff0c;并且能够解析它的指令&#xff0c;最终调用操作系统上的函数&#xff0c;完成我们想要的操作。 C开发出来的程序&#xff0c;编译…

SpringBoot SpringBoot 开发实用篇 4 数据层解决方案 4.10 MongoDB 基础操作

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇4 数据层解决方案4.10 MongoDB 基础操作4.10.1 MongoDB 基础操作4.10…

(3)paddle---近视眼睛分类的例子

1主要参考 &#xff08;0&#xff09;本教程和以下教程不够详细&#xff0c;还是推荐下面这个大佬的blog看一下 计算机视觉——眼疾图片识别&#xff08;数据集iChallenge-PM&#xff09;_「已注销」的博客-CSDN博客 &#xff08;1&#xff09;blibli视频 252-06_预测病理性…

Vite2 + Vue3 + TypeScript + Pinia 搭建一套企业级的开发脚手架

Vue2 与 Vue3 的区别 Vue3由于完全由TS进行重写&#xff0c;在应用中对类型判断的定义和使用有很强的表现。同一对象的多个键返回值必须通过定义对应的接口&#xff08;interface&#xff09;来进行类型定义。要不然在 ESLint 时都会报错。Vue2 与 Vue3 最大的区别: Vue2 使用…

jquery把页面<table>里的内容导出为后缀名为.xlsx的excel

1、问题描述 之前是用Blob把页面的<table>导出成.xls的Excel文件&#xff1a;Blob把html导出为excel文件_金斗潼关的博客-CSDN博客 不过由于微软的Excel对.xls扩展名的文件支持不是很好&#xff0c;打开会报一个警告 所以用户反馈说是不想弹出这个警告&#xff0c;要求…

【深度学习入门 2022 最新版】第一课 深度学习简介

【深度学习入门 2022 最新版】第一课 深度学习简介概述深度学习 vs 机器学习机器学习是什么深度学习是什么机器学习和深度学习的区别神经网络机器学习实现二分类神经网络实现二分类TensorFlowPyTorch神经网络的原理张量张量最小值 (补充)张量最大值 (补充)前向传播损失计算反向…

手撕AVL树

目录 一、概念 二、 结点的定义 2.1 键值对pair 2.2 定义细节 三、 AVL树的插入操作 3.1 平衡因子调整规则 3.2 旋转规则 3.2.1 新节点插入较高左子树的左侧 — 左左:右单旋 3.2.2 新节点插入较高右子树的右侧 — 右右:左单旋 3.2.3 新节点插入较高左子树的右侧 — …

论文管理系统(准备工作)

目录 一、项目需求响应图 二、准备工作 2.1创建一个Spring Initializr项目 2.2后端架构 2.2.1 controller层 2.2.2 entity层 2.2.3 interceptors层 2.2.4 mapper层 2.2.5 serivice层 2.2.6 main运行 2.2.7 mappers文件 2.3配置 application.yml文件 2.4加入依赖 一、项…

Postman如何携带token——Bearer Token和Headers

目录一、使用场景二、设置Bearer Token1.设置你的环境变量2.项目集合设置认证方式及环境变量3.登录接口的脚本三、通过脚本设置Headers1.登录请求设置环境变量2.设置集合的发送请求脚本一、使用场景 现在许多项目都使用jwt来实现用户登录和数据权限&#xff0c;校验过用户的用…

C++ Builder XE关于AdvStringGrid对EXCEL母表快速分表,并批量插入sheet子表简单操作

如何快速将ECXEL母表快速批量生成多个子表分表&#xff0c;并且在表中插入sheet子表的简单操作&#xff1a; //AdvStringGrid2->SaveToXLS(filename,false);//生成新EXCEL表格 //AdvStringGrid2->SaveToXLSSheet(filename,Fname);//插入sheet子表 //-----------------…

一篇五分生信临床模型预测文章代码复现——Figure 4-6 临床模型构建(五)

之前讲过临床模型预测的专栏,但那只是基础版本,下面我们以自噬相关基因为例子,模仿一篇五分文章,将图和代码复现出来,学会本专栏课程,可以具备发一篇五分左右文章的水平: 本专栏目录如下: Figure 1:差异表达基因及预后基因筛选(图片仅供参考) Figure 2. 生存分析,…

XSS-labs靶场实战(五)——第12-14关

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是XSS-labs靶场实战第12-14关。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严禁对…

认识和使用容器

目录 前言 一、容器化背后的发展历史和概念 1.容器的抽象 容器的比喻 2.计算机领域的容器 容器是一种标准化的软件单元 二、容器和微服务架构 1.容器的作用 快速搭建开发环境 将运行环境和配置放在代码中并部署 使用docker-compose来模拟生产环境 使用docker镜像进…

在JVM 中进程与线程关系、介绍线程:程序计数器、本地方法栈、虚拟机栈

首先,我们要了解进程和线程的基本概念 进程 process 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的*.exe应用程序就是一个进程。 线程 thread 进程中的一个执行任务(控制单元),负责当前进…