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)
核心原因分析
输出激活函数错误
多分类任务中,CrossEntropyLoss要求模型输出为未经过激活的logits,或经过Softmax归一化的概率(各类别概率和为1)。但当前模型使用Sigmoid作为输出激活,导致每个类别输出独立,概率和不为1,完全不符合多分类任务的损失计算逻辑,模型无法学习到有效的分类边界。LSTM输入维度逻辑错误
CNN输出张量为(batch_size, 64, 14, 14),其中第二个14是音频时间步维度,第三个14是频率维度。但代码中将其reshape为(batch_size, 14*14, 64),把时间和频率维度全部拼接成序列,完全破坏了音频的时间序列结构,LSTM无法捕捉时间维度的特征关联,等同于无效输入。数据预处理精度丢失
spec_to_image函数中将归一化后的频谱转为uint8类型(0-255整数),丢失了大量浮点精度信息,频谱中的细微特征被抹平,模型无法从低精度数据中学习有效特征。模型结构不合理
LSTM层数设置为10层,过深的网络极易导致梯度消失,模型参数无法有效更新,难以学习到数据规律。数据集划分不科学
直接按顺序划分训练/测试集,未考虑ESC-50数据集的fold分组设计,可能导致训练集与测试集的类别分布不一致,进一步影响模型泛化能力。
修复方案
替换输出激活函数
移除Sigmoid层,直接让全连接层输出logits(CrossEntropyLoss内部会自动计算LogSoftmax),或替换为nn.Softmax(dim=1)。修正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)修正数据预处理
去掉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)简化LSTM结构
将LSTM层数从10层降至2-3层,避免梯度消失:hidden_layers = 2 # 或3科学划分数据集
使用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]调整训练策略
增加训练轮数至30-50轮,加入学习率调度器(如torch.optim.lr_scheduler.ReduceLROnPlateau),提升模型收敛效果。
内容的提问来源于stack exchange,提问作者asu

