The SpeechBrain Toolkit
SpeechBrain是一个基于PyTorch的开源语音库,致力于提供一个简单、灵活、用户友好的工具包,可用于开发state-of-the-art的语音系统,包括语音内容识别、说话人识别等。
grad为NoneType的问题
SpeechBrain的模型参数是不会有这个问题的,毕竟人家也是这么训练过来的,梯度为None的话还怎么训练。但是对输入音频求梯度却是None
,问题演示如下:
1 2 3 4 5 6 7 8 9 |
audio.requires_grad = True out_prob, score, index, text_lab = classifier.classify_batch(audio) classifier.modules.zero_grad() cost = out_prob[0, 2] cost.backward() print(audio.grad) >>> None |
考虑对输入求梯度,而不是对模型参数求梯度主要是为了做一些对抗样本(Adversarial Example)的研究。
如果使用Pretrained
模型,首先可能想到的是把freeze_params
参数设成False
,如下:
1 2 3 4 5 |
classifier = EncoderClassifier.from_hparams(source="xxx/best_model", hparams_file='hparams_inference.yaml',\ savedir="xxx/best_model",\ freeze_params=False) |
具体可以参考speechbrain/pretrained/interface.py
文件(GitHub – interface.py)。EncoderClassifier
类继承了Pretrained
类。Pretrained
类中freeze_params
用于将所有参数的requires_grad
属性设为False
,如下面的源码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def _prepare_modules(self, freeze_params): """Prepare modules for computation, e.g. jit. Arguments --------- freeze_params : bool Whether to freeze the parameters and call ``eval()``. """ # Make jit-able self._compile_jit() self._wrap_distributed() # If we don't want to backprop, freeze the pretrained parameters if freeze_params: self.modules.eval() for p in self.modules.parameters(): p.requires_grad = False |
但这其实不是问题所在。熟悉PyTorch的Autograd机制就会知道,如果叶子结点x
的requires_grad
属性设为True
,y
的requires_grad
属性设为False
,z=x+y
的requires_grad
属性会自动为True
同时增加一个grad_fn
属性,可以求z
对x
的梯度x.grad
。注意到这个过程是不需要y
的requires_grad
也设为True
的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
x = Variable(torch.randn(2,2), requires_grad = True) y = Variable(torch.randn(2,2), requires_grad = False) z = (x+y).sum() z.backward() print(z.requires_grad) print(z.grad_fn) print(x.grad) >>> True >>> <AddBackward0 object at 0x7f25ef17eeb0> >>> tensor([[1., 1.], [1., 1.]]) |
同样的道理,z=model(x)
要计算x的梯度当然也不需要model
中的参数设置requires_grad
。于是思路就是好好研究一下EncoderClassifier
类的几个方法,究竟是哪一步导致grad
没有了。EncoderClassifier
类的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class EncoderClassifier(Pretrained): MODULES_NEEDED = [ "compute_features", "mean_var_norm", "embedding_model", "classifier", ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def encode_batch(self, wavs, wav_lens=None, normalize=False) def classify_batch(self, wavs, wav_lens=None) def classify_file(self, path) |
总的逻辑过程在classify_batch(self, wavs, wav_lens=None)
和classify_file(self, path)
中。这两个方法的区别是,前者的输入是一个准备好了的audio向量,而后者的输入是一个音频文件的路径。在craft对抗样本的时候主要使用classify_batch()
。
1 2 3 4 5 6 7 8 9 10 |
def classify_batch(self, wavs, wav_lens=None): """Performs classification on the top of the encoded features. """ emb = self.encode_batch(wavs, wav_lens) out_prob = self.modules.classifier(emb).squeeze(1) score, index = torch.max(out_prob, dim=-1) text_lab = self.hparams.label_encoder.decode_torch(index) return out_prob, score, index, text_lab |
第5行用前端(Front-end)编码器(Xvector之类的)将输入wavs
编码成embeddings,第6行将emb
输入后端(Back-end)分类器,得到各个label的概率分布。第7,8行得到判别结果。我们需要的是out_prob
对wavs
的梯度,但是结果就是None
。
那么问题可以这么来排除,可以将emb
设为叶子结点,看梯度能否从out_prob
传播回到emb
,如果可以,那么就可以排除self.modules.classifier(emb).squeeze(1)
的问题。
1 2 3 4 5 6 7 8 9 10 11 12 |
wavs, _ = torchaudio.load(wav_list[0]) wav_lens = None emb = Variable(classifier.encode_batch(wavs, wav_lens), requires_grad=True) out_prob = classifier.modules.classifier(emb).squeeze(1) cost = out_prob.mean() cost.backward() print(emb.grad is not None) >>> True |
emb.grad
是有的,说明self.modules.classifier(emb).squeeze(1)
没有问题。那么问题只能是在self.encode_batch(wavs, wav_lens)
了,看看它的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def encode_batch(self, wavs, wav_lens=None, normalize=False): """Encodes the input audio into a single vector embedding. """ # Leaving out unimportant parts # ...... # Computing features and embeddings feats = self.modules.compute_features(wavs) feats = self.modules.mean_var_norm(feats, wav_lens) embeddings = self.modules.embedding_model(feats, wav_lens) # Leaving out unimportant parts # ...... return embeddings |
主要问题集中在这8,9,10三行。利用上面同样的问题排除方法,一个一个函数排除,最后可以发现问题出在self.modules.compute_features(wavs)
这个方法中。变量只要进过这个方法,梯度就再也无法反向传播了。这个方法是通过hparams_file
参数在.yaml
文件中定义的。于是去打开hparams_inference.yaml
一看,发现这个方法的定义如下:
1 2 3 4 |
# Feature extraction compute_features: !new:speechbrain.lobes.features.Fbank n_mels: !ref <n_mels> |
查阅SpeechBrain的官方文档,可以找到speechbrain.lobes.features.Fbank
的源码,可参考 SpeechBrain – Source code for speechbrain.lobes.features
问题所在
于是就能找到问题所在:震惊!居然有个torch.no_grad()
。这能梯度传播就有鬼了呀。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Fbank(torch.nn.Module): """Generate features for input to the speech pipeline. """ # Leaving out unimportant parts # ...... def forward(self, wav): """Returns a set of features generated from the input waveforms. """ with torch.no_grad(): STFT = self.compute_STFT(wav) mag = spectral_magnitude(STFT) fbanks = self.compute_fbanks(mag) if self.deltas: delta1 = self.compute_deltas(fbanks) delta2 = self.compute_deltas(delta1) fbanks = torch.cat([fbanks, delta1, delta2], dim=2) if self.context: fbanks = self.context_window(fbanks) return fbanks |
以下摘自PyTorch文档:
- Context-manager that disabled gradient calculation.
-
Disabling gradient calculation is useful for inference, when you are sure that you will not call
Tensor.backward()
. It will reduce memory consumption for computations that would otherwise haverequires_grad=True
. -
In this mode, the result of every computation will have
requires_grad=False
, even when the inputs haverequires_grad=True
.
到这一步,实际上就能根据关键词找到同道中人了,也有人遇到了同样的问题,并且他跟我的分析非常相似Tracking gradient w.r.t. to input audio sample: Fbank breaks computation graph。虽然他也没有很优雅的解决办法。但至少可以感觉到问题的就在这里。
问题找到,接下来就是解决问题。第一个想法,SpeechBrain有没有能求grad的FilterBank函数?查一下文档还真有,叫speechbrain.processing.features.Filterbank
。但是仔细看看的话就会发现,speechbrain.lobes.features.Fbank
实际上用到了它。
1 2 3 4 5 6 7 8 9 10 |
import torch from speechbrain.processing.features import ( STFT, spectral_magnitude, Filterbank, DCT, Deltas, ContextWindow, ) |
意思就是,只把speechbrain.lobes.features.Fbank
换成speechbrain.processing.features.Filterbank
是不可行的,因为speechbrain.lobes.features.Fbank
里面还做了一些其他的操作。那最后的办法就是——自力更生,改写这个类的forward
方法。修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Fbank(torch.nn.Module): """Generate features for input to the speech pipeline. """ def __init__( ... allow_grad = False, # modified for autograd ): self.allow_grad = allow_grad ... def forward(self, wav): """Returns a set of features generated from the input waveforms. """ if self.allow_grad: # modified for autograd STFT = self.compute_STFT(wav) ... else: with torch.no_grad(): STFT = self.compute_STFT(wav) ... return fbanks |
这个类在一般情况下与原来一模一样,只有在实例化的时候将参数allow_grad
显式地设为True
,这个类就可以支持梯度反向传播。
接下来就是在.yaml
文件中定义新的compute_features
,如下:
1 2 3 4 5 |
# Feature extraction compute_features: !new:new_feature.Fbank # enable grad n_mels: !ref <n_mels> allow_grad: True |
注意别忘了allow_grad
。
Done!