You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CRNN模型训练准确率过低求助(附代码与准确率截图)

基于ESC-50训练CRNN模型准确率极低的问题分析

问题描述

在基于ESC-50数据集训练CRNN模型时,训练准确率始终维持在2%左右(与随机猜测水平一致),无法有效提升。以下是完整代码实现及训练情况,需分析准确率过低的核心原因并给出修复方案。

完整代码实现

依赖导入与设备配置

# !curl -L -s -o '/content/ESC-50-master-16k.tar' 'https://drive.google.com/uc?id=1hFt-qarD_Ihjb3jW7pd3tje2pcY5tfuJ&confirm=t'
# !tar -xvf "ESC-50-master-16k.tar"

from torch.utils.data import Dataset, TensorDataset, DataLoader
import torch
import torch.nn as nn
import torch.nn.functional as F
import librosa

import numpy as np
import pandas as pd

from glob import glob

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

数据预处理与数据集定义

metadata = pd.read_csv("./ESC-50-master-16k/meta/esc50.csv")
wav_list = sorted(glob("./ESC-50-master-16k/resample/*.wav"))

def spec_to_image(spec, eps=1e-6):
    mean = spec.mean()
    std = spec.std()
    spec_norm = (spec - mean) / (std + eps)
    spec_min, spec_max = spec_norm.min(), spec_norm.max()
    spec_scaled = 255 * (spec_norm - spec_min) / (spec_max - spec_min)
    spec_scaled = spec_scaled.astype(np.uint8)
    return spec_scaled

class esc50dataset(Dataset):
    def __init__(self, wavlist, metadata):
        self.labels = np.array(metadata.target).astype(int)
        self.audio = []
        for f in wavlist:
            wav, sr = librosa.load(f, sr=None)
            spec=librosa.feature.melspectrogram(wav, sr=sr, n_fft=1024, hop_length=640, n_mels=126) # [126,126]
            spec_db=librosa.power_to_db(spec,top_db=80)
            spec_image = np.expand_dims(spec_to_image(spec_db), axis=0)
            self.audio.append(spec_image.tolist())
        self.audio = np.array(self.audio)
    def __len__(self):
        return len(self.audio)
    def __getitem__(self, idx):
        return self.audio[idx], self.labels[idx]

dataset = esc50dataset(wav_list, metadata)
features = dataset[:][0]
labels = dataset[:][1]

数据集划分

train_size = 0.7
val_size = 0.5

# 制作训练集
split_id = int(len(features) * train_size) # 2000 * 0.7 = 1400
train_features, remain_features = features[:split_id], features[split_id:] # train_features = 1400, remain_features = 600
train_labels, remain_labels = labels[:split_id], labels[split_id:] # train_labels = 1400, remain_labels = 600

# 制作验证集和测试集
split_val_id = int(len(remain_features) * val_size) # 600 * 0.5 = 300
val_features, test_features = remain_features[:split_val_id], remain_features[split_val_id:] # val_features = 300, test_features = 300
val_labels, test_labels = remain_labels[:split_val_id], remain_labels[split_val_id:] # val_labels = 300, test_labels = 300

# 定义批量大小
batch_size = 32

# 创建张量数据集
train_set = TensorDataset(torch.from_numpy(train_features), torch.from_numpy(train_labels))
valid_set = TensorDataset(torch.from_numpy(val_features), torch.from_numpy(val_labels))
test_set = TensorDataset(torch.from_numpy(test_features), torch.from_numpy(test_labels))

# 创建数据加载器
train_loader = DataLoader(train_set, shuffle=True, batch_size=batch_size) # 1400 / 32
val_loader = DataLoader(valid_set, shuffle=True, batch_size=batch_size) # 300 / 32
test_loader = DataLoader(test_set, shuffle=True, batch_size=batch_size) # 300 / 32

# 模型超参数
in_channel = features[0].shape[0] # in_channel = 1
max_pool_kernel = 3

hidden_size = 256
hidden_layers = 10
output_classes = labels.max() + 1 # output_classes = 50
dropout = 0.25
learning_rate = 0.001
num_epochs = 10

CRNN模型定义

class CRNN(nn.Module):
    def __init__(self, in_channel, hidden_size, hidden_layers, output_classes, dropout):
        super(CRNN, self).__init__()
        self.hidden_layers = hidden_layers
        self.hidden_size = hidden_size

        self.CNN_layer1 = nn.Sequential(
            nn.Conv2d(in_channels=in_channel, out_channels=32, kernel_size=5, stride=1, padding=2),
            nn.BatchNorm2d(num_features=32),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=max_pool_kernel),  # 32, 42, 42
        )
        self.CNN_layer2 = nn.Sequential(
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
            nn.BatchNorm2d(num_features=64),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=max_pool_kernel),  # 64, 14, 14
        )
        self.lstm = nn.LSTM(input_size=64, hidden_size=hidden_size, num_layers=hidden_layers, batch_first=True)        
        self.dropout = nn.Dropout(dropout)
        self.fc = nn.Linear(hidden_size, output_classes) 
        self.sigmoid = nn.Sigmoid()
    
    def forward(self, x):
        x = x.float()
        x = self.CNN_layer1(x)  # 1, 126, 126 -> 32, 42, 42
        x = self.CNN_layer2(x)  # 32, 42, 42 -> 64, 14, 14
        x = x.reshape(x.size(0), -1, 64)  # 64, 14, 14 -> 12544 , x.size(0) = Batchsize = 32
        h0 = torch.zeros(self.hidden_layers, x.size(0), self.hidden_size).to(device) # torch.size([10, 32(batch_size), 512])
        c0 = torch.zeros(self.hidden_layers, x.size(0), self.hidden_size).to(device) # torch.size([10, 32(batch_size), 512])
        x, _ = self.lstm(x, (h0, c0))
        x = self.dropout(x)
        x = self.fc(x[:,-1,:])
        x = self.sigmoid(x)
        return x

训练与验证流程

model = CRNN(in_channel, hidden_size, hidden_layers, output_classes, dropout)

criterion = nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

from tqdm import tqdm
tqdm.pandas()

tr_loss = []
tr_acc = []
v_loss = []
v_acc = []

best_valid_loss = torch.inf
best_epoch = 0
model = model.to(device)
epochloop = tqdm(range(num_epochs), position=0, desc='Training', leave=True)

for epoch in epochloop:
    model.train()
    train_loss = 0
    train_acc = 0

    epochloop.set_postfix_str(f'Training epoch {epoch}/{len(epochloop)}') # visualize
    for idx, (audio, label) in enumerate(train_loader):
        audio, label = audio.to(device), label.to(device)
        out = model(audio)
        _, pred = torch.max(out.data, 1)

        # 计算准确率
        train_acc += (pred==label).sum()
        
        # 计算损失
        optimizer.zero_grad()
        loss = criterion(out, label)
        train_loss += loss.item()
        loss.backward()
        optimizer.step()

    # 验证流程
    model.eval()
    val_loss = 0
    val_acc = 0

    with torch.no_grad():
        for idx, (audio, label) in enumerate(val_loader):
            epochloop.set_postfix_str(f'Validation batch {idx}/{len(val_loader)}')
            audio, label = audio.to(device), label.to(device)

            # 前向传播
            out = model(audio)
            _, pred = torch.max(out.data, 1)
        
            # 计算准确率
            val_acc += (pred==label).sum()
        
            # 计算损失
            loss = criterion(out, label)
            val_loss += loss.item()

    model.train()

    # 验证损失下降时保存模型
    if val_loss / len(valid_set) <= best_valid_loss:
        best_valid_loss = val_loss / len(val_loader)
        best_epoch = epoch
        torch.save(model.state_dict(), "LSTM_epoch_{}.pth".format(epoch))

    # 打印每轮损失与准确率
    print(f'Epoch {epoch+1}/{num_epochs} | Train Loss: {train_loss / len(train_loader):.3f} Train Acc: {train_acc / len(train_set) * 100}% | Val Loss: {val_loss / len(val_loader):.3f} Val Acc: {val_acc / len(valid_set) * 100}%')
    tr_loss.append(train_loss / len(train_loader))
    tr_acc.append(train_acc.cpu().numpy() / len(train_set) * 100)
    v_loss.append(val_loss / len(val_loader))
    v_acc.append(val_acc.cpu().numpy() / len(valid_set) * 100)

核心原因分析

  1. 输出激活函数错误
    多分类任务中,CrossEntropyLoss要求模型输出为未经过激活的logits,或经过Softmax归一化的概率(各类别概率和为1)。但当前模型使用Sigmoid作为输出激活,导致每个类别输出独立,概率和不为1,完全不符合多分类任务的损失计算逻辑,模型无法学习到有效的分类边界。

  2. LSTM输入维度逻辑错误
    CNN输出张量为(batch_size, 64, 14, 14),其中第二个14是音频时间步维度,第三个14是频率维度。但代码中将其reshape为(batch_size, 14*14, 64),把时间和频率维度全部拼接成序列,完全破坏了音频的时间序列结构,LSTM无法捕捉时间维度的特征关联,等同于无效输入。

  3. 数据预处理精度丢失
    spec_to_image函数中将归一化后的频谱转为uint8类型(0-255整数),丢失了大量浮点精度信息,频谱中的细微特征被抹平,模型无法从低精度数据中学习有效特征。

  4. 模型结构不合理
    LSTM层数设置为10层,过深的网络极易导致梯度消失,模型参数无法有效更新,难以学习到数据规律。

  5. 数据集划分不科学
    直接按顺序划分训练/测试集,未考虑ESC-50数据集的fold分组设计,可能导致训练集与测试集的类别分布不一致,进一步影响模型泛化能力。

修复方案

  1. 替换输出激活函数
    移除Sigmoid层,直接让全连接层输出logits(CrossEntropyLoss内部会自动计算LogSoftmax),或替换为nn.Softmax(dim=1)。

  2. 修正LSTM输入维度
    将CNN输出的时间维度作为LSTM的序列轴,调整reshape逻辑:

    # 原reshape代码
    # x = x.reshape(x.size(0), -1, 64)
    # 修改为:将时间维度(第3维)作为序列长度,通道+频率维度作为特征
    x = x.permute(0, 3, 1, 2)  # (batch_size, time_steps, channels, freq_bins)
    x = x.flatten(2)  # (batch_size, 14, 64*14)
    
  3. 修正数据预处理
    去掉spec_to_image中的类型转换,保留浮点型归一化结果:

    def spec_to_image(spec, eps=1e-6):
        mean = spec.mean()
        std = spec.std()
        spec_norm = (spec - mean) / (std + eps)
        spec_min, spec_max = spec_norm.min(), spec_norm.max()
        spec_scaled = (spec_norm - spec_min) / (spec_max - spec_min)  # 归一化到0-1
        return spec_scaled.astype(np.float32)
    
  4. 简化LSTM结构
    将LSTM层数从10层降至2-3层,避免梯度消失:

    hidden_layers = 2  # 或3
    
  5. 科学划分数据集
    使用ESC-50自带的fold列划分,确保分布一致:

    # 示例:用fold 1-4作为训练集,fold5作为测试集
    train_mask = metadata['fold'].isin([1,2,3,4])
    test_mask = metadata['fold'] == 5
    
    train_features = features[train_mask]
    train_labels = labels[train_mask]
    test_features = features[test_mask]
    test_labels = labels[test_mask]
    
  6. 调整训练策略
    增加训练轮数至30-50轮,加入学习率调度器(如torch.optim.lr_scheduler.ReduceLROnPlateau),提升模型收敛效果。

内容的提问来源于stack exchange,提问作者asu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 16:20:26