🧠 第五章 深度学习算法
📄 一句话概括

3 秒 16 kHz 音频切成 298 帧,门限取峰值能量的 25%(0.1063),检测出 2 段语音, 与埋进去的两段相比起终点各差 1~4 帧。语音总时长 1.45 秒,只占 48%——一半是静音。

📝 本节和前面几节的关系

上一节处理文本,这一节处理声音。 语音聊天看着最像「AI」,其实它是本章链条最长的一个:四段接力,任何一段塌了后面全废。

一、语音聊天是四段接力

语音识别 → 语言理解 → 生成回答 → 语音合成。四段,缺一段都聊不起来。 第一段之前还有一道门:端点检测。它把静音切掉,只把真正有声音的部分送进识别模型。 这道门看着不起眼,却决定两件事:省多少算力,以及开头结尾的字会不会被削掉。 Demo 只做这一道门。 后面三段都要模型,沙箱里装不了,也调不到在线接口——这一点我在本节末尾会再说一次。

二、分帧:把连续的声音切成小片

声音是一串采样点。16 kHz 就是每秒 16000 个点,3 秒就是 48000 个点。 帧长 25 毫秒(400 点),帧移 10 毫秒(160 点)。相邻帧重叠 15 毫秒。 为什么要重叠?声音的特征在帧边界上会突变,重叠能让特征平滑过渡,也让短促的音不会被切在缝里丢掉。 3 秒按 10 毫秒一帧算,看着像 300 帧,实际算出 298 帧。差的两帧是首尾凑不满一整个窗被丢掉的。 每帧算一个能量(采样点平方的均值),就得到一条能量曲线。这一步是纯算术,不需要任何模型。

三、门限怎么定:峰值的 25%

门限不能写死一个绝对数。换一支麦克风、录音音量差十倍,绝对门限就全废。 所以取峰值能量的 25%,本例算出来是 0.1063。 25% 这个比例是我定的。定高了,说话声轻的地方会被当静音切掉;定低了,呼吸和底噪会被当成语音留下来。 Demo 还算了过零率,中位数是 0.029。 过零率是波形穿过零点的频繁程度:浊音(a、o 这类)波形慢,过零率低; 清音(s、f 这类)接近噪声,过零率高。 工业上的端点检测不会只用能量。能量加过零率,再叠一个几十 KB 的小网络,才在嘈杂环境里稳得住。

四、检测结果:差几个帧,够用

埋进去的两段是 0.40~1.05 秒和 1.55~2.45 秒。检测出来是 0.41~1.02 和 1.57~2.41。 起点差 1~2 帧,终点差 3~4 帧,也就是 10 到 40 毫秒。这个误差对人耳没影响,对识别模型也够用。 终点普遍偏早,是这套方法的固有偏差:能量掉到门限以下就判结束,而人说话结尾的音往往很轻,先掉下去。 语音总时长 1.45 秒,占 48%。 也就是说一半时间在静音——砍掉这一半,后面的识别模型就省了一半的活。 这个取舍没有标准答案:门限调低会多留静音(多算但安全),调高会削字头字尾(省算但丢内容)。 要看后面接的是什么模型、能容忍多少延迟。 最后把话说清:这个 Demo 只跑到「切出语音段」。 真要做语音聊天,还需要声学模型做识别、语言模型做理解与生成、声码器做合成,这三样沙箱里都没有, 我也不会假装跑过。

五、动手 Demo:语音端点检测:把静音切掉,只留说话段

合成一段 3 秒 16 kHz 音频,在两段指定区间里填带包络的谐波、其余填低幅噪声; 按 25 毫秒窗、10 毫秒移分帧,算每帧能量; 门限取峰值能量的 25%,把连续超门限的帧合并成段,再把帧号换算回秒,最后和埋进去的真实区间对比。

win, hop = int(0.025 * sr), int(0.010 * sr)   # 400 / 160
n = 1 + (len(x) - win) // hop                  # 298 帧

e = np.array([np.mean(x[i * hop:i * hop + win] ** 2)
              for i in range(n)])
thr = 0.25 * e.max()

on = e > thr
# 连续为 True 的帧并成一段,再按帧号换算回秒

实跑输出:

--------------------------------------------------------------------
语音端点检测:把静音切掉,只留说话段
--------------------------------------------------------------------
  采样率                   16000 Hz,时长 3.0 秒
  分帧                     窗 25 毫秒,帧移 10 毫秒,共 298 帧
  能量门限                 峰值能量的 25% = 0.1063
  过零率中位数             0.029

  真实语音段(我埋的):
    0.40 ~ 1.05 秒
    1.55 ~ 2.45 秒
  检测到的段:
  起点(秒)   终点(秒)   时长(秒)
  -----------------------------
      0.41       1.02       0.61
      1.57       2.41       0.84

  段数                     检测 2,真实 2
  语音总时长               1.45 秒(占 48%)

  能量曲线(每 10 毫秒一帧,按帧序号):
        *         **
       * *       *  *
                     *
      *   *     *

                      *
               *
  ****     ****        ****
  ^ 高 0.4240  低 0.0110

  端点检测是所有语音系统的第一道门:它砍掉静音,
  后面的识别模型就只需要处理真正有声音的那几秒。
  砍得准,能省一半算力;砍错了,开头结尾的字就丢了。

  这个 Demo 只到「切出语音段」。后面的语音识别(ASR)和
  语音合成(TTS)都要模型,沙箱里装不了,也调不到在线接口。
  所以「语音聊天」这个实例,能真跑的是前面这一段。

298 帧不是 300。 算法是 1 加 (48000 减 400) 整除 160,得 1 加 297 等于 298,最后一帧凑不满一个窗就被丢了。 门限 0.1063 正好落在语音段和静音段之间,所以两段都检出来了。 这个数不是调出来的,是峰值乘 0.25 算出来的。 两段时长 0.61 秒和 0.84 秒,加起来 1.45 秒,占全长 48%。另一半是静音,被砍掉。 能量曲线那张图能看出两团高能量区,中间掉下去一段——那就是两句之间的停顿。 曲线是用字符画的,一列一帧,横向缩过,只表示形状不表示精确值。

30 秒实验

  1. 把门限比例从 25% 改成 10% 和 50%,看检测出几段、语音总时长怎么变。
  2. 把帧移从 10 毫秒改成 25 毫秒,看段边界精度差多少。
  3. 想一想:为什么终点误差比起点误差大?

常见误区

  1. 「语音聊天就是语音识别」——它是四段接力,识别只是第一段。
  2. 「门限可以写死一个数」——录音音量差十倍就全废,得按峰值取比例。
  3. 「静音切掉不影响结果」——切早了削字头,切晚了白算,两头都疼。
自测三问(点开对答案) 1. 端点检测在整条语音链路里起什么作用? —— 砍掉静音,只把有声段送进识别模型,省算力也省延迟。 2. 为什么门限要用峰值的比例,而不是一个固定值? —— 录音音量差异很大,固定值换支麦克风就失效。 3. 本例的终点为什么普遍偏早? —— 能量掉到门限以下就判结束,而句尾的字往往很轻,先掉下去。

第五章到此结束。下一章进自然语言处理,把这一章零散摸到的文本处理,系统地讲一遍。

相关笔记