从零搭建音乐识别系统(一)整体功能介绍_程大海的博客-CSDN博客
从零搭建音乐识别系统(二)音频特征提取_程大海的博客-CSDN博客_音乐特征提取
从零搭建音乐识别系统(三)音乐分类模型_程大海的博客-CSDN博客
从零搭建音乐识别系统(四)embedding特征提取模型_程大海的博客-CSDN博客
从零搭建音乐识别系统(五)embedding特征提取模型验证_程大海的博客-CSDN博客
代码地址:https://github.com/xxcheng0708/AudioEmbeddingExtraction
在第二篇中,我们已经从每首歌曲中提取了20个相互之间重合度在50%~97.5%的10秒音乐片段,并将这些片段转换成了大小为[64, 1001]的梅尔频谱矩阵。假如我们现在的训练集中有10000首不同的歌曲,那么我们现在就有20 * 10000共20万个[64, 1001]的训练样本。本篇我们就使用度量学习方法训练模型来提取embedding特征。
关于度量学习方法,这里再次强调一下度量学习的几个核心概念:
1、选定度量学习方法使用的embedding相似度度量方法,通常采用欧氏距离(越小越相似),或者余弦相似度(越大越相似)
2、度量学习模型将每个输入样本转换为具有表征能力的固定大小(如128维)的embedding特征向量
3、经过训练后的度量学习模型提取的embedding向量,使得属于同一类别ID的embedding之间具有更小的空间距离(如欧氏距离),或者具有更大的相似度(如余弦相似度),使得属于不同类别ID的embedding之间具有更大的空间距离,或者具有更小的相似度
4、与传统的分类模型只能识别经过训练的类别不同,我们期望经过训练的度量学习模型能够泛化扩展到未见过的(unseen)其他数据上,以人脸识别模型为例(人脸识别通常也是度量学习模型),我们在开源的人脸识别数据库上(通常包含几十、上百万个人物身份)训练好模型,然后将这个模型用来识别任何我们想要识别的人物身份,这就要求度量学习模型能够很好的泛化到unseen的数据类别上
使用度量学习模型训练音乐embedding提取模型主要包含以下主要步骤:
1、训练数据预处理
由于我们的每条训练数据是保存在npy文件里面的,所以我们需要自定义一个预处理npy数据文件的方法。
npy数据文件读取:
class MusicNpyDataset(Dataset):
def __init__(self, filenames, labels, transform):
self.filenames = filenames
self.labels = labels
self.transform = transform
def __len__(self):
return len(self.filenames)
def __getitem__(self, idx):
image = np.load(self.filenames[idx]).astype(np.float32)
# 分类模型使用[64, 251]大小的特征矩阵进行训练
# image = image[:, ::4]
image = self.transform(image)
return image, self.labels[idx]
数据归一化、数据增强:
在embedding模型训练过程中,我们使用三种数据处理方法:
1、零均值归一化ZeroNormalizeTransform
其中,零均值归一化处理用在数据加载的transform阶段,SpecAugment用在模型forward阶段,MixUp用在loss计算阶段。
class ZeroNormalizeTransform(object):
"""
数据增强,对输入矩阵进行零均值归一化
"""
def __call__(self, img):
img_mean = img.mean()
img = img - img_mean
return img
train_transforms = torchvision.transforms.Compose(
[
torchvision.transforms.ToTensor(),
ZeroNormalizeTransform()
]
)
val_transforms = torchvision.transforms.Compose(
[
torchvision.transforms.ToTensor(),
ZeroNormalizeTransform()
]
)
mixup数据增强方法,参考自mixup官方代码实现:
import numpy as np
import torch
def mixup_data(x, y, alpha=1.0, use_cuda=True):
'''Returns mixed inputs, pairs of targets, and lambda'''
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1
batch_size = x.size()[0]
if use_cuda:
index = torch.randperm(batch_size).cuda()
else:
index = torch.randperm(batch_size)
mixed_x = lam * x + (1 - lam) * x[index, :]
y_a, y_b = y, y[index]
return mixed_x, y_a, y_b, lam
2、训练数据加载
在上一篇音乐分类模型的介绍中,我们已经介绍了相关的数据加载方法,由于度量学习方法与分类方法之间存在的差异,需要对数据加载方法进行一些限制,主要不同在于DataLoader的sampler采样器方面,如果看过之前关于度量学习相关的文章应该会了解到,度量学习相关的损失函数会将样本分为anchor、positive和negative,然后计算anchor和positive,anchor和negative之间的相似度来计算损失,所以我们在进行训练数据加载的样本采样时,要保证采样的batch中对于每个样本都有多个与之对应的positive样本,也就是说要保证采样的每个batch中,对于被采样的每个类别的样本数量都要大于1个,这里我们借助pytorch-metric-learning这个优秀的度量学习开源代码来实现我们的样本采样。
关于pytorch-metric-learning这个开源代码库,我们后续关于度量学习模型中损失函数的调用,评价指标的计算等都来自于这个代码库,强烈推荐,强烈推荐。
from pytorch_metric_learning import samplers
def fetch_data(data_path, ratio, batch_size, train_transforms, val_transforms, num_workers=4, seed=100):
random.seed(seed)
# 使用torchvision解析磁盘上按类别存放好的npy文件
dataset = torchvision.datasets.DatasetFolder(data_path, loader=np.load, extensions=("npy",))
classes = dataset.classes
character = [[] for _ in range(len(classes))]
random.shuffle(dataset.samples)
for x, y in dataset.samples:
character[y].append(x)
# 查看每个类别的总样本数量
for i in range(len(classes)):
print("{}: {}".format(classes[i], len(character[i])))
# 根据ratio比例,划分训练集、验证集、测试集
train_inputs, val_inputs, test_inputs = [], [], []
train_labels, val_labels, test_labels = [], [], []
for i, data in enumerate(character):
num_sample_train = int(len(data) * ratio[0])
num_sample_val = int(len(data) * ratio[1])
num_val_index = num_sample_train + num_sample_val
# 训练集
for x in data[:num_sample_train]:
train_inputs.append(str(x))
train_labels.append(i)
# 验证集
for x in data[num_sample_train:num_val_index]:
val_inputs.append(str(x))
val_labels.append(i)
# 测试集
for x in data[num_val_index:]:
test_inputs.append(str(x))
test_labels.append(i)
train_dataset = MusicNpyDataset(train_inputs, train_labels, train_transforms)
train_dataloader = DataLoader(train_dataset, batch_size=batch_size,
num_workers=num_workers, drop_last=True, shuffle=False,
sampler=samplers.MPerClassSampler(train_labels, m=8, batch_size=batch_size,
length_before_new_iter=len(train_labels)),
pin_memory=True)
val_dataset = MusicNpyDataset(val_inputs, val_labels, val_transforms)
val_dataloader = DataLoader(val_dataset, batch_size=batch_size,
drop_last=False, shuffle=False, num_workers=num_workers,
sampler=samplers.MPerClassSampler(val_labels, m=8, batch_size=batch_size,
length_before_new_iter=len(val_labels)),
pin_memory=True)
test_dataset = MusicNpyDataset(test_inputs, test_labels, val_transforms)
test_dataloader = DataLoader(test_dataset, batch_size=batch_size,
drop_last=False, shuffle=False, num_workers=num_workers,
sampler=samplers.MPerClassSampler(test_labels, m=8, batch_size=batch_size,
length_before_new_iter=len(test_labels)),
pin_memory=True)
return train_dataloader, val_dataloader, test_dataloader, train_dataset, val_dataset, test_dataset, classes
在构建DataLoader时,我们指定了使用 samplers.MPerClassSampler 采样器,采样器的参数m=8表示采样的batch中,每个类别会采样8个样本,也就是说,加入我们使用的batch_size=64,那么采样的一个batch中一共会包含8类样本,每类8个,这样我们在后面计算loss的时候,就满足度量学习损失函数的要求了。
另外强调比较重要的一点,在前面已经说了,度量学习是想让模型在unseen没见过的数据上表现的很好,所以,我们在构建验证集和测试集的时候要独立于训练集,这样验证集和测试集对于训练的模型来说就是unseen的。所以我们需要准备两份数据,一份用于训练(假如存放在data_path_1),另一份用于验证和测试(假如存放在data_path_2)。
train_dataloader, _, _, train_dataset, _, _, train_classes = fetch_data(data_path=data_path_1,
ratio=[1.0, 0.0, 0.0],
batch_size=64,
train_transforms=train_transforms,
val_transforms=val_transforms)
val_dalaloader, test_dataloader, _, val_dataset, test_dataset, _, val_classes = fetch_data(data_path=data_path_2,
ratio=[0.7, 0.3, 0.0],
batch_size=64,
train_transforms=val_transforms,
val_transforms=val_transforms)
3、特征提取网络模型构建
数据处理好了之后,下面就是要定义网络模型,将数据输入到模型中进行训练,我们使用ResNet18作为分类模型,由于原始的ResNet18模型是处理三通道大小为224 x 224的图像数据的,而我们的输入数据是一通道的64 x 1001的矩阵,所以要对ResNet18网络模型进行一些改造,主要有以下三个改造点:
1、把ResNet18的第一层卷积层更换成可以处理一通道输入的卷积层
2、把ResNet18模型最后的平均池化层更换更自适应平局池化层(这一步仅在Pytorch版本过低的时候需要),从而使得网络模型能够处理64 x 1001大小的输入数据
3、修改网络模型的输出层,将输出层的维度修改为我们预期输出的embedding的维度(如128维)
import torch
from torch import nn
from torchlibrosa.augmentation import SpecAugmentation
from torchvision.models.resnet import BasicBlock, ResNet, Bottleneck
class MusicFingerPrint(ResNet):
def __init__(self, out_dimension=128, feature_dimension=64, in_channel=1, model_name="resnet18"):
self.out_dimension = out_dimension
self.feature_dimension = feature_dimension
self.in_channel = in_channel
self.model_name = model_name
if self.model_name == "resnet18":
block = BasicBlock
layers = [2, 2, 2, 2]
elif self.model_name == "resnet34":
block = BasicBlock
layers = [3, 4, 6, 3]
elif self.model_name == "resnet50":
block = Bottleneck
layers = [3, 4, 6, 3]
super(MusicFingerPrint, self).__init__(block, layers)
# SpecAugment数据增强
self.spec_aug = SpecAugmentation(time_drop_width=64, time_stripes_num=2,
freq_drop_width=8, freq_stripes_num=2)
if self.model_name == "resnet18":
if self.in_channel != 1:
self.conv1 = nn.Conv2d(self.in_channel, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
self.fc = nn.Linear(in_features=512, out_features=self.out_dimension)
elif self.model_name == "resnet34":
if self.in_channel != 1:
self.conv1 = nn.Conv2d(self.in_channel, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
self.fc = nn.Linear(in_features=512, out_features=self.out_dimension)
elif self.model_name == "resnet50":
if self.in_channel != 1:
self.conv1 = nn.Conv2d(self.in_channel, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
self.fc = nn.Linear(in_features=2048, out_features=self.out_dimension)
nn.init.kaiming_normal_(self.conv1.weight, mode="fan_in", nonlinearity="relu")
nn.init.kaiming_normal_(self.fc.weight, mode="fan_in", nonlinearity="relu")
def forward(self, x):
batch_size = x.size(0)
# SpecAugment仅训练时使用
if self.training:
x = x.transpose(2, 3)
x = self.spec_aug(x)
x = x.transpose(2, 3)
out = self._forward_impl(x)
return out
在模型构建的过程中,同时初始化了SpecAugment数据增强方法,在forward推理方法中,SpecAugment数据增强只在模型训练阶段使用。
4、损失函数
我们本次使用pair-based的损失函数CircleLoss,关于pair-based损失函数的相关介绍请参考之前的文章介绍。
我们本次使用的损失函数Circle Loss以及相关的难样本挖掘方法均来自pytorch-metric-learning代码库。
from pytorch_metric_learning import distances, miners, losses
# 输出的embedding特征维度
out_dimension = 128
# embedding特征之间相似度的评价方法,余弦相似度
distance = distances.CosineSimilarity()
# 难样本挖掘方法,加速模型收敛
circle_loss_miner = miners.MultiSimilarityMiner()
circle_loss_func = losses.CircleLoss(m=0.25, gamma=400, distance=distance)
以上是关于损失函数的定义,下面看一下损失函数的调用:
X = X.cuda()
y = y.cuda()
if mixup_enable:
X, y_a, y_b, lam = mixup_data(X, y, 1.0, True)
X, y_a, y_b = map(torch.autograd.Variable, (X, y_a, y_b))
X = X.cuda()
y_a = y_a.cuda()
y_b = y_b.cuda()
y_pred = model(X)
loss = lam * circle_loss_func(y_pred, y_a) + (1 - lam) * circle_loss_func(y_pred, y_b)
5、评价指标
关于度量学习的评价指标,具体可以参考pytorch-metric-learning的accuracy_calculation部分,以及论文《A Metric Learning Reality Check》。
论文《A Metric Learning Reality Check》提出了mean_average_precision_at_r评价指标,这个评价指标有点类似于top k准确率,但是不同点在于,他把top k的准确率按照预测结果的先后顺序做了加权计算,更能体现模型的具体泛化性能,所以我们也使用这个评价指标。关于mean_average_precision_at_r指标的解释可以看一下这个文章。
6、测试集性能验证
在训练数据加载那部分,我们已经将训练集和验证集、测试集进行区分开了,在这里我们在验证集上计算一下mean_average_precision_at_r评价指标。
from pytorch_metric_learning import testers
from pytorch_metric_learning.utils.accuracy_calculator import AccuracyCalculator
def get_all_embeddings(dataset, model):
tester = testers.GlobalEmbeddingSpaceTester()
return tester.get_all_embeddings(dataset, model)
accuracy_calculator = AccuracyCalculator(
include=(
"precision_at_1",
"r_precision",
"mean_average_precision_at_r",
),
k="max_bin_count"
)
def test(val_set, test_set, model, accuracy_calculator):
"""
在val_set构造的embedding向量库中检索与test_set构造的embedding的相似向量
:param val_set:
:param test_set:
:param model:
:param accuracy_calculator:
:return:
"""
model.eval()
val_embeddings, val_labels = get_all_embeddings(val_set, model)
test_embeddings, test_labels = get_all_embeddings(test_set, model)
val_labels = val_labels.squeeze(1)
test_labels = test_labels.squeeze(1)
# 将计算放到cpu和内存里面来计算,因为我的验证集和测试集一共大概40个样本,在22G的单张显卡上放不下
val_embeddings = val_embeddings.cpu()
test_embeddings = test_embeddings.cpu()
val_labels = val_labels.cpu()
test_labels = test_labels.cpu()
accuracies = accuracy_calculator.get_accuracy(
test_embeddings,
val_embeddings,
test_labels,
val_labels,
False
)
return accuracies