不熟悉语音唤醒的人看此文前 可以先了解一下语音唤醒的一些基本情况发展现状,评价标准等,以免偏颇。
最近做了一个windows下的语音唤醒功能。一些情况记录如下: 之前想过使用百度或科大讯飞的唤醒功能,但百度的目前不支持windows,讯飞使用的是c++.. 后来搜到windows下可以使用mycroft-precise做语音唤醒功能 就拿来试试了。?
1, 首先奇怪的是,知乎网友说 windows下可以使用mycroft-precise,但git-hub页面上的介绍: Precise is designed to run on Linux. It is known to work on a variety of Linux distributions including Debian, Ubuntu and Raspbian. It probably operates on other *nx distributions. ? 看起来不支持windows。 那windows下 到底能否玩起来呢? ?我先下了源码测试和看看 发现至少如下两点有影响:
? 如果只使用唤醒功能,windows下是可以玩起来的,我现在就跑起来了。例如可以下载已经训练好的hey-mycroft.pb 试试。 ?个人试的 使用默认和修改后的控制参数的值 ?能唤醒但成功率都不太高,也就是假负比较高。(也可能是我发音不标准) ?(1) ?但是训练模型需要收集语音,如果使用 precise-collect, 在windows下会有问题。因为collect 需要 from termios import tcsetattr, tcgetattr, TCSADRAIN ? 这句话在linux下无需特别安装什么就能执行成功,但windows下pip怎么也找不到termios这个包。 后来才了解到 termios与串口开发/POSIX规范相关 ?win可以用Cygwin代替。不过我这里 暂时就没追着修改测试这个了,用了另一种方法: 普通录音机程序 录得.m4a文件,再使用ffmpeg转成 precise要求的16000采样频率等要求的格式。 ? ffmpeg 我去年因为magenta折腾过,现在有点connect the dot的感觉,嘿嘿。 ?(2) precise-listen?时, on_activation的调用函数?会调用 ? site-packages\precise\util.py 里的activate_notify ?这里的 play_audio 在win下可能会失败。 ?linux应该也要装东西才OK。 在window下要做修改才能跑通。 我最终的代码 继承了PreciseRunner类, 调用的on_activation的函数是自己写的,不会有这个问题。
2,关于训练的语音来源(以下唤醒词以"小律小律"为例)? ? 由于我手上的linux机器都没有声卡,不能试用 precise-collect,就用windows的录音机采集 wake-word 和 not-wake-word。? ? 根据官网的提示,在录wake-word 时,前面都空了一两秒钟,后面立即结束。一般一条记录两三秒。? ? 录not-wake-word时,官网的提示没强调这个,我就随便录了几条几秒的记录,后来某些记录都录了1分钟左右或更长了。 ? 折腾训练几天后, 再看到源码中这句话, 崩溃了五分钟:
? ? if len(audio) > pr.max_samples:
? ? ? ? audio = audio[-pr.max_samples:] ??
? ?用于训练的语音长度 ?实际只取了后面最多 pr.max_samples长度,而这个的默认值,是buffer_t * sample_rate = 1.5 * 16000 ?等于1.5秒时长。 所有的wake-word not-wake-word 都只取了后1.5时长... 对我的wake-word 影响小点,而not-wake-word 很多录的都没用上,导致训练得不够.. ? ?意识到这之后, 我又开始拿起goldwave, 更精确的处理 录好转换后的音频了。(回想人生 再次connect the dot) 因为两三秒的东西,windows的播放器里看不清楚的,用goldwave或其他音频处理工具, 能准确的控制前后的空白部分,剪成预期想要的样子(总长1.5s以内 前面需要留白的留白)。用这样处理后的数据训练 效果提升了一些些。 (其实刚开始时效果不明显,当我发现train_data代码中的cache后 再崩溃了五分钟..其实之前也发现了本地有个.cache文件夹 但竟然没点进去看没联想到一起..)? ? ?我想着 wake-word 前面的空白有啥用呢 ?是不是去掉空白直接用"小律小律"部分,效果会更好呢? ?实际效果掉得很厉害,很难识别成功。 后来想想 可能是因为 真正有意愿说唤醒词小律小律之前 一般会有一点空白,而一段话中的小律小律,不期望被用来唤醒? ?
3,调整真正/真负偏向之间的控制参数 有两个:trigger_level=3, sensitivity=0.5 ? trigger_level 这是一个什么参数? ?刚开始我也花了好些时间在上面,现在 便于理解的说 (并不是大白话说),就是如果把这个值调小, 在说"小律小" 或"小律" 或"小"字时,(不需要等"小律小律"全部说完), 就可能激活了。 ?
4,发现相同或相似的韵母 很容易被误唤醒。 比如 小聚小聚 ?小旭小旭 小玉小玉 ?小拘小拘 小菊小菊 小举小举 等等。 我下了百度的demo apk,用小度小度唤醒时,说小雾小雾 小律小律 小都小都 小读小读 小赌小赌等 也很容易被误唤醒。 搜了一下,不止我一个人发现,还有人说 每次都用傻度傻度唤醒.. ?当然了 我的傻律 效果还更差一点.. ? ?关于第3 和4 这里我们多看几层源码吧:
? ?(1):关键的 TriggerDetector.update代码 在不知道实际处理逻辑时有点难以理解。 ? ? TriggerDetector.activation 可以理解为 chunk连续激活(激活趋势为连续升)次数。 chunk来自于流,一直在更新,如果连续被激活超过trigger_level次, 就认为唤醒了。 ? ? chunk_activated 为false时, 如果activation > 0 就要自减1, 这是因为要达到的效果是:激活趋势为连续升。 ?如果前面的chunk激活了,后面一个chunk又没有激活,则这个连续激活 要减1次。 ?
? ? self.activation = -(8 * 2048) // self.chunk_size ? ? ? ? 分母的chunk_size 默认值为Engine.chunk_size 2048: Higher numbers decrease CPU usage but increase latency / Higher values are less computationally expensive 数字越大越省cpu但会变慢. ? ? 分子 为什么是(8 * 2048)呢?.. 好吧 写本文时 再拾起以前的疑问..可能是如下依据:https://wenjie.store/archives/about-bytebuf-3 ?:chunk划分为2048个Page,每个Page大小为8kb,Page是给ByteBuf分配内存的最小调度单位 ..(不懂java 逃 不确定用在这里是否正确,这个文章里的chunk比precise里的chunk 看起来要大 page对chunk?.. 欢迎知道的大侠指导指正) ? ? 如果认为已经唤醒(has_activated为True) 或本chunk激活且最近曾经唤醒过(chunk_activated and self.activation < 0) 则保持目前的唤醒状态,不会再次唤醒 ?self.activation计算出来为-8(后续的chunk未激活会递增至0),可以理解为 前8个chunk 都是唤醒的激活的 无需/不能再次唤醒
def update(self, prob):
# type: (float) -> bool
"""Returns whether the new prediction caused an activation"""
chunk_activated = prob > 1.0 - self.sensitivity
# print(" ------------- ", self.sensitivity, round(prob, 3), chunk_activated, self.activation) # 这句打印帮你看得更清 再加个 has_activated后的打印
if chunk_activated or self.activation < 0:
self.activation += 1
has_activated = self.activation > self.trigger_level
if has_activated or chunk_activated and self.activation < 0:
self.activation = -(8 * 2048) // self.chunk_size
if has_activated:
return True
elif self.activation > 0:
self.activation -= 1
return False
? ? ? ?? ? ? (2):为啥相同的韵母 模型会分不太清? ?go~ 去训练模型的代码看看: ? ? TrainData.from_folder 是个classmethod, 从find_wavs函数中得到wav文件 没啥特殊的, data.load 里,vectorizer 函数参数是关键。 它使用归一化后的wav转np.array文件, 用 Vectorizer.mfccs 处理得到特征。
vectorizers = {
Vectorizer.mels: lambda x: mel_spec(
x, pr.sample_rate, (pr.window_samples, pr.hop_samples),
num_filt=pr.n_filt, fft_size=pr.n_fft
),
Vectorizer.mfccs: lambda x: mfcc_spec(
x, pr.sample_rate, (pr.window_samples, pr.hop_samples),
num_filt=pr.n_filt, fft_size=pr.n_fft, num_coeffs=pr.n_mfcc
),
Vectorizer.speechpy_mfccs: lambda x: __import__('speechpy').feature.mfcc(
x, pr.sample_rate, pr.window_t, pr.hop_t, pr.n_mfcc, pr.n_filt, pr.n_fft
)
}
而 mfcc_spec的代码,跟网上很多介绍计算mfcc的步骤基本都是能对应上的。(现学现炒..) ?
def mfcc_spec(audio, sample_rate, window_stride=(160, 80),
fft_size=512, num_filt=20, num_coeffs=13, return_parts=False):
"""Calculates mel frequency cepstrum coefficient spectrogram"""
powers = power_spec(audio, window_stride, fft_size)
if powers.size == 0:
return np.empty((0, min(num_filt, num_coeffs)))
filters = filterbanks(sample_rate, num_filt, powers.shape[1])
mels = safe_log(np.dot(powers, filters.T)) # Mel energies (condensed spectrogram)
mfccs = dct(mels, norm='ortho')[:, :num_coeffs] # machine readable spectrogram
mfccs[:, 0] = safe_log(np.sum(powers, 1)) # Replace first band with log energies
if return_parts:
return powers, filters, mels, mfccs
else:
return mfccs
? ? 看样子 对于不同的样本,filterbanks的结果 大体是一样的 ?会造成mels的值 不同的的原因 主要在powers上,也就是power_spec的计算: ?np.fft.rfft 离散傅立叶变换,再计算得到的复数的实部虚部平方和除以fft_size。?
def chop_array(arr, window_size, hop_size):
"""chop_array([1,2,3], 2, 1) -> [[1,2], [2,3]]"""
return [arr[i - window_size:i] for i in range(window_size, len(arr) + 1, hop_size)]
def power_spec(audio: np.ndarray, window_stride=(160, 80), fft_size=512):
"""Calculates power spectrogram"""
frames = chop_array(audio, *window_stride) or np.empty((0, window_stride[0]))
fft = np.fft.rfft(frames, n=fft_size)
return (fft.real ** 2 + fft.imag ** 2) / fft_size
? ? 好吧 到这里我还是没找到 为什么/怎样改进 对相似韵母的识别情况.. ?百度都没能改进的,哪能这么快被我找到,呵呵,不过将来不保证 哈哈
5, 另外的,项目中激活后需要实现说话录音功能。语音检测要用VAD技术。python有个库 webrtcvad可以用。当我找到这些例子,又再觉得python真友好,也感谢别人的分享,不然我自己很难/几乎不可能折腾出来。当然如果有国产语言 也对程序员友好生态又好 就太好啦。 ? ? 关键的判断 active = vad.is_speech(chunk, default_rate) ?C源码我还没学习 原理可参考 https://www.cnblogs.com/dylancao/p/7663755.html ? ? webrtc的vad检测原理是根据人声的频谱范围,把输入的频谱分成六个子带(80Hz~250Hz,250Hz~500Hz,500Hz~1K,1K~2K,2K~3K,3K~4K。)分别计算这六个子带的能量。然后使用 高斯模型的概率密度函数做运算,得出一个 对数似然比函数。对数似然比 分为全局和局部,全局是六个子带之加权之和,而局部是指每一个子带则是局部,所以语音判决会先判断子带,子带判断没有时会判断全局,只要有一方过了,就算有语音。
写这些的过程,又多自问自答了一些问题,还是挺有收获。 语音方面我基础薄弱,上面的理解很可能有错误,欢迎指正,谢谢!
|