MATLAB人声分离实践:NMF与RPCA双路线从入门到调参
2026/8/31 4:57:24 网站建设 项目流程

简介:本资源面向音频信号处理初学者与MATLAB进阶学习者,聚焦音乐与人声分离这一典型盲源分离任务,覆盖音频后期制作、智能语音增强及数字音乐分析等实际应用场景。压缩包共17个文件(28.96MB),包含6个核心MATLAB脚本(如repet.m、urepet.m、review.m等,实现STFT特征提取、NMF/ICA建模与GUI交互演示)、4段含伴奏与人声的MP3测试音频(含流行、动漫配乐等多风格样本)、2份PDF技术报告(含算法原理、实验设计与评估指标说明)、3个.zbak备份文件及1个说明文本,结构清晰、模块对应完整流程。已有56人学习下载,资源提供从数据加载、时频分析、模型构建、算法实现到结果评估的全链路可运行代码,配套注释详尽,并涵盖谱减后处理与SISNR量化评估等关键实践细节,便于读者复现、调试与二次开发。

1. 分离的本质:一段混音里,人声和伴奏到底差在哪

1.1 为什么不能靠高通/低通滤波一劳永逸

入坑音乐分离的第一步,十有八九会先想:人声不是集中在某个频段吗?低通滤掉低频,高通滤掉高频,不就把伴奏抠出来了吗?我当年也是这么想的,直到把一段鼓点密集、吉他和弦铺满全频段的流行歌丢进滤波器,出来的结果惨不忍睹——人声消了,但镲片的沙沙声、钢琴的高频泛音全留在所谓“伴奏轨”里,听着像蒙了一层漏水的墙皮。

原因很简单:人声并不是只在某个孤立的频段,它的基频可能只有一两百赫兹,但泛音能一直延伸到几千甚至上万赫兹。伴奏里的弦乐、钢琴、镲片同样横跨这个范围。频率轴上,二者从头到尾都在重叠,任何固定分频点的滤波器都切不干净。这个问题不是滤波能解决的,必须在更丰富的表达空间里做文章。

1.2 把问题搬到时频域,变成一个矩阵分解问题

真正实用的做法是把音频先变成时频图,也就是做短时傅里叶变换(STFT)。在二维的时频谱上,人声和伴奏的形态差异会变得明显:伴奏里的和弦、鼓点往往反复出现,在谱图上呈现周期性、规律性的纹理;人声则更像一条条随时间扭动的谐波线,有大量瞬时的能量起伏。用个生活化的类比:你拍了一张站在花墙前面的人像照,墙纸的花纹高度重复、规律性强,而人只要稍微动一下,轮廓和姿态就会打破这种规律。去背景的时候你不会去逐像素“让某个颜色消失”,而是会想办法把“重复出现的背景纹理”和“形状变化的前景”分开。

放到音频里,这个二维时频谱就是一张灰度图。伴奏对应“重复出现的背景纹理”,人声对应“形状变化的前景”。所以分离问题最终变成了:怎么把一张二维谱矩阵拆成两个部分,一部分是低秩的、重复的伴奏,另一部分是稀疏的、变化的人声。这就是后面要讲的NMF和RPCA的出发点。

1.3 为什么在MATLAB里做这件事

老实说,现在开源社区里人声分离的顶流工具几乎全是Python生态,Spleeter、Demucs、UVR这些,效果确实好。但当时我手上的工程从数据读取、前端增强到后期评估全在MATLAB里,为分离这一个环节单独拉一条Python环境,光依赖管理就能折腾一下午。更重要的是,NMF和RPCA这种经典方法本身就是线性代数操作,MATLAB的矩阵运算和内置的svdnnmf函数天然顺手,几十行代码就能跑通。这篇文章把我实际跑过的两条路线完整过一遍,包括参数怎么选、重建怎么验证、结果怎么判好判坏,适合那些不想跳出MATLAB工作流、又想自己掌控分离细节的读者。

2. 第一步跑通:STFT与逆变换的参数选型与重建校验

2.1 读音频与转单声道

音频预处理的起点很枯燥,但坑很多。我先把立体声转成单声道,这里要说明:直接取平均是最省事的做法,但对有些混音来说,人声恰好被做在正中间,左右声道取平均确实能保留人声,而部分乐器和声像偏移会被削弱,这反而给后续分离帮了忙。如果不想损失立体声信息,也可以左右声道分别做分离再合回去,但为了讲清原理,我们先在单声道上跑通。

[x, fs] = audioread('mixture.wav'); if size(x, 2) > 1 x = mean(x, 2); % 转单声道 end

2.2 窗口、帧移与FFT长度的取舍

STFT的参数决定了你在时间分辨率和频率分辨率之间怎么站队。以一个44.1kHz采样率的音频为例,我习惯用的是:

winLen = 1024; % 约23.2ms hopLen = 256; % 约5.8ms nfft = 1024; win = hann(winLen, 'periodic')';

三个参数得拆开看。winLen是分析窗口长度,1024个采样点意味着频率分辨率大约fs/winLen = 43Hz,这个精度足以分辨相邻的音符和大部分泛音列。hopLen是帧移,越小时间维度上越细腻,256对应大约75%的重叠,重建时接缝更平缓。nfft一般等于winLen,除非你想做补零插值。

为什么不把窗口拉长到4096?那样频率分辨率确实能到10Hz左右,但时间分辨率会掉到约93ms,歌手咬字、换气、辅音起音这些瞬态信息全糊成一团,分离出来的人声会缺少“嘹亮”的感觉。反过来,窗口缩到256,时间分辨率好了,但频率分辨率变成172Hz,相邻谐波会粘在一起,后面的矩阵分解算法很难把它们区分开。1024对这个任务来说是个起步点,后面可以根据实际听感在512到2048之间微调。

2.3 逆变换重建必须验证

分离之前,必须先确认正逆变换这块地基是稳的。方法是:把信号做一遍STFT,再立刻做ISTFT重建,对比原始波形。

X = stft(x, fs, 'Window', win, 'OverlapLength', winLen - hopLen, 'FFTLength', nfft); x_recon = istft(X, fs, 'Window', win, 'OverlapLength', winLen - hopLen, 'FFTLength', nfft, 'ConjugateSymmetric', true); relErr = norm(x(:) - x_recon(:)) / norm(x(:)); fprintf('重建相对误差: %.6f\n', relErr);

如果relErr远大于1e-6,先别往下走。大概率是参数没对齐,比如OverlapLength写成hopLen了,或者Windownfft不匹配。这一步相当于施工前的承重墙验收,省掉它的话,后面所有分离结果都会带着一层莫名其妙的“水声”伪影,你还以为是算法问题,其实是重建链路就在漏风。注意,用这个istft函数需要R2021a以上的版本;如果手头版本比较老,可以用spectrogram拿复数谱,再自己写一个加权重叠相加(Weighted Overlap-Add)配合合成立刻恢复,原理一样,只是代码多一些。

3. NMF路线:幅度谱分解与组件归属的尴尬

3.1 NMF能给我们什么

非负矩阵分解(NMF)的思想不复杂:把幅度谱矩阵V(尺寸是频率点数乘帧数)近似分解成两个非负小矩阵的乘积V ≈ W * H。其中W的每一列是一个“频谱基底”,可以理解成一种声音的“颜色模板”;H的每一行是该基底随时间出现的强度。因为音频幅度谱天然非负,这种分解不会出现“负数声音相消”这种没有物理意义的组合。

NMF用来做分离的思路是:假设伴奏里的鼓、贝斯、钢琴可以由少数几个基底表示,人声由另外几个基底表示,那么分解之后只要把基底分成“伴奏组”和“人声组”,再分别重建就能得到分离结果。这个想法在原理上清晰,但实操时有一道坎。

3.2 用30行实现一个乘法更新NMF

MATLAB的Statistics and Machine Learning Toolbox里有nnmf函数,开箱即用。不过自己写一遍乘法更新能让你真正理解它,而且遇到工具箱没装的情况也不慌。标准更新公式是Lee和Seung提出的乘法规则,我按自己的习惯做了列归一化,避免WH之间的缩放歧义导致数值溢出:

function [W, H] = simple_nmf(V, k, iters) [m, n] = size(V); W = rand(m, k) + 0.01; H = rand(k, n) + 0.01; epsVal = 1e-9; for iter = 1:iters % 更新 H H = H .* ((W' * V) ./ (W' * W * H + epsVal)); % 更新 W,再做列归一化,避免数值溢出 W = W .* ((V * H') ./ (W * H * H' + epsVal)); colNorm = sum(W, 1) + epsVal; W = W ./ colNorm; H = H .* colNorm'; end end

调用的时候传入幅度谱:

V = abs(X); k = 8; % 基底数 [W, H] = simple_nmf(V, k, 200);

注意k的取值很关键。太小,模型表达力不够,每种声音都得硬挤进少数模板,分离效果粗糙;太大,每个基底变成只刻画某一帧的细节,反而不知道拿它怎么办。我自己做简单试听时通常从k=8起步,分析成分,再逐步调整。

3.3 组件归属问题与一个能跑的软掩码方案

NMF分解本身只负责“把谱拆成几份”,它没有告诉你哪份是人声。这是NMF分离最尴尬的地方:模型是对的,分类是猜的。理论上可以通过监督学习训练一个分类器判断基底属于哪类声音,但在一个自包含的项目里,我用过最朴素的启发式是:人声的频谱质心通常偏高,且时间激活模式更稀疏变化。如果只设k=2,那大概率一个基底覆盖了稳定的伴奏,另一个覆盖了时变的人声。用k=8以上时,就得把多个基底聚成两组。

一个很快的软掩码做法是:把每个基底的分量都重建出来,再按频谱质心挑出像人声的那部分,生成时频掩码。

function [voice, music] = nmf_split(x, fs, k, iters) winLen = 1024; hopLen = 256; nfft = 1024; win = hann(winLen, 'periodic')'; X = stft(x, fs, 'Window', win, 'OverlapLength', winLen-hopLen, 'FFTLength', nfft); V = abs(X); [W, H] = simple_nmf(V, k, iters); comps = zeros(size(V,1), size(V,2), k); for i = 1:k comps(:,:,i) = W(:,i) * H(i,:); end freqs = linspace(0, fs/2, size(V,1))'; centroid = zeros(k, 1); for i = 1:k centroid(i) = sum(freqs .* W(:,i)) / sum(W(:,i)); end [~, vocalIdx] = max(centroid); % 一个非常粗糙的启发式 total = sum(comps, 3) + 1e-6; mask = comps(:,:,vocalIdx) ./ total; voiceV = V .* mask; musicV = V .* (1 - mask); voice = istft(voiceV .* exp(1i*angle(X)), fs, ... 'Window', win, 'OverlapLength', winLen-hopLen, 'FFTLength', nfft, 'ConjugateSymmetric', true); music = istft(musicV .* exp(1i*angle(X)), fs, ... 'Window', win, 'OverlapLength', winLen-hopLen, 'FFTLength', nfft, 'ConjugateSymmetric', true); voice = voice(:); music = music(:); end

这段代码能跑,但效果也就到“能听出来分离了个东西”的程度。问题在于频谱质心不一定等于区分人声和伴奏的可靠特征。高音区和弦、镲片也有很高的质心,它们极容易被误归为人声。

3.4 如果真想用NMF出好结果:预训练基底

想让NMF分离有实际可用性,一种更靠谱的做法是“监督式NMF”:先准备几条纯人声和纯伴奏的样本,分别做NMF提取基底,得到W_vocalsW_music;然后对新的混合音频,固定这些基底不变,只更新H。这样基底的归属是已知的,分离就变成求解一个带约束的线性最小二乘问题。

实际做的时候,控制基底的更新只用一次乘法更新即可,也就是把前面simple_nmf里同时更新WH改成只更新H。这种路线效果会比纯无监督好不少,但需要你有适合的样本数据,所以如果你手头只有孤零零一首歌,且不打算收集训练数据,NMF这条路我就建议浅尝辄止,重点看下一条RPCA。

4. RPCA路线:把伴奏当“低秩背景”的正确姿势

4.1 低秩与稀疏的含义

RPCA(鲁棒主成分分析)是NMF之外另一条经典路线,而且在实际体验里比纯无监督NMF稳定得多。它的核心假设是:伴奏在时频谱中表现为低秩部分,因为音乐伴奏的结构化重复度高,铺开的谱矩阵本质上是很多相似列的组合;人声则表现为稀疏部分,因为人声在时频谱上只占据相对少的高能量时频点,其余位置能量较低。

把幅度谱V分解成L + S,其中L是低秩矩阵,对应伴奏;S是稀疏矩阵,对应人声。求解过程通过最小化||L||_* + λ||S||_1实现,前者是核范数(矩阵奇异值之和),抑制矩阵的秩;后者是L1范数,抑制稀疏矩阵里非零元素的个数。这个优化问题现在最常被用的是非精确增广拉格朗日乘子法(inexact ALM),总共几十行就能实现。

4.2 ALM求解RPCA的MATLAB实现

下面这段代码是我在项目里实际用的版本,它来自经典的RPCA求解框架,我做了必要的简化和收敛判断,注释里写清了每一步在干什么:

function [L, S] = inexact_alm_rpca(V, lambda, tol, maxIter) if nargin < 2 || isempty(lambda) lambda = 1 / sqrt(max(size(V))); end if nargin < 3 || isempty(tol) tol = 1e-7; end if nargin < 4 || isempty(maxIter) maxIter = 1000; end normV = norm(V, 'fro'); Y = V / max(norm(V(:)), 1/lambda); mu = 1.25 / norm(V, 2); rho = 1.5; L = zeros(size(V)); S = zeros(size(V)); for iter = 1:maxIter % 更新 L: 对 V - S + Y/mu 做奇异值软阈值 [U, sig, Vt] = svd(V - S + (1/mu)*Y, 'econ'); sig = max(sig - 1/mu, 0); L = U * diag(sig) * Vt; % 更新 S: 对 V - L + Y/mu 做逐元素软阈值 M = V - L + (1/mu)*Y; S = sign(M) .* max(abs(M) - lambda/mu, 0); % 更新拉格朗日乘子 Y = Y + mu * (V - L - S); mu = mu * rho; % 收敛检查 err = norm(V - L - S, 'fro') / normV; if err < tol break; end end end

调用时直接传幅度谱:

V = abs(X); lambda = 1 / sqrt(max(size(V))); [L, S] = inexact_alm_rpca(V, lambda);

lambda这个参数值得细说。通用默认值是1/sqrt(max(size(V))),这是理论推导出来适合一般矩阵的取值。但在音频分离里它不是一个死数。我实测的感觉是:如果你觉得人声轨里伴奏残留太多,可以稍微调大lambda,比如1.3倍默认值,强制S更稀疏,把背景里的成分进一步推向L;如果你觉得人声轨丢掉了太多高频气息和辅音,听起来“虚”,就适当调小lambda,让S容纳更多能量。注意,lambda永远是在“人声干净”和“人声完整”之间做权衡,不存在一个万能值。

4.3 用混合谱的相位重建时域信号

RPCA分解和NMF一样,处理的都是幅度谱。但人耳对相位极其敏感,偏偏现成的相位信息只有混合音频这一份。怎么办?标准做法是直接把混合谱的相位同时赋给分离后的LS,然后用ISTFT变回时域。听着有点粗暴,但它确实是最常用、也最容易落地的方案。频域掩码分离的伪影多半来自这个“共享相位”的近似,但实际听感仍可接受。

X_phase = exp(1i * angle(X)); music = istft(L .* X_phase, fs, ... 'Window', win, 'OverlapLength', winLen-hopLen, 'FFTLength', nfft, 'ConjugateSymmetric', true); voice = istft(S .* X_phase, fs, ... 'Window', win, 'OverlapLength', winLen-hopLen, 'FFTLength', nfft, 'ConjugateSymmetric', true); audiowrite('music_out.wav', music/max(abs(music)), fs); audiowrite('voice_out.wav', voice/max(abs(voice)), fs);

这里我习惯在保存前做一次归一化,不是必须的,但能避免生成一个整体音量特别小的文件,每次试听都要手动调增益。

整个流程跑下来,我体感RPCA比纯无监督NMF省心不少。不用纠结分配给哪个基底,也不用先准备训练样本。它是那种“起步就能用,调参只影响细节”的算法。

5. 结果怎么验收:主观听感、客观指标与常见伪影

5.1 先听,再算数

分离算法这东西,最诚实的验收方式是耳朵。我习惯把原曲、音乐轨、人声轨三段音频在一个播放器里连续切换着听,重点盯三件事:

  • 人声轨里有没有还能清楚听见的鼓点或乐器,有就说明伴奏泄漏进S了。
  • 音乐轨里有没有残留人声的“虚影”,尤其是副歌高音部分,容易在L里留下痕迹。
  • 分离后人声有没有“水声”或“金属声”,这通常是STFT参数太小或者相位近似导致的伪影。

如果在听感上有明显不满,先别急着改lambda,回头检查一下STFT的hopLen是否足够小。帧移过大时,逆变换的重叠区域太少,软掩码在时间衔接处不平滑,听起来就会有一种“沙沙碎碎”的杂质。把hopLen从256降到128,有时候比调任何矩阵分解参数都立竿见影。

5.2 用SDR量化分离质量

如果你手上恰好有分离之前的纯人声、纯伴奏的真值(比如你拿一首歌的工程分轨来测试),就可以用信号失真比(SDR)来做量化评估。SDR本质上衡量的是分离信号和真值之间的能量比,越高说明分离越干净。一个简单的MATLAB实现是:

function sdr = calc_sdr(est, ref) est = est(:); ref = ref(:); n = min(length(est), length(ref)); est = est(1:n); ref = ref(1:n); sdr = 10 * log10(sum(ref.^2) / sum((est - ref).^2)); end

测的时候分别对人声轨和音乐轨算一遍,然后取平均。多说一句:SDR高不等于听感好,因为有些分离结果虽然整体能量逼近真值,但残留的鼓点、齿音可能比SDR高但能量占比小的信号更刺耳。所以我的习惯是“耳朵先过一遍,SDR再定量确认”。

5.3 常见伪影和调参方向

根据我跑过的几首不同风格的歌,整理了一个简单的排查表:

症状可能原因调整方向
人声轨里还有钢琴/吉他声lambda偏小,S吸收了大量非稀疏成分调大lambda,例如1.2~1.5倍默认值
音乐轨里残留人声S过分稀疏,部分人声能量被推进了L调小lambda,例如0.7~0.9倍默认值
分离后人声发闷/没有气息感STFT频率分辨率不足或lambda过大增大winLen到2048,同时适当调小lambda
鼓点被打散,音乐轨缺乏力度算法把鼓的瞬态当成稀疏成分抓走了调大lambda,强制更多内容进入低秩

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询