来源:http://hntea.xyz/ros%E5%AE%9E%E6%88%98/%E6%95%B0%E5%AD%97%E8%AF%AD%E9%9F%B3%E5%A4%84%E7%90%86/MFCC-%E7%89%B9%E5%BE%81%E6%8F%90%E5%8F%96/
MFCC 特征提取
概念
MFCC特征是一种基于内耳频率分析的人类声音感知模型,MFCC 集提供了具有感知意义的,平滑的语音频谱随时间的估计。人类内耳结构工作原理:机械震动在耳蜗的入口产生驻波,引起基底膜以与输入声波频率相称的频率协调在此频率上的最大幅度震动。
基底膜的运动机制:
- 在细胞膜不同的地方有一组频率响应(基底膜排有30000多个内毛细胞)
- 基底膜个被视为非均匀线性滤波器组
- 滤波器组中的单个滤波器大体是常数Q(Q是中心频率和滤波器带宽的比值)
- 基底膜的不同区域对输入信号的不同频率组成成分做出最大的响应,我们将响应最大值的那些频率称为特征频率
既然通过相关滤波器组来模仿人类听觉器官的工作原理,那么如何设计滤波器组和提取特征尤为重要。对于MFCC特征提取的基本思想:基于滤波器组的频率分析,滤波器组的带宽间隔约为临界子带的间隔。对于4KHz的带宽,大约使用20个滤波器。
其中临界子带指的是什么呢?因为基底膜可以看作是滤波器组,所以每个滤波器都会有相应的滤波范围,这个范围便称为临界子带,其实这个有针对的计算公式。不过为了统一,便创建了一个新的频率单元,称为Bark。Bark尺度相当于临界子带的索引,每个索引代表不同的频率范围。
算法步骤
针对MFCC特征提取,其算法步骤原理都是差不多的,一般经过以下步骤来实现:
- 将音频信号分帧加窗
- 利用傅立叶变换计算频谱
- 将频谱按照 Bark 的划分方式划分为 N 个单元,对这 N 个单元使用滤波器组分别计算每个滤波器对应的 Bark 单元中的能量
- 将 3 中获取的每个能量转换为对应的对数能量
- 对 4 中的结果作离散余弦变换(DCT)
- 保留变换后的 12~20 个结果,该结果即为求解的 MFCC 特征向量
- 特征归一化
其中要注意最后一步:在使用该特征向量时,为了平均每一维度的特征值对预测的贡献,需要对该特征向量进行归一化处理(也就是说若该特征集中的某个维的值远大于其它维度的值,那么在对特征进行训练时,该维度会主导整个整个网络的训练,这需要避免的)。对于该特征集,常见的归一化处理为倒谱均值归一化 CMN和全局特征标准化。
- 归一化原理:对与每个句子,样本归一化首先要用该句子所有的帧特征估算每维 i 的均值;
- 全局特征标准化的目标是使用全局转换缩放每维数据,使得最终的特征向两处于相似的动态范围内,对于MFCC和 FBANK ,通常将每维特征归一化为均值为 0 ,方差为 1 ;注意这只是统计学中的保守估计,只有当样本数量足够大,均值为0的概率才更大,即对于特征集统计出来的均值不一定为0。
对于计算公式相对简单,可以通过下面的数据归一化参考链接看看;这里不再重复。接下来就是对特征集合 MFCC 的提取过程。
算法实现
因为整个开发围绕这ROS的通信框架,那么对于MFCC特征集,根据之前的设计,同样在特征类中添加一个特征提取函数,并且在 AudioFeatureSet.msg文件中添加MFCC特征数组。由于 Aquila 库中已经实现特征提取过程,那么我们只需要简单调用即可。不过这里有一个问题是:对特征归一化要在每一帧中进行好还是获取整个音频文件再来统计呢?这两种实现方案对与训练又会有什么不一样?
下面就对每一个MFCC集进行特征归一化,简单实现如下:
/*
* 函数功能:获取MFCC特征集
* 参数说明:
* mfccValues: 特征集存放空间
* numFeatures:特征序列个数
* */
void FeatureSet::mfccFeatures(std::vector<double> &mfccValues, std::size_t numFeatures)
{
std::vector<double> tmp(numFeatures);
mfccValues.resize(numFeatures);
Aquila::Mfcc mfcc(m_data.size());
Aquila::SignalSource signal(m_data,m_sampleFrequency);
tmp = mfcc.calculate(signal,numFeatures);
//均值归一 CMM -- 减弱声学信道扭曲带来的影响
double sum = std::accumulate(std::begin(tmp), std::end(tmp), 0.0);
double mean = sum / tmp.size();
for(int i=0;i<numFeatures;i++)
{
tmp[i] = tmp[i] - mean;
}
mfccCMN(tmp,mfccValues);
}
下面依据相关计算公式对特征进行全局归一化处理:
/*
* 函数功能:MFCC 全局特征标准化
* 参数说明:
* input: 输入特征集
* output: 输出特征集
*
* MFCC 集全局特征标准化,即归一化均值为0,方差为1
* */
static void mfccCMN(const std::vector<double> &input,std::vector<double> &output)
{
//均值
double sum = std::accumulate(std::begin(input), std::end(input), 0.0);
double mean = sum / input.size();
sum = 0.0;
//标准差
for(int i=0;i<input.size();i++)
{
sum += (input[i]-mean)*(input[i]-mean);
}
sum = sum/input.size();
double xita = std::sqrt(sum);
//标准化
for(int i=0;i<input.size();i++)
{
output[i] = (input[i]-mean)/xita;
}
std::cout<<std::accumulate(std::begin(output), std::end(output), 0.0)<<std::endl;
}
不要怀疑结果输出之后的均值不是0,注意是围绕0上下波动..(这是针对每一帧归一化,数据量不足),另外还不要注意一点是MFCC对应的是每一帧的特征提取,相当于一个静态的过程。若需要以此对语音进行建模,由于语音是一个动态过程,那么需要使用 MFCC 的一阶和 二阶 差分对动态过程进行逼近,也就是对语音信号变化做不确定估算。所以在语音识别中常用的是以下组合方式:
- 39维特征集为例分为: 13静态系数 + 13一阶差分系数 + 13 二阶差分系数;
- 其中的13维度分为: 语音的其它特征(如能量,过零) + 12维度MFCC
其中一阶差分就是离散函数中连续相邻两项之差。定义 X(k),则此函数的一阶差分就是:
Y(k)=X(k+1)−X(k)Y(k)=X(k+1)−X(k)
同理,Y(k) 一阶差分 X(k)的 二阶差分为:
Z(k)=Y(k+1)−Y(k)=X(k+2)−2∗X(k+1)+X(k)Z(k)=Y(k+1)−Y(k)=X(k+2)−2∗X(k+1)+X(k)
Z(k)为此函数的二阶差分。
对于基于人耳听觉的特征已经提取出来了,接下来就是如何利用这些特征来进行语音识别。