CNN+BiLSTM !!最强序列建模组合!!!

哈喽,我是我不是小upper~

做深度学习的同学肯定都有过这困扰:处理时序数据、文本序列或是传感器序列时,CNN 和 LSTM 到底该 pick 谁?

其实不用死磕二选一!

对于大多数需要兼顾 "抓局部细节模式" 和 "连长程关联" 的任务来说,CNN+BiLSTM 就是对超有默契的黄金搭档,强强联合才是最优解呀~


为什么是 CNN+BiLSTM?

在深度学习处理时序数据、文本序列或传感器序列的场景中,很多从业者都会纠结模型选型,但CNN与BiLSTM的组合绝非二选一的取舍,而是针对序列建模核心痛点的 "精准破局方案"。

一、序列建模的核心痛点与单模型局限

我们在做序列建模时,始终面临两个绕不开的核心需求,同时也是单模型难以兼顾的痛点:

  1. 局部模式的精准捕捉:序列中往往存在关键局部结构,比如文本里情感词的固定搭配、传感器数据中突发事件对应的数值尖峰,这类特征需要依赖局部感受野来锁定;
  2. 长程依赖的全局关联:序列的完整语义或趋势由上下文的长距离关系决定,比如文本中否定词对后续情感倾向的扭转、患者生理指标在跨时段的关联影响,这类关联需要具备记忆和双向上下文感知的能力。

若仅用 CNN,虽能高效抓取局部特征,却会因感受野有限而忽略跨长时段的关联;若仅用 LSTM,虽能建模长程依赖,但存在参数冗余、训练效率低的问题,且对局部微小模式的感知效率远不及卷积。

而 CNN+BiLSTM 的组合恰好实现优势互补:CNN 为模型搭建稳定的局部特征金字塔并赋予局部不变性,BiLSTM 则通过门控机制实现双向信息整合与长程记忆,既保障了梯度传播的稳健性,又能兼顾局部与全局特征。因此在多数序列分类任务中,该组合能显著提升模型性能与泛化能力。

二、底层原理

(一)一维卷积:序列局部特征的高效提取

当处理序列数据时,一维卷积会沿时间轴滑动卷积核,完成局部特征的提取与层级化构建。

1. 输入与卷积输出定义

设输入为长度为T的序列,每个时间步的特征维度为,则输入可表示为矩阵。若卷积核大小为k(时间维度的窗口长度)、输入通道数为(即din)、输出通道数为,则在时间步t处,卷积输出第c个通道的特征可表示为:

其中,为卷积核权重,为偏置项,σ为 ReLU、Sigmoid 等非线性激活函数,且需满足(超出序列长度的位置可通过 padding 补齐)。

2. 层级化感受野的递推

通过堆叠多层卷积,可逐步扩大模型的感受野,实现从细粒度到粗粒度的特征提取。若不使用空洞卷积,设第l层卷积的感受野为,卷积核大小为,则感受野的递推公式为:

其中,(首层感受野等于卷积核大小),为第m层卷积的步幅(若步幅=1,则公式简化为)。

3. 一维卷积的核心优势
  • 可在局部窗口内形成专属 "模式检测器",对序列中的尖峰、固定词组等局部特征具备平移不变性;
  • 基于参数共享机制,计算效率高且对局部噪声鲁棒性强;
  • 可搭配池化层或步幅卷积,构建多尺度特征金字塔。

(二)双向 LSTM(BiLSTM):长程双向上下文的精准建模

LSTM 通过独特的门控机制缓解了传统 RNN 的梯度消失问题,可稳定记忆远距离信息;而 BiLSTM 则同时沿正向 (从序列开头到结尾)和反向(从序列结尾到开头)处理数据,实现双向上下文的深度融合。

1. 单向 LSTM 的门控机制

设单向 LSTM 的输入为时间步t的特征,上一时刻记忆单元为、隐状态为,各权重矩阵分别为W(输入权重)、U(隐状态权重),偏置为b,则各核心模块的计算公式为:

  • 输入门 :控制新信息的输入权重
  • 遗忘门 :控制历史记忆的保留比例
  • 输出门 :控制记忆单元向隐状态的输出比例
  • 候选记忆 :生成当前时刻的候选记忆内容
  • 记忆单元更新 :融合历史记忆与当前候选记忆
  • 隐状态输出 :生成当前时刻的隐状态

其中,⊙为哈达玛积(逐元素相乘),σ为 Sigmoid 激活函数,tanh为双曲正切激活函数。

2. BiLSTM 的双向信息融合

设正向 LSTM 在时间步t的隐状态为,反向 LSTM 的隐状态为,单向隐状态维度为,则 BiLSTM 的最终隐状态为双向隐状态的拼接:

其中,";" 表示维度拼接操作。

3. BiLSTM 的核心优势
  • 实现双向上下文的整合,可精准理解序列的完整语义(如文本中前后文的逻辑关联);
  • 门控机制保障了长程依赖建模的稳定性,避免了传统 RNN 的梯度消失问题;
  • 分类任务中可灵活选择最后时刻隐状态、全局平均池化或注意力池化完成特征汇聚。

三、CNN+BiLSTM 的融合结构与信息流

1. 经典融合架构

CNN+BiLSTM 的标准流程为 **"CNN 局部特征提取→BiLSTM 长程依赖建模→全连接层分类"**:先通过一维 CNN 将原始序列转化为结构化的高层局部特征,再将卷积输出的通道特征作为 BiLSTM 的输入,最后通过特征聚合与全连接层输出类别概率分布。

2. 完整信息流公式

设原始输入序列为,经L层一维 CNN 后得到高层局部特征(T′为卷积后序列长度,由 padding 和步幅决定);将输入 BiLSTM,得到各时间步的双向隐状态

最后可通过以下 3 种方式完成特征聚合,再送入全连接层FC得到类别分布

  • 最后时刻聚合 :直接取序列末尾的双向隐状态作为全局特征
  • 全局平均池化 :对所有时间步的隐状态做均值聚合,兼顾全序列信息
  • 注意力池化 :为不同时间步隐状态分配权重,聚焦关键特征先计算注意力权重 再加权求和得到全局特征:

其中,为注意力权重参数,ba∈Rda为偏置项,da为注意力隐层维度。

3. 融合架构的核心优势

  • 降噪减负:CNN 先将原始输入中的噪声转化为结构化通道特征,大幅降低 BiLSTM 的特征处理负担;
  • 加速收敛:卷积的局部特征先验让模型无需在原始噪声数据中盲目学习,提升训练效率;
  • 优势协同:BiLSTM 在卷积特征基础上建模,可精准挖掘局部模式的长程序列关联;
  • 参数优化:将局部特征学习任务交给轻量高效的 CNN,避免了用超大 LSTM 隐层实现端到端记忆的参数冗余问题。

四、何时 CNN+BiLSTM 更优?

CNN 与 BiLSTM 的组合并非万能解,但在中等长度、同时具备显著局部模式与长程上下文逻辑的序列建模任务中,是兼顾性能与效率的高性价比方案。不同领域的任务适配逻辑、特征流转机制及选型边界可具体拆解如下:

1. 典型适配场景的核心逻辑与特征公式

1.1 文本分类任务:兼顾局部搭配与全局语义

文本分类的核心需求是同时识别词 / 短语级的局部情感 / 语义搭配 (如 "不 + 好用" 这类否定情感组合)和整句的全局上下文逻辑(如前文铺垫对后文观点的影响)。

  • CNN 的作用:以词嵌入为输入,通过一维卷积捕捉局部词组搭配的特征模式,过滤无意义的词汇噪声;
  • BiLSTM 的作用:在卷积提取的局部特征基础上,整合双向上下文语义,完成情感或类别判定。
对应场景的特征流转公式

设文本序列的词嵌入矩阵为(T为文本长度,为词嵌入维度),经一维 CNN 处理后得到局部词组特征,再输入 BiLSTM 得到双向隐状态,最终通过注意力池化聚合特征并分类:

其中为全连接层权重,为文本分类类别数。

1.2 生理信号 / 工业传感器时序任务:捕捉局部异常与跨时段演变

生理信号(如心电、脑电)和工业传感器数据的核心需求是识别局部异常波形 (如心电的早搏峰值、传感器的故障尖峰)和跨时段的状态演变(如患者症状的渐进性变化、设备参数的缓慢漂移)。

  • CNN 的作用:对原始时序信号做局部卷积,精准定位波形中的异常段和特征峰值,生成结构化的局部状态特征;
  • BiLSTM 的作用:整合这些局部状态的时序关联,挖掘先兆模式与故障 / 病症的长程依赖。
对应场景的特征流转公式

设传感器 / 生理信号的原始时序为为信号维度,如多导联心电的导联数),经多层一维 CNN 和池化操作后得到局部波形特征,输入 BiLSTM 后采用全局平均池化聚合全时段状态特征(避免单时刻特征的偶然性):

若需聚焦异常时段,可替换为最大池化,强化异常特征的权重。

1.3 金融时间序列任务:整合短期形态与长程策略逻辑

金融时序(如股票 K 线、期货价格)的核心需求是识别短期价格形态 (如阳包阴 K 线组合、MACD 金叉的局部走势)和长程趋势逻辑(如政策周期对价格的持续影响、多周期指标的联动)。

  • CNN 的作用:对 K 线的价格、成交量等维度做局部卷积,提取短期技术形态特征;
  • BiLSTM 的作用:整合这些短期形态的时序关联,挖掘长程的风险累积或趋势延续逻辑。
对应场景的特征流转公式

设金融时序的多维度指标(价格、成交量、均线等)为,经 CNN 得到短期形态特征,输入 BiLSTM 后结合最后时刻聚合(聚焦最新趋势)完成趋势 / 涨跌分类:

1.4 多模态序列任务:实现局部模态聚合与跨模态时序对齐

多模态序列(如视频的帧图像 + 音频时序、医疗的影像切片 + 检验指标时序)的核心需求是聚合单模态的局部特征 (如影像的病灶区域、音频的特征频段)和实现跨模态的时序对齐(如影像病灶变化与检验指标的时序关联)。

  • CNN 的作用:对各模态的局部信息分别做卷积(如 2D CNN 处理影像切片、1D CNN 处理音频 / 检验指标),完成单模态局部特征的提取与聚合;
  • BiLSTM 的作用:对多模态卷积特征做时序对齐,整合跨模态的长程上下文关联。
对应场景的特征流转公式

设模态 1(如影像切片时序)为,经 2D CNN 得到局部特征;模态 2(如检验指标)为,经 1D CNN 得到特征;将多模态特征拼接为,再输入 BiLSTM 完成跨模态时序建模:

2. 方案选型边界:何时不用 CNN+BiLSTM?

2.1 单 CNN 即可胜任的场景

当序列长度极短 (如短句分类、单周期传感器数据)或长程依赖不显著(如独立的文本短语情感判定、单批次工业质检信号)时,无需 BiLSTM 的长程建模能力,单 CNN 即可通过多层卷积扩大感受野,以更低的计算成本完成任务。

2.2 注意力 / 纯 Transformer 更优的场景

当序列的局部模式不明显、核心逻辑高度依赖全局关联时(如长文档的语义摘要、多轮对话的意图理解),CNN 的局部感受野会限制全局信息捕捉;此时基于自注意力的 Transformer 或纯注意力模型,可通过全局注意力机制直接建模任意位置的关联,效果优于 CNN+BiLSTM。

2.3 核心结论

只有在序列为中等长度、同时具备 "可识别的局部模式" 和 "强关联性的长程上下文" 时,CNN+BiLSTM 才能最大化其 "局部特征提取 + 长程依赖整合" 的协同优势,成为兼顾性能与效率的最优解。

完整案例

为了方便说明问题,我们构造一个虚拟序列分类任务,共 4 类。

整个数据特性:

每条序列长度 T=50,每步特征维度d=8 。

每个类别由不同的局部模式和长程规则定义:

  • 类 0:序列前段出现上升局部模式且后段平稳。

  • 类 1:在中段出现两个相距一定的尖峰对

  • 类 2:序列末段出现下降局部模式

  • 类 3:全局和局部混合规则,比如起伏中但均值偏高 并伴随一个微小尖峰

CNN 负责检测局部模式(如尖峰、上升/下降片段),BiLSTM 负责根据模式的出现位置与组合关系进行判别。

python 复制代码
import os
import random
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
from sklearn.metrics import confusion_matrix, roc_curve, auc, accuracy_score
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
import seaborn as sns
from torch.optim.lr_scheduler import ReduceLROnPlateau
import sklearn  # 查看版本
print(f"sklearn版本: {sklearn.__version__}")

def set_seed(seed=42):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True  # 增加确定性

set_seed(42)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 优先用GPU
print(f"使用设备: {device}")

# 合成序列数据生成(增加轻微噪声增强,提升泛化性)
def generate_sequence(num_samples=2000, seq_len=50, feat_dim=8, num_classes=4):
    """
    生成带有类别特有局部模式与长程关系的序列数据,增加随机噪声增强泛化性。
    """
    X = np.random.randn(num_samples, seq_len, feat_dim) * 0.4  # 基础噪声
    y = np.zeros(num_samples, dtype=np.int64)
    
    for i in range(num_samples):
        cls = np.random.randint(0, num_classes)
        y[i] = cls
        
        # 基础趋势(不同类别的上升或下降趋势)
        trend = np.linspace(0, 1, seq_len) if cls in [0, 3] else np.linspace(1, 0, seq_len)
        X[i, :, 0] += trend * (0.8 if cls==0 else 0.5)
        X[i, :, 1] += trend[::-1] * (0.3 if cls==2 else 0.2)
        
        # 类 1:局部尖峰对
        if cls == 1:
            pos1 = np.random.randint(seq_len//3, seq_len//2)
            pos2 = pos1 + np.random.randint(5, 12)
            X[i, pos1, 2] += 3.0
            X[i, pos2, 2] += 2.5
            # 尖峰附近做平滑增强
            for p in [pos1, pos2]:
                for w in range(-2, 3):
                    idx = np.clip(p + w, 0, seq_len-1)
                    X[i, idx, 2] += (2.0 - abs(w)) * 0.3
        
        # 类 0:序列开头的上升模式
        if cls == 0:
            start = np.random.randint(0, 6)
            window = range(start, min(start+8, seq_len))
            for t in window:
                X[i, t, 3] += 0.2 * (t - start)
            X[i, seq_len//2:, 3] += 0.0  # 后半段保持平稳
        
        # 类 2:结尾区域下降模式
        if cls == 2:
            end = np.random.randint(seq_len-10, seq_len-3)
            window = range(max(0, end-8), end)
            base = X[i, :, 4].copy()
            for j, t in enumerate(window):
                X[i, t, 4] += -0.25 * j
            X[i, end:, 4] += -0.25 * len(list(window))
        
        # 类 3:全局均值更高 + 中间小峰值 + 正弦波动
        if cls == 3:
            X[i, :, 5] += 0.8
            mid = np.random.randint(seq_len//2-4, seq_len//2+4)
            X[i, mid, 5] += 1.5
            osc = 0.3 * np.sin(np.linspace(0, 6*np.pi, seq_len))
            X[i, :, 6] += osc
    
    # 对通道做随机置换增加难度
    perm = np.arange(feat_dim)
    np.random.shuffle(perm)
    X = X[:, :, perm]
    
    # 增加轻微随机噪声(提升泛化性)
    X += np.random.randn(*X.shape) * 0.05
    return X.astype(np.float32), y

# 数据增强:序列时间轴轻微偏移
def augment_sequence(x):
    """对序列做随机时间轴偏移(-2~+2步),增强鲁棒性"""
    shift = np.random.randint(-2, 3)
    if shift == 0:
        return x
    elif shift > 0:
        return np.pad(x[shift:], ((0, shift), (0, 0)), mode='edge')
    else:
        return np.pad(x[:shift], ((-shift, 0), (0, 0)), mode='edge')

# 生成训练/验证/测试集
num_classes = 4
train_X, train_y = generate_sequence(num_samples=2400, seq_len=50, feat_dim=8, num_classes=num_classes)
val_X, val_y = generate_sequence(num_samples=600, seq_len=50, feat_dim=8, num_classes=num_classes)
test_X, test_y = generate_sequence(num_samples=600, seq_len=50, feat_dim=8, num_classes=num_classes)

# 转换为Tensor并创建DataLoader
train_ds = TensorDataset(torch.tensor(train_X), torch.tensor(train_y))
val_ds = TensorDataset(torch.tensor(val_X), torch.tensor(val_y))
test_ds = TensorDataset(torch.tensor(test_X), torch.tensor(test_y))

train_loader = DataLoader(train_ds, batch_size=64, shuffle=True, drop_last=True)
val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)
test_loader = DataLoader(test_ds, batch_size=128, shuffle=False)


class CNNBiLSTM(nn.Module):
    def __init__(self, input_dim=8, cnn_channels=[64, 128], kernel_size=5, lstm_hidden=64, 
                 num_classes=4, dropout=0.3, lstm_dropout=0.2):
        super().__init__()
        # 定义LSTM层数(修复NameError的核心)
        lstm_num_layers = 1
        
        # Conv1d 输入维度:(N, C_in, L)
        self.conv_block = nn.Sequential(
            nn.Conv1d(in_channels=input_dim, out_channels=cnn_channels[0], 
                      kernel_size=kernel_size, padding=kernel_size//2),
            nn.BatchNorm1d(cnn_channels[0]),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.MaxPool1d(kernel_size=2, stride=1),  # 增加池化,降低序列长度
            nn.Conv1d(in_channels=cnn_channels[0], out_channels=cnn_channels[1], 
                      kernel_size=kernel_size, padding=kernel_size//2),
            nn.BatchNorm1d(cnn_channels[1]),
            nn.ReLU(),
            nn.Dropout(dropout)
        )
        
        # BiLSTM 输入维度=最后一层 CNN 的通道数(修复dropout判断的变量)
        self.lstm = nn.LSTM(
            input_size=cnn_channels[-1], 
            hidden_size=lstm_hidden, 
            batch_first=True, 
            bidirectional=True, 
            num_layers=lstm_num_layers,  # 使用提前定义的变量
            dropout=lstm_dropout if lstm_num_layers>1 else 0  # 修复:用定义好的lstm_num_layers
        )
        
        # 分类层:改用全局平均池化(更鲁棒,避免仅依赖最后时间步)
        self.fc = nn.Sequential(
            nn.Linear(2*lstm_hidden, 128),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(128, num_classes)
        )
    
    def forward(self, x):
        # 输入 x: (N, T, D) → 转成 (N, D, T) 适配Conv1d
        x = x.transpose(1, 2)  
        z = self.conv_block(x)  # (N, C, T)
        
        # 转回 (N, T, C) 适配LSTM
        z = z.transpose(1, 2)  
        out, (hn, cn) = self.lstm(z)  # out: (N, T, 2h)
        
        # 全局平均池化(替代仅取最后时间步,提升泛化性)
        agg = torch.mean(out, dim=1)  # (N, 2h)
        logits = self.fc(agg)
        return logits, agg  # 返回特征用于 t-SNE 可视化

# 初始化模型(现在无变量未定义错误)
model = CNNBiLSTM(
    input_dim=8, 
    cnn_channels=[64, 128], 
    kernel_size=5, 
    lstm_hidden=64, 
    num_classes=num_classes, 
    dropout=0.3  # 增加dropout率
).to(device)

# 评估函数
def evaluate(model, loader):
    model.eval()
    all_logits = []
    all_labels = []
    with torch.no_grad():
        for xb, yb in loader:
            xb, yb = xb.to(device), yb.to(device)
            logits, _ = model(xb)
            all_logits.append(logits.cpu().numpy())
            all_labels.append(yb.cpu().numpy())
    logits = np.concatenate(all_logits, axis=0)
    labels = np.concatenate(all_labels, axis=0)
    preds = logits.argmax(axis=1)
    acc = accuracy_score(labels, preds)
    loss = nn.CrossEntropyLoss()(torch.tensor(logits), torch.tensor(labels)).item()
    return loss, acc, logits, labels

# 优化器与学习率调度器(核心:增加学习率衰减)
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=5e-4)  # 增加权重衰减
criterion = nn.CrossEntropyLoss()
scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3, verbose=True)

history = {"train_loss": [], "val_loss": [], "train_acc": [], "val_acc": []}
best_val_acc = 0.0
patience = 5  # 早停耐心值
patience_counter = 0

# 模型训练(增加数据增强+梯度裁剪+早停)
epochs = 25
for epoch in range(1, epochs+1):
    model.train()
    total_loss = 0.0
    total_correct = 0
    total_count = 0
    
    for xb, yb in train_loader:
        xb, yb = xb.to(device), yb.to(device)
        
        # 训练时数据增强:随机时间轴偏移
        if np.random.rand() > 0.2:  # 80%概率增强
            xb = torch.stack([torch.tensor(augment_sequence(x.cpu().numpy())) for x in xb]).to(device)
        
        optimizer.zero_grad()
        logits, _ = model(xb)
        loss = criterion(logits, yb)
        loss.backward()
        
        # 梯度裁剪(防止梯度爆炸)
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        
        optimizer.step()
        
        total_loss += loss.item() * xb.size(0)
        preds = logits.argmax(dim=1)
        total_correct += (preds == yb).sum().item()
        total_count += xb.size(0)
    
    train_loss = total_loss / total_count
    train_acc = total_correct / total_count
    
    # 验证集评估
    val_loss, val_acc, _, _ = evaluate(model, val_loader)
    
    # 学习率调度
    scheduler.step(val_loss)
    
    # 早停逻辑
    if val_acc > best_val_acc:
        best_val_acc = val_acc
        patience_counter = 0
        torch.save(model.state_dict(), "best_cnn_bilstm.pth")  # 保存最优模型
    else:
        patience_counter += 1
        if patience_counter >= patience:
            print(f"早停触发!Epoch: {epoch}, 最佳验证准确率: {best_val_acc:.4f}")
            break
    
    history["train_loss"].append(train_loss)
    history["train_acc"].append(train_acc)
    history["val_loss"].append(val_loss)
    history["val_acc"].append(val_acc)
    
    print(f"Epoch {epoch:02d}: train_loss={train_loss:.4f} train_acc={train_acc:.4f} "
          f"val_loss={val_loss:.4f} val_acc={val_acc:.4f}")

# 加载最优模型评估测试集
model.load_state_dict(torch.load("best_cnn_bilstm.pth"))
test_loss, test_acc, test_logits, test_labels = evaluate(model, test_loader)
print(f"\n[Test] loss={test_loss:.4f} acc={test_acc:.4f}")

# 提取测试特征用于 t-SNE
model.eval()
test_features = []
test_preds = []
with torch.no_grad():
    for xb, yb in test_loader:
        logits, feats = model(xb.to(device))
        test_features.append(feats.cpu().numpy())
        test_preds.append(logits.argmax(dim=1).cpu().numpy())
test_features = np.concatenate(test_features, axis=0)
test_preds = np.concatenate(test_preds, axis=0)

# 多分类 ROC(One-vs-Rest)
y_true_onehot = np.eye(num_classes)[test_labels]
y_score = test_logits
fpr = dict()
tpr = dict()
roc_auc = dict()
for i in range(num_classes):
    fpr[i], tpr[i], _ = roc_curve(y_true_onehot[:, i], y_score[:, i])
    roc_auc[i] = auc(fpr[i], tpr[i])

# 混淆矩阵
cm = confusion_matrix(test_labels, test_logits.argmax(axis=1))

# t-SNE 降维(sklearn 1.7.1兼容:用max_iter)
tsne = TSNE(
    n_components=2, 
    random_state=42, 
    perplexity=30, 
    max_iter=800  # sklearn 1.7.1支持的参数
)
X_tsne = tsne.fit_transform(test_features)

# 可视化分析
plt.rcParams['font.sans-serif'] = ['SimHei']  # 中文显示
plt.rcParams['axes.unicode_minus'] = False

# 1) 训练 & 验证曲线(双坐标轴)
plt.figure(figsize=(10, 6))
plt.title("训练/验证曲线", fontsize=16)

epochs_range = np.arange(1, len(history["train_loss"])+1)
color_loss_train = '#ff00ff'
color_loss_val = '#ff7f0e'
color_acc_train = '#17becf'
color_acc_val = '#2ca02c'

ax1 = plt.gca()
lns1 = ax1.plot(epochs_range, history["train_loss"], color=color_loss_train, linewidth=2.5, label='训练损失')
lns2 = ax1.plot(epochs_range, history["val_loss"], color=color_loss_val, linewidth=2.5, linestyle='--', label='验证损失')
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.grid(True, alpha=0.3)

ax1b = ax1.twinx()
lns3 = ax1b.plot(epochs_range, history["train_acc"], color=color_acc_train, linewidth=2.5, label='训练准确率')
lns4 = ax1b.plot(epochs_range, history["val_acc"], color=color_acc_val, linewidth=2.5, linestyle='--', label='验证准确率')
ax1b.set_ylabel('Accuracy')

lns = lns1 + lns2 + lns3 + lns4
labels = [l.get_label() for l in lns]
ax1.legend(lns, labels, loc='lower right', fontsize=10)
plt.tight_layout()
plt.show()

# 2) 混淆矩阵
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='magma', cbar=True, square=True)
plt.xlabel('预测类别')
plt.ylabel('真实类别')
plt.title(f'混淆矩阵 (测试准确率={test_acc:.2f})')
plt.tight_layout()
plt.show()

# 3) 多分类 ROC 曲线
plt.figure(figsize=(10, 7))
colors = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3']

for i in range(num_classes):
    plt.plot(fpr[i], tpr[i], color=colors[i], linewidth=2.5, label=f'类别 {i} (AUC={roc_auc[i]:.2f})')

plt.plot([0, 1], [0, 1], color='#7f7f7f', linestyle='--', linewidth=1.5, label='随机水平')
plt.xlabel('假阳性率 (FPR)')
plt.ylabel('真阳性率 (TPR)')
plt.title('多分类 ROC 曲线')
plt.grid(True, alpha=0.3)
plt.legend(loc='lower right', fontsize=10)
plt.tight_layout()
plt.show()

# 4) t-SNE 特征可视化
plt.figure(figsize=(10, 7))
markers = ['o', '^', 's', '*']
palette = ['#fb9a99', '#1f78b4', '#33a02c', '#fdbf6f']

for cls in range(num_classes):
    idx = np.where(test_labels == cls)[0]
    plt.scatter(X_tsne[idx, 0], X_tsne[idx, 1], s=35, marker=markers[cls], c=palette[cls],
                edgecolor='k', linewidth=0.5, alpha=0.85, label=f'类别 {cls}')

plt.title('CNN+BiLSTM 全局平均池化特征的 t-SNE')
plt.xlabel('维度 1')
plt.ylabel('维度 2')
plt.grid(True, alpha=0.3)
plt.legend(loc='best', fontsize=10)
plt.tight_layout()
plt.show()

训练/验证损失与准确率

模型的收敛趋势与是否过拟合。左 Y 轴是损失,右 Y 轴是准确率。实线为训练、虚线为验证。

从图中,咱们可以判断,模型是否在稳定收敛?若训练损失持续下降,验证损失也下降,说明学习有效。

是否出现过拟合?若训练准确率持续上升,但验证准确率不升反降,说明过拟合,需加大正则化或减少复杂度。

是否需要调节学习率?若震荡明显或收敛缓慢,可能需要调参。该图能直观指导后续的超参数选择。

混淆矩阵

不同类别之间的预测混淆情况及类间易混淆对。鲜艳的 magma colormap 让高值区域突显。

从图中,咱们可以判断,哪些类别被误判最多?有无特定类别(如类 1 或类 3)的混淆容易发生?

混淆是否体现我们设计的局部模式或长程规则相似性?例如类 0 的上升局部模式 与类 2 的下降模式是否在边缘段容易混淆?

是否需要针对某类数据增强或改进特征抽取?

多类别 ROC 曲线

从阈值无关的角度衡量分类器质量,展示各类别的一对多 ROC 曲线和 AUC。

从图中,咱们可以判断,哪个类别最容易被正确区分(AUC 高)?哪个类别较难(AUC 低)?

是否需要使用类别不平衡策略或阈值调优?可根据曲线形状选择更合适的决策阈值或损失加权。

对于实际生产,如果某类误检成本高,可通过 ROC 分析进行阈值移动,使召回与精度的权衡更合理。

t-SNE 特征嵌入

可视化 BiLSTM 汇聚后的高维特征(最后时刻),观察类间分布是否分离。不同颜色+形状的点代表不同类别,便于区分。

从图中,咱们可以判断,CNN+BiLSTM 是否学到可分离的特征空间?若类簇明显分离,说明表示学习有效。

哪些类别簇有重叠?这可提示需要加入注意力机制或改进卷积核设计以加强区分。

是否存在异常点(离群点)?可能是样本噪声或模型未学到该类的关键特征。

总体结论:CNN+BiLSTM 的组合通过卷积先验 + 序列记忆的方式,在需要同时识别局部模式与长程依赖的多数序列任务上具有优势,体现为更好地学习到稳健的可分特征、更高的泛化性能以及更实用的诊断信号。

大家,如果在实际的情形下,进一步的优化方向包括:

  • 引入注意力池化或自注意力模块增强长程交互;

  • 使用空洞卷积扩展感受野、提高多尺度建模能力;

  • 对于类不平衡任务引入 Focal Loss 或类权重;

大家如果有问题,评论区留言~

如果对大家有所帮助,记得点赞或者转发~

相关推荐
锐学AI1 小时前
从零开始学MCP(四)- 认识MCP clients
人工智能·python
QT 小鲜肉1 小时前
【孙子兵法之下篇】010. 孙子兵法·地形篇深度解析与现代应用
人工智能·笔记·读书·孙子兵法
用户377833043491 小时前
( 教学 )Agent 构建 Prompt(提示词)6. 输出修正解析器 OutputFixingParser
人工智能·langchain
浪浪山_大橙子1 小时前
使用Electron+Vue3开发Qwen3 2B桌面应用:从想法到实现的完整指南
前端·人工智能
亚马逊云开发者1 小时前
【Agentic AI for Data系列】Kiro实战:DuckDB vs Spark技术选型全流程
人工智能
QT 小鲜肉1 小时前
【孙子兵法之下篇】010. 孙子兵法·地形篇
人工智能·笔记·读书·孙子兵法
Jay20021111 小时前
【机器学习】30 基于内容的过滤算法
人工智能·算法·机器学习
神州问学1 小时前
一个“神级”外挂——Google NotebookLM
深度学习