Android Kotlin语音发音准确率对比结果异常问题求助
问题:MFCC+DTW计算发音相似度时,无关音频相似度偏高(70%-90%)
我正在开发一款对比用户发音与设备音频准确率的应用,采用MFCC提取特征+DTW算法对比的方案,但发现即使录制和设备音频完全不同的句子,相似度始终维持在70%-90%区间。尝试过切换CosineDistance、EuclideanDistance,也对MFCC做了归一化处理,问题依然存在。
以下是我用于检查相似度百分比的类:
class SimilarityChecker { private val sampleRate = 16000 private val nMFCC = 13 private val bufferSize = 512 private val nFFT = 2048 private val hopLength = 512 private val nMels = 128 fun percentage(file1Path: String, file2Path: String): Double { val mfcc1 = extractMFCCs(file1Path) val mfcc2 = extractMFCCs(file2Path) val percentage = calculateDTWDistance(mfcc1 , mfcc2) return (percentage * 100.0).roundToInt() / 100.0 } private fun extractMFCCs(@NonNull path: String): MutableList<MutableList<Float>> { Log.i("path from Main:", path) var fileList = JLibrosa().loadAndRead(path, sampleRate, -1) val MFCC = JLibrosa().generateMFCCFeatures(fileList, sampleRate, this.nMFCC, nFFT, nMels, hopLength) .map { arr -> arr.toList() } return MFCC.map { d -> d.toMutableList() }.toMutableList() } private fun calculateDTWDistance( referenceFeatures: MutableList<MutableList<Float>>, studentFeatures: MutableList<MutableList<Float>> ) : Double { val dtw = DTW() val ldTW = io.github.cawfree.dtw.DTW() var distances: MutableList<Double> = mutableListOf() for (i in referenceFeatures.indices) { distances.add( FastDTW.compare( TimeSeriesBase( referenceFeatures[i].mapIndexed { index, value -> TimeSeriesItem( index.toDouble(), TimeSeriesPoint(doubleArrayOf(value.toDouble())) ) } ), TimeSeriesBase( studentFeatures[i].mapIndexed { index, value -> TimeSeriesItem( index.toDouble(), TimeSeriesPoint(doubleArrayOf(value.toDouble())) ) } ), 10, Distances.EUCLIDEAN_DISTANCE ).distance ) } val distance = distances.average() return if (distances.max() == 0.0) 100.0 else ((distances.max() - distance) / distances.max()) * 100 } }
问题根源分析
DTW使用逻辑错误
代码中对每个MFCC维度(比如第1到第13个系数)单独执行DTW,再取平均距离转换为相似度。但MFCC的正确结构是:每一行是一个时间帧的13维特征向量,应该把整个MFCC矩阵作为多维时间序列执行DTW,而非拆分维度单独计算,否则会丢失特征间的关联信息,导致区分度大幅下降。相似度转换公式不合理
((distances.max() - distance)/distances.max())*100的计算逻辑依赖单个维度的最大距离,这个值不具备全局可比性,会导致无关音频的距离被错误映射到高相似度区间。缺少关键预处理步骤
未对MFCC执行均值方差归一化(CMVN),无法消除不同录制环境(如麦克风差异、背景噪音)带来的特征偏移,导致无关音频的特征分布重叠度高。
修正方案
1. 正确处理多维MFCC时间序列
将整个MFCC的帧序列(每个帧是13维向量)传入DTW,计算全局的DTW距离。
2. 优化相似度映射逻辑
DTW距离越小,序列相似度越高,需将距离映射到0-100区间:
- 先通过离线统计确定无关音频的DTW距离阈值(比如取大量无关样本的平均距离+2倍标准差)
- 相似度 =
max(0, 100 - (当前DTW距离 / 阈值距离) * 100)
3. 加入MFCC预处理(CMVN)
对每个MFCC维度做均值方差归一化,消除环境干扰。
修正后的代码示例
class SimilarityChecker { private val sampleRate = 16000 private val nMFCC = 13 private val nFFT = 2048 private val hopLength = 512 private val nMels = 128 // 离线统计得到的无关音频DTW距离阈值(根据实际样本调整) private val maxDtwThreshold = 150.0 fun percentage(file1Path: String, file2Path: String): Double { val mfcc1 = extractAndPreprocessMFCCs(file1Path) val mfcc2 = extractAndPreprocessMFCCs(file2Path) val dtwDistance = calculateMultiDTWDistance(mfcc1, mfcc2) // 映射为0-100的相似度 val similarity = if (dtwDistance >= maxDtwThreshold) 0.0 else (100.0 - (dtwDistance / maxDtwThreshold) * 100.0) return similarity.roundToInt() / 100.0 } private fun extractAndPreprocessMFCCs(path: String): List<DoubleArray> { Log.i("path from Main:", path) val audioData = JLibrosa().loadAndRead(path, sampleRate, -1) // 提取MFCC特征 val mfccFeatures = JLibrosa().generateMFCCFeatures(audioData, sampleRate, nMFCC, nFFT, nMels, hopLength) val mfccList = mfccFeatures.map { it.toDoubleArray() }.toList() // 执行CMVN归一化 return applyCMVN(mfccList) } private fun applyCMVN(mfccList: List<DoubleArray>): List<DoubleArray> { val numFeatures = mfccList.firstOrNull()?.size ?: return mfccList val means = DoubleArray(numFeatures) { 0.0 } val stds = DoubleArray(numFeatures) { 0.0 } // 计算每个维度的均值 mfccList.forEach { frame -> frame.forEachIndexed { idx, value -> means[idx] += value } } means.forEachIndexed { idx, sum -> means[idx] = sum / mfccList.size } // 计算每个维度的标准差 mfccList.forEach { frame -> frame.forEachIndexed { idx, value -> stds[idx] += Math.pow(value - means[idx], 2.0) } } stds.forEachIndexed { idx, sum -> stds[idx] = Math.sqrt(sum / mfccList.size) // 避免除以0 if (stds[idx] < 1e-6) stds[idx] = 1.0 } // 归一化每个帧 return mfccList.map { frame -> frame.mapIndexed { idx, value -> (value - means[idx]) / stds[idx] }.toDoubleArray() } } private fun calculateMultiDTWDistance(reference: List<DoubleArray>, student: List<DoubleArray>): Double { // 构造多维时间序列 val refTimeSeries = TimeSeriesBase( reference.mapIndexed { timeStep, features -> TimeSeriesItem(timeStep.toDouble(), TimeSeriesPoint(features)) } ) val studentTimeSeries = TimeSeriesBase( student.mapIndexed { timeStep, features -> TimeSeriesItem(timeStep.toDouble(), TimeSeriesPoint(features)) } ) // 计算全局DTW距离 return FastDTW.compare(refTimeSeries, studentTimeSeries, 10, Distances.EUCLIDEAN_DISTANCE).distance } }
额外优化建议
- 扩展特征维度:在MFCC基础上计算一阶差分(delta)和二阶差分(delta-delta),将特征维度扩展到39维,提升区分度。
- 加入DTW窗口约束:使用Sakoe-Chiba窗口限制DTW路径搜索范围,减少无关帧的匹配。
- 动态校准阈值:定期用新的样本更新
maxDtwThreshold,确保相似度计算的准确性。
内容的提问来源于stack exchange,提问作者Cooper
相关产品推荐
相关产品推荐

