填坑cmvn

cmvn : 计算mean, variance, count of frame这三个量。上次并没有详细分析计算cmvn的细节,这里把这个坑填了。

启动准备

tools/compute_cmvn_stats.py

python -m ipdb tools/compute_cmvn_stats.py \
--num_workers 0 --train_config $train_config \
--in_scp $wave_data/$train_set/wav.scp_$minsec \
--out_cmvn $wave_data/$train_set/global_cmvn

相关的输入的参数为:

  • Namespace(in_scp='data/train/wav.scp_0.1',
  • log_interval=1000,
  • num_workers=0, 【这是为了方便ipdb调试,普通情况下可以设置为16。。。】
  • out_cmvn='data/train/global_cmvn',
  • train_config='conf/train_conformer_bidecoder_large.yaml')

第一个是关于wav的,csj这里,按照>=0.1s过滤之后,剩余932,577行。每一行的内容是:

S04F1603_00000.308_00005.321
/workspace/asr/wenet/examples/csj/s0/data/wav/S04F1603.wav_00000.308_00005.321.wav

两列,左边是id,右边是wav文件的绝对路径。第四个是输出的global_cmvn文件。第五个是配置文件。读取之后,得到的是:

ipdb> configs
{'encoder':'conformer','encoder_conf': {'output_size':512,'attention_heads':8,
'linear_units':2048,'num_blocks':12,'dropout_rate':0.1,'positional_dropout_rate':0.1,
'attention_dropout_rate':0.1,'input_layer':'conv2d','normalize_before':True,
'cnn_module_kernel':31,'use_cnn_module':True,'activation_type':'swish',
'pos_enc_layer_type':'rel_pos','selfattention_layer_type':'rel_selfattn',
'cnn_module_norm':'layer_norm'},'decoder':'bitransformer','decoder_conf':
{'attention_heads':8,'linear_units':2048,'num_blocks':3,'r_num_blocks':3,
'dropout_rate':0.1,'positional_dropout_rate':0.1,'self_attention_dropout_rate':0.1,
'src_attention_dropout_rate':0.1},'model_conf': {'ctc_weight':0.3,'lsm_weight':0.1,
'length_normalized_loss':False,'reverse_weight':0.3},'dataset_conf': {'filter_conf':
{'max_length':2000,'min_length':50,'token_max_length':400,'token_min_length':1,
'min_output_input_ratio':0.05,'max_output_input_ratio':10.0},'resample_conf':
{'resample_rate':16000},'speed_perturb':True,'fbank_conf': {'num_mel_bins':80,
'frame_shift':10,'frame_length':25,'dither':1.0},'spec_aug':True,'spec_aug_conf':
{'num_t_mask':3,'num_f_mask':2,'max_t':50,'max_f':10},'shuffle':True,
'shuffle_conf': {'shuffle_size':1500},'sort':True,'sort_conf': {'sort_size':500},
'batch_conf': {'batch_type':'static','batch_size':12}},'grad_clip':5,'accum_grad':4,
'max_epoch':200,'log_interval':100,'optim':'adam','optim_conf': {'lr':0.002},
'scheduler':'warmuplr','scheduler_conf': {'warmup_steps':50000}}

重要的是:

feat_dim = configs['dataset_conf']['fbank_conf']['num_mel_bins'] # 80 dimensions

主要流程


cmvn的主要流程

然后是最核心的对于一个batch计算相关的mean, variance和frame数量的代码:


把一个batch内部的20条wav的梅尔谱结果叠加起来

上面的脑图有几个点,其一,waveform * (1<<15),相当于把取值放大了32768倍。例如:

ipdb> waveform, sample_rate
(tensor([[-0.0012, -0.0011, -0.0011,  ..., -0.0007,  0.0002,  0.0010]]), 16000)
ipdb> waveform.shape
torch.Size([1, 28592])
ipdb> 1 << 15
32768
ipdb> n
> /workspace/asr/wenet/examples/csj/s0/tools/compute_cmvn_stats.py(51)__call__()
50             waveform = waveform * (1 << 15)
---> 51             if self.resample_rate != 0 and self.resample_rate != sample_rate:
52                 resample_rate = self.resample_rate

ipdb> waveform
tensor([[-38., -36., -36.,  ..., -22.,   5.,  32.]])

其二,调用kaldi.fbank的输出,mat的shape为[177, 80],取值例如:

ipdb> mat.shape
torch.Size([177, 80])
ipdb> mat
tensor([[11.1986, 12.1797, 12.5807,  ..., 19.6952, 19.5846, 17.6198],
[11.2527, 11.5830, 12.3206,  ..., 20.5928, 19.4825, 16.8303],
[10.2722,  8.4219, 11.0196,  ..., 17.4477, 15.4190, 12.8465],
...,
[10.9639, 11.8858, 10.9370,  ..., 11.4633, 11.9100, 10.6619],
[10.2542, 11.2394, 10.0596,  ..., 12.2695, 12.2623, 10.2948],
[ 9.0662,  9.5829,  9.5333,  ..., 12.2994, 11.4364, 10.0366]])

其三,获取mean_stat之后,其实是简单相加(80个维度),取值例如:

--->61             mean_stat += torch.sum(mat, axis=0)
ipdb>mean_stat
tensor([1668.3152, 1652.7643, 1792.3387, 2019.7705, 2261.6038, 2389.7666,
...,
2553.3887, 2138.4426])

其四,获取var_stat的是:

> /workspace/asr/wenet/examples/csj/s0/tools/compute_cmvn_stats.py(63)__call__()
62             var_stat += torch.sum(torch.square(mat), axis=0)
---> 63             number += mat.shape[0]
64         return number, mean_stat, var_stat

ipdb> var_stat
tensor([16120.1914, 15886.4385, 18786.2188, 24513.6582, 31858.9766, 35987.9102,
...,
38383.5391, 27215.1484])

例如,其中一个值的由来为:

ipdb> torch.square(mat)[:,0]
tensor([125.4096, 126.6239, 105.5190, 138.9763, 142.1694, 167.2804, 134.3045,
...,
71.5625,  75.5625,  85.0866,  82.4624,  72.6762, 104.7350, 120.2076,
105.1492,  82.1951])
ipdb> sum(torch.square(mat)[:,0])


recall主流程

上面计算之后,其实得到的是(累计和):

  • mean,(1,80),80维度的向量,类似:, 其中N= frame count,是一个frame的80个维度的向量;

  • variance, (1, 80),类似:, 其中N= frame count,是一个frame的80个维度的向量;

但看i=0的时候的计算:

ipdb> p count
176228620
ipdb> n
> /workspace/asr/wenet/wenet/utils/cmvn.py(38)_load_json_cmvn()
37     for i in range(len(means)):
---> 38         means[i] /= count
# count=N, means[i]除以N之后, 就是这个维度的均值了(一共80个维度)

39         variance[i] = variance[i] / count - means[i] * means[i]

---> 40         if variance[i] < 1.0e-20:
41             variance[i] = 1.0e-20
---> 42         variance[i] = 1.0 / math.sqrt(variance[i])
43     cmvn = np.array([means, variance])

然后,上面执行的是:


至此,cmvn就算搞定了。

目前的位置

模型初始化完毕,往后就是train部分和评估部分了

然后是参数的规模:

135M参数

the number of model params: 135,154,810 = 135M参数。

load checkpoint

导入checkpoint

系统会先保存一份当前model,然后读取给定的checkpoint(如果有的话);并读取该checkpoint下达到的如下信息:

{'cv_loss': 2.504995254788169, 'epoch': 119, 'lr': 0.0009465458080551907, 'step': 225100}

当前start_epoch=120,因为之前的checkpoint是到epoch=119。

optimizer

往后比较重要的是优化器的构造:


优化器
构造的是:
ipdb> optimizer
Adam (
Parameter Group 0
amsgrad: False
betas: (0.9, 0.999)
eps: 1e-08
lr: 0.002
weight_decay: 0
)

其次是关于学习率的规划器:


学习率变化的规划器

得到的是:

WarmupLR(warmup_steps=50000)

data_loader: one batch

【wenet/dataset/processor.py里面的方法是核心】

root@1032f8d48655:/workspace/asr/wenet/examples/csj/s0#
vi /opt/conda/lib/python3.8/site-packages/torch/utils/data/dataloader.py +522


看细节,跟踪_next_data()方法

num_workers=0

prefetch, default=2

class Processor's __iter__, append "import ipdb; ipdb.set_trace()"

回顾一下之前的分析,


11个关口:

dataset=DataList(lists, shuffle=True, partition=True)

  • processor.parse_raw

  • processor.tokenize [切词]

  • processor.filter

  • processor.resample

  • processor.speed_perturb

  • processor.compute_fbank [计算filter banks]

  • processor.spec_aug [spectrum augment-谱增强]

  • processor.shuffle

  • processor.sort

  • processor.batch

  • processor.padding

padding: 11

ipdb>self.f,self.args,self.kw
(,(),{})

调用torch.nn.utils.rnn.py中的pad_sequence方法来搞padding

例如:feats补0.0000

ipdb>padded_feats
tensor([[[ 6.9371,  7.1257,  7.7550,  ..., 11.1890, 11.4491,  9.2112],
[11.5080, 12.6101, 12.9887,  ..., 12.0408, 12.3439, 10.0229],
[12.9132, 14.4461, 15.3878,  ..., 11.3328, 11.0440,  9.5172],
...,
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000]],

...

[[ 8.3853,  9.5206, 10.3255,  ..., 14.2314, 14.4264, 12.7104],
[10.3463,  9.3440,  9.7137,  ..., 15.6107, 15.8510, 13.8745],
[11.0108, 11.0644, 12.7715,  ..., 15.2525, 15.1387, 13.0166],
...,
...,
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000]]])

labels补-1

ipdb>padding_labels
tensor([[1554, 4019, 4112,   -1,   -1,   -1,   -1],
[2248, 1741, 1867,   -1,   -1,   -1,   -1],
[1885, 2161, 1741, 1908, 1609, 1548,   -1],
[1516, 1845, 2161,   -1,   -1,   -1,   -1],
[5366, 2616, 1741,   -1,   -1,   -1,   -1],
[1762, 1527, 1548, 5375, 1816,   -1,   -1],
[3183, 1527, 1885, 1670,   -1,   -1,   -1],
[1715, 1737, 1527, 1548, 1908, 1845, 1575],
[1677, 1952, 1741,   -1,   -1,   -1,   -1],
[1554, 1609, 1952, 1867,   -1,   -1,   -1],
[3310, 3296, 1741, 1867,   -1,   -1,   -1],
[2307, 1554, 1870,   -1,   -1,   -1,   -1]])

batch: 10

接着调用:

ipdb> self.source, self.f, self.args, self.kw
(,
, (), {'batch_type': 'static', 'batch_size': 12})

static_batch,从data中取batch_size=12个数据,并发送出来。


静态batch

sort: 9

ipdb> self.source, self.f, self.args, self.kw
(,
, (), {'sort_size': 500})

根据feature length排序,shuffle之后,batch之前。从而可以把utts的相同length的样本,放入一个batch。

sort_size < shuffle_size


sort,根据frame长度

shuffle: 8

ipdb> self.source, self.f, self.args, self.kw
(,
, (), {'shuffle_size': 1500})

10000个数据,搞一次shuffle,重新洗牌


shuffle, 重新洗牌

spec_aug: 7

声谱增强!

ipdb> self.source, self.f, self.args, self.kw
(,
, (),
{'num_t_mask': 3, 'num_f_mask': 2, 'max_t': 50, 'max_f': 10})

time mask, y[start:end, :]=0, start.time, end.time. length=random.[1, 50]

freq mask, y[:, start:end]=0, start.freq, end.freq, length=random.[1, 10]

随机删除一些输入,属于人工增加一些噪声。。。

sample['feat'] 是对这个的处理。

80维度的梅尔谱。


上面的beg, length, end的取值都是示例

compute_fbank: 6

ipdb> self.source, self.f, self.args, self.kw
(,
, (),
{'num_mel_bins': 80, 'frame_shift': 10,
'frame_length': 25, 'dither': 1.0})

调用的是kaldi.fbank

sample里面有,'sample_rate', 'wav' = waveform, 'key', 'label',

fbank:

  • waveform

  • num_mel_bins=80

  • frame_length

  • frame_shift

  • dither

  • energy_floor

  • sample_frequency=sample_rate

yield, key=sample['key'], label=sample['label'], feat=mat


搞出80维度的梅尔谱,调用kaldi.fbank方法

speed_perturb: 5

ipdb> self.source, self.f, self.args, self.kw
(,
, (), {})

speeds=[0.9, 1.0, 1.1]

改变wav的“语速”。

wav, _ = torchaudio.sox_effects.apply_effects_tensor(

waveform,

sample_rate,

[['speed', str(speed)], ['rate', str(sample_rate)]])

通过这个方法,就把原始语音的语速给修改了。


修改语速(0.9 or 1.1)

resample: 4

ipdb> self.source, self.f, self.args, self.kw
(,
, (), {'resample_rate': 16000})

只有当新指定的sample_rate和原来不相同的时候,这个才有效:

sample['wav'] = torchaudio.transforms.Resample(

orig_freq=sample_rate,

new_freq = resample_rate) (waveform)


重新采样,如果需要的话

filter: 3

    ipdb> self.source, self.f, self.args, self.kw
    (,
    , (),
    {'max_length': 2000, 'min_length': 50, 'token_max_length': 400,
    'token_min_length': 1, 'min_output_input_ratio': 0.05,
    'max_output_input_ratio': 10.0})
  • max-length, 100 frames -> 1s; max=2000 frames = 20 seconds
  • min-length, 是关于wav的长度的;min=50 frames = 0.5 seconds

  • token-max-length, 400

  • token-min-length,是关于text的长度的;1

  • min-output-input-ratio= len(sample['label']) / num_frames,不能相差太大。。。

  • max-output-input-ratio

{'filter_conf':
{'max_length': 2000,
'min_length': 50,
'token_max_length': 400,
'token_min_length': 1,
'min_output_input_ratio': 0.05,
'max_output_input_ratio': 10.0},

过滤三策略

三个过滤策略。


tokenize: 2

    ipdb> self.source, self.f, self.args, self.kw
    (,
    ,
    ({'': 0, '': 1, '...'': 5501},
    None, None, False), {})

是基于tokenizer来对文本序列切词的

bpe_model = None,所以是unigram分词(类似于character-based)


切词的脑图,示例

parse_raw: 1

ipdb> self.source, self.f, self.args, self.kw
(,
, (), {})

读取raw wav,返回

  • key=key, (wav.id)

  • txt = txt,文本,原始json里面的文本

  • wav = waveform,这是从torchaudio.load(wav_file)得到的。

  • sample_rate = sample_rate,也是torchaudio.load(wav_file)得到的。

先执行这一步!

一行数据类似:

{'src':
'{"key": "S05F1072_00343.690_00344.688",
"wav":
"/workspace/asr/wenet/examples/csj/s0/data/wav/S05F1072.wav_00343.690_00344.688.wav",
"txt": "で"}',
'rank': 0,
'world_size': 1,
'worker_id': 0,
'num_workers': 1}

脑图为:


读取raw data

batch 搞定了。

[wenet/dataset/processor.py]

待续了,总算把最重要的数据准备阶段看清了。