基于 CNN14 分辨 20 种食物咀嚼音
本项目开发了一个基于CNN14深度学习模型的食物声音分类系统,能够识别20种不同食物的咀嚼声音。系统采用梅尔频谱图作为音频特征表示,通过数据增强技术(时域拉伸和频谱掩蔽)提升模型泛化能力。CNN14模型包含6个卷积块和全局平均池化层,专为音频信号处理优化。
基于 CNN14 分辨 20 种食物咀嚼音
代码详见:https://github.com/xiaozhou-alt/Food_Recognition
文章目录
一、项目介绍
本项目使用深度学习 CNN14 模型对食物相关的声音进行分类,能够识别20种不同的食物类别。主要功能包括:
- 音频预处理和特征提取(梅尔频谱图)
- 数据增强(时域拉伸、频谱增强)
- 基于 CNN14 架构的深度学习模型训练
- 模型验证和性能评估
本次项目源于:[AI入门系列]美食侦探:食物声音识别学习赛 (aliyun.com)
数据集下载:
二、文件夹结构
Food_Recognition/
├── data/ # 数据集文件夹
├── train/ # 训练集音频文件
├── log/ # 训练记录文档
├── output/
├── pic/ # 输出图片文件夹
├── model/ # 训练模型文件夹
├── samples/ # 测试样例输出文件夹
├── sample_0_spectrogram.png # 0号测试样例音频图
├── sample_0_wings_pred_wings.wav # 0号测试样例原音频(文件名中包含原始和预测标签)
...
├── class_mapping.json # 类别映射文件
├── classification_report.txt # 分类结果报告
├── train.py # 主训练
├── predict.py # 预测
├── data.ipynb # 数据分析jupyter notebook
├── requirements.txt
└── README.md
三、数据集介绍
数据集包含20类食物相关的声音,总体 7000 个WAV格式音频文件,采样率16kHz,最长时长为22秒,平均时长为4.53秒。音频时长、音频个数、分类的分布关系如下图所示(更多信息可查看 data.ipynb 或自行研究数据集):


数据集样例:
基于 CNN14 分辨 20 种食物咀嚼音-数据集展示
四、CNN14 模型介绍
CNN14 是由 Qiuqiang Kong 等人在论文 PANNs: Large-Scale Pretrained Audio Neural Networks for Audio Pattern Recognition 中提出的核心模型之一。它是 PANNs (Pretrained Audio Neural Networks) 项目的一部分,该项目旨在提供大规模预训练的音频神经网络模型,用于各种音频模式识别任务,如 音频标记 (Audio Tagging)、声音事件检测 (Sound Event Detection, SED) 和 环境声音分类 等。
CNN14 是一个相对深层但结构清晰的卷积神经网络,其名称“14”来源于其包含 14 个具有可学习参数的层(主要是卷积层),其核心结构如下:
-
输入: 模型接收 log-Mel 频谱图 (Log-Mel Spectrogram) 作为输入。这是一个二维表示,其中:
- 一个维度代表 时间(帧数)。
- 另一个维度代表 频率(Mel 频带数)。常见的输入尺寸是 T x F (例如,1024 frames x 64 mel bands)。
-
卷积块: 模型主要由 6 个卷积块 (Convolutional Blocks) 堆叠而成。每个卷积块通常包含以下层(按顺序):
- 2D 卷积层 (Conv2D): 这是核心特征提取层。卷积核大小在前几个块通常使用 5x5,后续块可能使用 3x3。关键点:卷积核在时间和频率两个维度上同时操作。
- 批归一化层 (Batch Normalization, BN):加速训练,提高模型稳定性。
- ReLU 激活函数: 引入非线性。
- 2D 最大池化层 (MaxPool2D):用于空间下采样,降低特征图的空间维度(时间和频率维度)。
池化策略是其与常规图像 CNN 的一个显著区别点(见下文)。
-
卷积块细节(典型配置):
- Block1: Conv2D (in=1, out=64, kernel=5x5, stride=1x1) -> BN -> ReLU -> MaxPool2D (pool_size=2x2, stride=2x2)
- Block2: Conv2D (in=64, out=128, kernel=5x5, stride=1x1) -> BN -> ReLU -> MaxPool2D (2x2, 2x2)
- Block3: Conv2D (in=128, out=256, kernel=5x5, stride=1x1) -> BN -> ReLU -> MaxPool2D (2x2, 2x2)
- Block4: Conv2D (in=256, out=512, kernel=5x5, stride=1x1) -> BN -> ReLU -> MaxPool2D (2x2, 2x2)
- Block5: Conv2D (in=512, out=1024, kernel=5x5, stride=1x1) -> BN -> ReLU -> MaxPool2D (2x2, 2x2) 通常只对时间轴进行池化,频率轴可能已降至1
- Block6: Conv2D (in=1024, out=2048, kernel=5x5, stride=1x1) -> BN -> ReLU -> MaxPool2D (2x2, 2x2) 通常只对时间轴进行池化

4. 全局池化: 在最后一个卷积块之后,使用 全局平均池化 (Global Average Pooling, GAP) 层。该层将最后一个卷积层输出的特征图(尺寸为 T’ x F’ x 2048)在时间和频率维度上同时取平均,得到一个 2048 维的特征向量。这一步极大地减少了参数数量,并强制模型学习具有全局代表性的特征。
优势和特点:
- 针对音频优化: 虽然结构上借鉴了图像 CNN 的成功经验(如 VGG),但其输入(Mel 频谱图)和池化策略的设计(后期主要在时间轴池化)明确考虑了 音频信号时频表示的特性(时间动态性关键,频率轴在高层可压缩)。
- 强大的特征提取器: 作为 PANNs 的核心模型,CNN14 在 大规模音频数据集 (AudioSet) 上进行了预训练。这使得其学习到的特征(特别是 GAP 层输出的 2048 维向量或其之前的卷积特征图)具有极强的泛化能力,可以有效地迁移到各种下游音频任务(如声音事件检测、音乐分类、语音情感识别、异常声音检测等),通常只需微调或添加简单的任务头就能取得优异效果。
- 在音频任务上性能优越: CNN14 及其变体在多个音频基准数据集(如 AudioSet 音频标记、DCASE 声音事件检测挑战赛)上都取得了当时 state-of-the-art 或极具竞争力的结果,证明了其有效性。
五、项目实现
1. 基础配置
- 音频采样率: 16 k H z 16kHz 16kHz(标准语音采样率)
- 音频时长: 7 s 7s 7s(统一处理长度)
- 梅尔频谱参数: 128 128 128 个梅尔滤波器组
- 分类类别: 20 20 20 种不同的饮食声音
DATA_PATH = './data/train'
SAMPLE_RATE = 16000 # 音频采样率
DURATION = 7000 # 音频时长(毫秒)
N_MELS = 128 # 梅尔频带数
N_CLASSES = 20 # 分类类别数
2. 数据预处理
实现核心的数据处理功能,包括类别映射字典的创建(class_to_idx 和 idx_to_class)和两个关键的数据增强类。
- ComplexTimeStretch 类在复数域进行时域拉伸,通过短时傅里叶变换将音频转为频谱,应用随机速度变化(0.8-1.2倍),再逆变换回波形,保持音高不变的同时增加时间维度多样性。
- SpecAugment 类实现频谱增强,包含频率 掩蔽( 15 15 15 个频带)、时间 掩蔽( 35 35 35 个时间步)和 随机增益 ( − 6 -6 −6 到 6 6 6 分贝)三种增强技术,每种以 50 % 50\% 50% 概率应用。
- AudioDataset 自定义数据集类处理音频加载全流程:从磁盘读取文件、重采样 至 16 k H z 16kHz 16kHz、统一长度 7 s 7s 7s、应用时域拉伸、转换为 梅尔频谱图、转分贝单位、应用频谱增强,最终返回增强后的频谱图和标签,可选返回原始波形用于后续分析。
# 获取类别映射
class_names = sorted(os.listdir(DATA_PATH))
class_to_idx = {cls_name: i for i, cls_name in enumerate(class_names)}
idx_to_class = {i: cls_name for cls_name, i in class_to_idx.items()}
# 时域拉伸类(复数域操作)
class ComplexTimeStretch:
def __init__(self, sample_rate, p=0.5):
...
def __call__(self, waveform):
if torch.rand(1).item() < self.p:
# 转换为复数频谱
...
# 随机时域拉伸
...
# 转回波形
...
return waveform
# 频谱增强类
class SpecAugment:
def __init__(self, freq_mask_param=15, time_mask_param=35, p=0.5):
...
def __call__(self, spec):
# 确保输入是3D [channels, freq, time]
...
# 随机频率掩蔽
...
# 随机时间掩蔽
...
# 随机增益 (在分贝域操作)
...
return spec.squeeze(0) # 移除通道维度,返回2D
# 自定义数据集类
class AudioDataset(Dataset):
def __init__(self, data_path, transform=None, time_stretch=None, return_waveform=False):
...
# 收集所有音频文件路径和标签
for class_name in class_names:
...
def __len__(self):
return len(self.file_paths)
def __getitem__(self, idx):
...
# 加载音频
# 重采样
# 统一音频长度
# 应用时域拉伸(如果需要)
# 转换为梅尔频谱图
# 转换为分贝单位
# 数据增强(在频谱图上)
# 添加通道维度 (1, n_mels, time)
# 返回原始波形用于播放
...
return mel_specgram, label
获取的类别映射的 json 文件格式如下:
class_mapping.json:
{“class_to_idx”: {“aloe”: 0, “burger”: 1, “cabbage”: 2, “candied_fruits”: 3, “carrots”: 4, “chips”: 5, “chocolate”: 6, “drinks”: 7, “fries”: 8, “grapes”: 9, “gummies”: 10, “ice-cream”: 11, “jelly”: 12, “noodles”: 13, “pickles”: 14, “pizza”: 15, “ribs”: 16, “salmon”: 17, “soup”: 18, “wings”: 19}, “idx_to_class”: {“0”: “aloe”, “1”: “burger”, “2”: “cabbage”, “3”: “candied_fruits”, “4”: “carrots”, “5”: “chips”, “6”: “chocolate”, “7”: “drinks”, “8”: “fries”, “9”: “grapes”, “10”: “gummies”, “11”: “ice-cream”, “12”: “jelly”, “13”: “noodles”, “14”: “pickles”, “15”: “pizza”, “16”: “ribs”, “17”: “salmon”, “18”: “soup”, “19”: “wings”}}
3. 数据集准备
这里创建数据增强对象实例:时域拉伸 (time_stretch)和 频谱增强 (spec_augment),各增强操作应用概率设为 50 % 50\% 50%。完整数据集通过 AudioDataset 加载,启用所有增强并设置返回原始波形。数据集按 8 : 2 8:2 8:2 比例随机分割为训练集和验证集,为确保验证集评估的客观性,显式禁用验证集的所有数据增强( transform 和 time_stretch 设为 None )。两个 DataLoader 分别处理训练集和验证集,采用 64 的批量大小,训练集启用 洗牌 (shuffle),两者都使用 4 4 4 个工作进程 并行加载 和 锁页内存 (pin_memory)加速 GPU 数据传输。
# 创建数据增强对象
time_stretch = ComplexTimeStretch(SAMPLE_RATE, p=0.5)
spec_augment = SpecAugment(freq_mask_param=15, time_mask_param=35, p=0.5)
# 加载完整数据集
full_dataset = AudioDataset(DATA_PATH, transform=spec_augment, time_stretch=time_stretch, return_waveform=True)
# 划分训练集和验证集 (80%训练, 20%验证)
train_size = int(0.8 * len(full_dataset))
val_size = len(full_dataset) - train_size
train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size])
# 验证集不需要数据增强
val_dataset.dataset.transform = None
val_dataset.dataset.time_stretch = None
# 数据加载器
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=4, pin_memory=True)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False, num_workers=4, pin_memory=True)
4. CNN14 模型架构定义
之前介绍模型架构时已经有详细说明,此处简单说明项目中使用的模型架构定义,不再过多赘述
网络包含 5 5 5 个卷积块,每个块由两个 3 × 3 3×3 3×3 卷积层(保持空间尺寸)、批归一化、ReLU激活和 2 × 2 2×2 2×2 最大池化(下采样)组成。通道数逐块倍增(64→128→256→512→1024),第五块末尾使用自适应平均池化将特征图压缩为 1 × 1 1×1 1×1。分类头由两个全连接层构成,中间加入 50 % 50\% 50% 的 Dropout 防止过拟合。
前向传播过程简洁:输入频谱图依次通过卷积块,展平特征后通过分类头输出类别预测。这种设计平衡了特征提取能力和参数效率。
class CNN14(nn.Module):
def __init__(self, num_classes, in_channels=1):
super(CNN14, self).__init__()
self.cnn = nn.Sequential(
# Block 1
nn.Conv2d(in_channels, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2),
# ...类似结构重复至Block 5...
self.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(1024, 512),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(512, num_classes)
)
def forward(self, x):
...
5. 模型初始化与迁移学习
迁移学习 通过加载预训练的 CNN14 权重实现:首先加载 预训练字典,过滤出卷积层权重(排除全连接层),移除原始权重键中的model.前缀以匹配当前模型定义。使用update()方法将预训练权重合并到当前模型字典,最后以 非严格模式 (allow_unmatched_keys)加载,使分类头权重随机初始化。这种策略充分利用预训练特征提取能力,同时适应新任务的类别数量。
# 初始化模型
model = CNN14(num_classes=N_CLASSES).to(device)
# 加载预训练权重
pretrained_dict = torch.load('/kaggle/working/cnn14.pth')
model_dict = model.state_dict()
# 1. 提取模型参数并重命名
pretrained_dict = {k.replace('model.', ''): v for k, v in pretrained_dict.items()
if k.startswith('model') and 'fc' not in k}
# 2. 更新当前模型参数
model_dict.update(pretrained_dict)
# 3. 加载更新后的参数(允许部分不匹配)
model.load_state_dict(model_dict, strict=False)
6. 损失函数与优化器
训练使用 交叉熵 损失函数,优化器采用 AdamW(学习率 0.0001 0.0001 0.0001,权重衰减 1 e − 5 1e-5 1e−5),其优势在于更有效的 正则化。
学习率调度器基于验证准确率调整:当准确率连续 3个epoch 未提升时,学习率减半(factor=0.5),监控模式设为最大化验证准确率(mode=‘max’)。
# 损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=0.0001, weight_decay=1e-5)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='max', factor=0.5, patience=3, verbose=True
)
7. 开始训练!
训练循环包含 train_epoch 和 validate 函数。
训练阶段 执行标准流程:前向传播、损失计算、反向传播、参数更新,同时计算批次准确率;验证阶段 禁用梯度计算,仅执行前向传播和预测。主循环设置最大 150 150 150 轮次,但实现 早停机制:当验证准确率连续 15 15 15 轮未提升时提前终止训练。每轮结束后更新学习率调度器,记录各项指标,并仅在验证准确率提升时保存模型。这种设计平衡了训练充分性和计算效率。
def train_epoch(model, loader, optimizer, criterion):
# 训练逻辑:前向传播、损失计算、反向传播、参数更新
# 返回epoch损失和准确率
def validate(model, loader, criterion, return_predictions=False):
# 验证逻辑:无梯度计算的前向传播
# 可返回预测结果用于后续分析
# 训练循环
NUM_EPOCHS = 150
best_val_acc = 0.0
patience = 15 # 早停等待周期
for epoch in range(NUM_EPOCHS):
# 训练阶段
train_loss, train_acc = train_epoch(model, train_loader, optimizer, criterion)
# 验证阶段
val_loss, val_acc = validate(model, val_loader, criterion)
# 更新学习率
scheduler.step(val_acc)
# 记录历史
history['train_loss'].append(train_loss)
history['train_acc'].append(train_acc)
# ...其他记录...
# 早停机制
if val_acc > best_val_acc:
# 保存最佳模型
else:
no_improve_epochs += 1
# 检查是否触发早停
训练过程的输出示例如下:
731.9s 27 ✅ 预训练权重加载成功(忽略全连接层)
736.4s 28 /usr/local/lib/python3.11/dist-packages/torch/optim/lr_scheduler.py:62: UserWarning: The verbose parameter is deprecated. Please use get_last_lr() to access the learning rate.
736.4s 29 warnings.warn(
736.4s 30 ✅ 类别映射已保存
736.4s 31
736.4s 32 Epoch 1/150
796.4s 33 Training: 0%| | 0/88 [00:00<?, ?it/s]
…
811.6s 35 Train Loss: 2.4702 | Train Acc: 0.2521
811.6s 36 Val Loss: 3.1489 | Val Acc: 0.1707
811.6s 37 Saved new best model with val acc: 0.1707
…
3211.8s 305 Epoch 40/150
3261.3s 306 Training: 0%| | 0/88 [00:00<?, ?it/s]
Training: 1%| | 1/88 [00:02<03:47, 2.61s/it]
…
Training: 100%|██████████| 88/88 [00:49<00:00, 1.78it/s]
3273.0s 307 Validating: 0%| | 0/22 [00:00<?, ?it/s]
…
Validating: 100%|██████████| 22/22 [00:11<00:00, 1.90it/s]
3273.0s 308 Train Loss: 0.0030 | Train Acc: 1.0000
3273.0s 309 Val Loss: 0.1184 | Val Acc: 0.9629
3273.0s 310 No improvement for 15/15 epochs
3273.0s 311 Early stopping triggered after 40 epochs
3273.5s 312 Best Validation Accuracy: 0.9664
六、结果展示
训练过程中的 损失记录 和 准确率指标 变化如下图所示:
模型在由数据集划分出的验证集上得到的 混淆矩阵 如下所示:

验证集上的分类报告如下:
precision recall f1-score support aloe 0.91 0.97 0.94 80 burger 0.97 0.97 0.97 69 cabbage 0.98 0.96 0.97 68 candied_fruits 1.00 0.97 0.99 103 carrots 0.99 0.99 0.99 96 chips 0.98 0.99 0.98 85 chocolate 0.91 0.97 0.94 32 drinks 0.97 0.97 0.97 40 fries 0.96 0.98 0.97 81 grapes 0.99 0.99 0.99 70 gummies 0.97 0.98 0.97 87 ice-cream 0.99 0.94 0.96 99 jelly 0.91 0.98 0.95 44 noodles 0.96 0.94 0.95 53 pickles 0.98 0.96 0.97 111 pizza 0.99 0.99 0.99 68 ribs 0.89 0.94 0.92 54 salmon 0.95 0.93 0.94 57 soup 0.95 0.92 0.93 38 wings 0.98 0.95 0.97 65 accuracy 0.97 1400 macro avg 0.96 0.96 0.96 1400 weighted avg 0.97 0.97 0.97 1400
随机选取五个验证集中样本进行测试,展示样本的原音频和频谱图(此处仅展示 Sample 1):ps:下面的音频播放不是真的(C站不支持音频导入,想听的同学可以查看我在 Kaggle 上的 Notebook 训练 log)

战绩可查 ∠( ᐛ 」∠)_:

如果你喜欢我的文章,不妨给小周一个免费的点赞和关注吧!
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)