diff --git a/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/.gitattributes b/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..a6344aac8c09253b3b630fb776ae94478aa0275b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/.gitattributes @@ -0,0 +1,35 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text diff --git a/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/README.md b/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/README.md new file mode 100644 index 0000000000000000000000000000000000000000..154df8298fab5ecf322016157858e08cd1bccbe1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/README.md @@ -0,0 +1,3 @@ +--- +license: apache-2.0 +--- diff --git a/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/avg_model.pt b/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/avg_model.pt new file mode 100644 index 0000000000000000000000000000000000000000..591e989f57c7da642690ce550caa7a4220d64119 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/avg_model.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9314cd0184d3823c70a2518d354397bf049832f90fb1d7584ff6c0b0d8b152a +size 55525357 diff --git a/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/config.yaml b/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3b2b74882efa1d2df9cb851fd52d6385d315db75 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker-voxceleb-redimnet2-B6-LM/config.yaml @@ -0,0 +1,147 @@ +checkpoint: exp/redimnet2/expB6_v2/-LM/models/model_0.pt +data_type: shard +dataloader_args: + batch_size: 16 + drop_last: true + num_workers: 4 + pin_memory: false + prefetch_factor: 4 +dataset_args: + aug_prob: 0.0 + cmvn: false + filter: false + frontend: tfmel + num_frms: 600 + resample_rate: 16000 + sample_num_per_epoch: 0 + shuffle: true + shuffle_args: + shuffle_size: 2500 + spec_aug: false + speed_perturb: false + tfmel_args: + do_preemph: true + do_spec_aug: false + eps: 1.0e-08 + f_max: 7600 + f_min: 20 + hop_length: 160 + n_fft: 512 + n_mels: 72 + norm_signal: true + win_length: 400 +enable_amp: true +exp_dir: exp/redimnet2/expB6_v2/-LM +gpus: +- 2 +- 3 +grad_accum_steps: 8 +log_batch_interval: 100 +loss: CrossEntropyLoss +loss_args: {} +margin_scheduler: MarginScheduler +margin_update: + epoch_iter: 34125 + final_margin: 0.3 + fix_start_epoch: 0 + increase_start_epoch: 0 + increase_type: exp + initial_margin: 0.3 + update_margin: false +model: ReDimNet2Wrap +model_args: + C: 64 + F: 72 + block_1d_type: conv+att + block_2d_type: basic_resnet + causal: none + compress_tconvs: true + emb_bn: false + embed_dim: 192 + feat_dim: 72 + fm_weigthing_type: NC + global_context_att: true + group_divisor: 1 + hop_length: 160 + out_channels: 224 + pooling_func: ASTP + return_2d_output: true + spec: null + spec_in_channels: 1 + stages_setup: + - - - 1 + - 1 + - 3 + - 3 + - - - 3 + - 3 + - 64 + - - - 2 + - 1 + - 4 + - 2 + - - - 3 + - 3 + - 64 + - - - 1 + - 2 + - 5 + - 2 + - - - 3 + - 3 + - 48 + - - - 2 + - 1 + - 5 + - 1 + - - - 3 + - 3 + - 48 + - - - 1 + - 2 + - 4 + - 0.75 + - - - 3 + - 3 + - 32 + - - - 2 + - 1 + - 3 + - 0.5 + - - - 3 + - 3 + - 24 +model_init: null +noise_data: data/voxceleb/musan/lmdb +num_avg: 1 +num_epochs: 5 +optimizer: SGD +optimizer_args: + lr: 0.0001 + momentum: 0.9 + nesterov: true + weight_decay: 2.0e-05 +projection_args: + do_lm: false + easy_margin: false + embed_dim: 192 + lanbuda: 0.7 + margin_type: C + num_class: 5994 + project_type: sphereface2 + scale: 32.0 + t: 3 +reverb_data: data/voxceleb/rirs/lmdb +save_epoch_interval: 1 +scheduler: ExponentialDecrease +scheduler_args: + epoch_iter: 34125 + final_lr: 1.0e-05 + initial_lr: 0.0001 + num_epochs: 5 + scale_ratio: 4.0 + warm_from_zero: false + warm_up_epoch: 0 +seed: 42 +train_data: data/voxceleb/vox2_dev/shard.list +train_label: data/voxceleb/vox2_dev/utt2spk diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/prepare_data.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/prepare_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..5770152b098ff109db3bce9d9101c336a783bc97 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/prepare_data.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +# Copyright (c) 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +stage=-1 +stop_stage=-1 +sre_data_dir= +data=data + +. tools/parse_options.sh || exit 1 + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + mkdir -p external_tools + # Download voice activity detection model pretrained by Silero Team + wget -c https://github.com/snakers4/silero-vad/archive/refs/tags/v4.0.zip -O external_tools/silero-vad-v4.0.zip + unzip -o external_tools/silero-vad-v4.0.zip -d external_tools +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + # The meta data for SRE16 should be pre-prepared using Kaldi recipe: + # https://github.com/kaldi-asr/kaldi/tree/master/egs/sre16/v2 + for dset in swbd_sre sre sre16_major sre16_eval_enroll sre16_eval_test; do + mkdir -p ${data}/${dset} + cp ${sre_data_dir}/${dset}/wav.scp ${data}/${dset}/wav.scp + [ -f ${sre_data_dir}/${dset}/utt2spk ] && cp ${sre_data_dir}/${dset}/utt2spk ${data}/${dset}/utt2spk + [ -f ${sre_data_dir}/${dset}/spk2utt ] && cp ${sre_data_dir}/${dset}/spk2utt ${data}/${dset}/spk2utt + done +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Get vad segmentation for dataset." + # Set VAD min duration + min_duration=0.255 + for dset in swbd_sre sre16_major sre16_eval_enroll sre16_eval_test; do + python3 local/make_system_sad.py \ + --repo-path external_tools/silero-vad-4.0 \ + --scp ${data}/${dset}/wav.scp \ + --min-duration $min_duration > ${data}/${dset}/vad + done + tools/filter_scp.pl -f 2 ${data}/sre/wav.scp ${data}/swbd_sre/vad > ${data}/sre/vad + + # For PLDA training, it is better to augment the training data + python3 local/generate_sre_aug.py --ori_dir ${data}/sre \ + --aug_dir ${data}/sre_aug \ + --aug_copy_num 2 + tools/utt2spk_to_spk2utt.pl ${data}/sre_aug/utt2spk > ${data}/sre_aug/spk2utt + +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + for dset in swbd_sre; do + python3 local/utt2voice_duration.py \ + --vad_file ${data}/${dset}/vad \ + --utt2voice_dur ${data}/${dset}/utt2voice_dur + done +fi + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + # Following the Kaldi recipe: https://github.com/kaldi-asr/kaldi/blob/71f38e62cad01c3078555bfe78d0f3a527422d75/egs/sre16/v2/run.sh#L189 + # We filter out the utterances with duration less than 5s + for dset in swbd_sre; do + python3 local/filter_utt_accd_dur.py \ + --wav_scp ${data}/${dset}/wav.scp \ + --utt2voice_dur ${data}/${dset}/utt2voice_dur \ + --filter_wav_scp ${data}/${dset}/filter_wav.scp \ + --dur_thres 5.0 + mv ${data}/${dset}/wav.scp ${data}/${dset}/wav.scp.bak + mv ${data}/${dset}/filter_wav.scp ${data}/${dset}/wav.scp + done + + # Similarly, following the Kaldi recipe, + # we throw out speakers with fewer than 3 utterances. + for dset in swbd_sre; do + tools/fix_data_dir.sh ${data}/${dset} + cp ${data}/${dset}/spk2utt ${data}/${dset}/spk2utt.bak + awk '{if(NF>2){print $0}}' ${data}/${dset}/spk2utt.bak > ${data}/${dset}/spk2utt + tools/spk2utt_to_utt2spk.pl ${data}/${dset}/spk2utt > ${data}/${dset}/utt2spk + tools/fix_data_dir.sh ${data}/${dset} + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/score.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/score.sh new file mode 100644 index 0000000000000000000000000000000000000000..dbe868bc5291713cbab9dce7ae19b0d28fe17e7f --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/score.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenhzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir= +trials="trials trials_tgl trials_yue" +data=data + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "apply cosine scoring ..." + mkdir -p ${exp_dir}/scores + trials_dir=${data}/trials + for x in $trials; do + echo $x + python wespeaker/bin/score.py \ + --exp_dir ${exp_dir} \ + --eval_scp_path ${exp_dir}/embeddings/eval/xvector.scp \ + --cal_mean True \ + --cal_mean_dir ${exp_dir}/embeddings/sre16_major \ + ${trials_dir}/${x} + done +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "compute metrics (EER/minDCF) ..." + scores_dir=${exp_dir}/scores + for x in $trials; do + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${x}.score \ + 2>&1 | tee -a ${scores_dir}/sre16_cos_result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${x}.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/score_plda.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/score_plda.sh new file mode 100644 index 0000000000000000000000000000000000000000..8b2f7802b3cd6933f721d50649e278e18dfeb1e8 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/score_plda.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Copyright (c) 2023 Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +exp_dir= +trials="trials trials_tgl trials_yue" +data=data +aug_plda_data=0 + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +if [ $aug_plda_data = 0 ];then + sre_plda_data=sre +else + sre_plda_data=sre_aug +fi + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "train the plda model ..." + python wespeaker/bin/train_plda.py \ + --exp_dir ${exp_dir} \ + --scp_path ${exp_dir}/embeddings/${sre_plda_data}/xvector.scp \ + --utt2spk ${data}/${sre_plda_data}/utt2spk \ + --indim 256 \ + --iter 10 + echo "plda training finished" +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "adapt the plda model ..." + python wespeaker/bin/adapt_plda.py \ + -mo ${exp_dir}/plda \ + -ma ${exp_dir}/plda_adapt \ + -ad ${exp_dir}/embeddings/sre16_major/xvector.scp \ + -ws 0.75 \ + -as 0.25 + echo "plda adapted finished" +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "apply plda scoring ..." + mkdir -p ${exp_dir}/scores + trials_dir=${data}/trials + for x in $trials; do + echo "scoring on " $x + python wespeaker/bin/eval_plda.py \ + --enroll_scp_path ${exp_dir}/embeddings/sre16_eval_enroll/xvector.scp \ + --test_scp_path ${exp_dir}/embeddings/sre16_eval_test/xvector.scp \ + --indomain_scp ${exp_dir}/embeddings/sre16_major/xvector.scp \ + --utt2spk data/sre16_eval_enroll/utt2spk \ + --trial ${trials_dir}/${x} \ + --score_path ${exp_dir}/scores/${x}.pldascore \ + --model_path ${exp_dir}/plda_adapt + done +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "compute metrics (EER/minDCF) ..." + scores_dir=${exp_dir}/scores + for x in $trials; do + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${x}.pldascore \ + 2>&1 | tee -a ${scores_dir}/sre16_plda_result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${x}.pldascore + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/utt2voice_duration.py b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/utt2voice_duration.py new file mode 100644 index 0000000000000000000000000000000000000000..2dd7d1c137ad374c56f7495df9b563d871e70d8b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/local/utt2voice_duration.py @@ -0,0 +1,36 @@ +# Copyright (c) 2023 Zhengyang Chen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import fire +from collections import OrderedDict + + +def main(vad_file, utt2voice_dur): + utt2voice_dur_dict = OrderedDict() + + with open(vad_file, 'r') as f: + for line in f.readlines(): + segs = line.strip().split() + utt, start, end = segs[-3], float(segs[-2]), float(segs[-1]) + if utt not in utt2voice_dur_dict: + utt2voice_dur_dict[utt] = 0.0 + utt2voice_dur_dict[utt] += end - start + + with open(utt2voice_dur, 'w') as f: + for utt, duration in utt2voice_dur_dict.items(): + f.write('{} {}\n'.format(utt, duration)) + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/path.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/path.sh new file mode 100644 index 0000000000000000000000000000000000000000..b90a5154cf4a8f25b9a42851c59393f07dacb350 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/path.sh @@ -0,0 +1,5 @@ +export PATH=$PWD:$PATH + +# NOTE(kan-bayashi): Use UTF-8 in Python to avoid UnicodeDecodeError when LC_ALL=C +export PYTHONIOENCODING=UTF-8 +export PYTHONPATH=../../../:$PYTHONPATH diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..16f412114d9745e78e84f22564758d92e018cc46 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v2/run.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +# Copyright 2022 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) + +. ./path.sh || exit 1 + +stage=-1 +stop_stage=-1 + +HOST_NODE_ADDR="localhost:29400" +num_nodes=1 +job_id=2024 + +# the sre data should be prepared in kaldi format and stored in the following directory +# only wav.scp, utt2spk and spk2utt files are needed +sre_data_dir=sre_data_dir +data=data +data_type="shard" # shard/raw +# whether augment the PLDA data +aug_plda_data=0 + +config=conf/resnet.yaml +exp_dir=exp/ResNet34-TSTP-emb256-fbank40-num_frms200-aug0.6-spFalse-saFalse-Softmax-SGD-epoch150 +gpus="[0,1]" +num_avg=10 +checkpoint= + +trials="trials trials_tgl trials_yue" + +. tools/parse_options.sh || exit 1 + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Prepare datasets ..." + ./local/prepare_data.sh --stage 2 --stop_stage 5 --sre_data_dir ${sre_data_dir} --data ${data} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Convert train data to ${data_type}..." + for dset in swbd_sre; do + python tools/make_shard_list.py --num_utts_per_shard 1000 \ + --num_threads 16 \ + --prefix shards \ + --shuffle \ + --vad_file ${data}/$dset/vad \ + ${data}/$dset/wav.scp ${data}/$dset/utt2spk \ + ${data}/$dset/shards ${data}/$dset/shard.list + done + + echo "Convert data for PLDA backend training and evaluation to raw format..." + if [ $aug_plda_data = 0 ];then + sre_plda_data=sre + else + sre_plda_data=sre_aug + fi + for dset in ${sre_plda_data} sre16_major sre16_eval_enroll sre16_eval_test; do + python tools/make_raw_list.py --vad_file ${data}/$dset/vad \ + ${data}/$dset/wav.scp \ + ${data}/$dset/utt2spk ${data}/$dset/raw.list + + done + # Convert all musan data to LMDB + python tools/make_lmdb.py ${data}/musan/wav.scp ${data}/musan/lmdb + # Convert all rirs data to LMDB + python tools/make_lmdb.py ${data}/rirs/wav.scp ${data}/rirs/lmdb +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Start training ..." + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/bin/train.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + --data_type "${data_type}" \ + --train_data ${data}/swbd_sre/${data_type}.list \ + --train_label ${data}/swbd_sre/utt2spk \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + ${checkpoint:+--checkpoint $checkpoint} +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Do model average ..." + avg_model=$exp_dir/models/avg_model.pt + python wespeaker/bin/average_model.py \ + --dst_model $avg_model \ + --src_path $exp_dir/models \ + --num ${num_avg} + + model_path=$avg_model + if [[ $config == *repvgg*.yaml ]]; then + echo "convert repvgg model ..." + python wespeaker/models/convert_repvgg.py \ + --config $exp_dir/config.yaml \ + --load $avg_model \ + --save $exp_dir/models/convert_model.pt + model_path=$exp_dir/models/convert_model.pt + fi + + echo "Extract embeddings ..." + local/extract_sre.sh \ + --exp_dir $exp_dir --model_path $model_path \ + --nj 32 --gpus $gpus --data_type raw --data ${data} \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + --aug_plda_data ${aug_plda_data} +fi + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "Score using Cosine Distance..." + local/score.sh \ + --stage 1 --stop-stage 2 \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "Score with adapted PLDA ..." + local/score_plda.sh \ + --stage 1 --stop-stage 4 \ + --data ${data} \ + --exp_dir $exp_dir \ + --aug_plda_data ${aug_plda_data} \ + --trials "$trials" +fi + +if [ ${stage} -le 7 ] && [ ${stop_stage} -ge 7 ]; then + echo "Export the best model ..." + python wespeaker/bin/export_jit.py \ + --config $exp_dir/config.yaml \ + --checkpoint $exp_dir/models/avg_model.pt \ + --output_file $exp_dir/models/final.zip +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/README b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/README new file mode 100644 index 0000000000000000000000000000000000000000..da92aa0354bfc8f440525d6ebe09805bc7896f0e --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/README @@ -0,0 +1,29 @@ +Changed a little in make_system_sad.py to make split a large data set in parts +when extracting VAD. It took ages to start otherwise and this will also be +helpful in case there is a crash since output is saved after each part instead +of after the whole set. + +# We use some scripts from Kaldi (combine_data.sh and fix_data_dir.sh) + +# This should not be needed anymore. +# ln -s $KALDI_ROOT/egs/wsj/s5/utils +# export PATH=$PATH:$(pwd)/utils/ # This is necessary since some Kaldi scripts assume other Kaldi scripts exists in the path. +#export PATH=$PATH:$KALDI_ROOT/ + + +CTS + spk / utt +Org. data 6867 / 605760 +After VAD 6867 / 605704 +After removing T < 5s 6867 / 604774 +After removing utt/spk < 3 6867 / 604774 + +VOX + spk / utt +Org. data 7245 / 1245525 +After VAD 7245 / 1245469 +After removing T < 5s 7245 / 816385 +After removing utt/spk < 3 7245 / 816385 + +Total +After removing utt/spk < 3 14112 / 1421159 \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2482cddd8dc74e93b23204a9e1e23b43b3f6e059 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/README.md @@ -0,0 +1,99 @@ +### Main differences from ../v2 +* The training data is the CTS superset plus VoxCeleb with GSM codec +* The test data is SRE16, SRE18, and SRE21 +* Preprocessing of embeddings before backend/scoring is supported + +### Important +Similarly to ../v2, this recipe uses silero vad https://github.com/snakers4/silero-vad +downloaded from here https://github.com/snakers4/silero-vad/archive/refs/tags/v4.0.zip +If you intended to use this recipe for an evaluation/competition, make sure to check that +it is allowed to use the data that has been used to train Silero. + +### Instructions +* Set the paths in stage 1. The variable ```sre_data_dir``` is assumed to be prepared by + Kaldi (https://github.com/kaldi-asr/kaldi/tree/master/egs/sre16/v2). + Only the eval and unlabeled (major) data of sre16 is taken from there. + ```voxceleb_dir``` is the path to voxceleb prepared by wespeaker (```../../voxceleb/v2```). + If you set it to "" (empty string), the preparation will be run here. For the other datasets, + the path to the folder provided by LDA should be provided. The relevant LDC numbers and + file names of the data can be seen in the script. If you don't have + one or two of the "eval/dev" sets of "sre16", "sre18" or "sre21" and not specify it, you may + have to comment it from some more places in order to avoided crashes. (Eventually + the script will hopefully be made more robust to this.) + If you don't have the CTS superset data, you can skip stage 5 in ```local/prepare_data.sh``` + and instead replace the CTS data it with some other data, e.g., the training data prepared in ```../v2``` + If so, it is probably the easiest to name this data "CTS" since this name is assumed later + in the recipe. +* Select which torchrun command to use in stage 3. The first line + (currently commented) is for "single-node, multi-worker" (one + pytorch job per machine). The second line is for "Stacked + single-node multi-worker" (more than one pytorch job may be + submitted to the same node in your cluster.) See + https://pytorch.org/docs/stable/elastic/run.html for explanations. +* Stage 3 (training) and stage 4 (embedding extraction) need GPU. You may have + to arrange how to run these parts based on your environment. + + +### Explanation of embedding processing + +The code supports flexible combinations of embedding processing steps, such as length-norm and LDA. +A processing chain is specified e.g., as follows +``` +mean-subtract --scp $mean1_scp | length-norm | lda --scp $lda_scp --utt2spk $utt2spk --dim $lda_dim | length-norm" +``` +The script ```wespeaker/bin/prep_embd_proc.py``` takes such a processing chain as input, loops through the processing steps (separated by ```|```), calculates +the necessary processing parameters (means, lda transforms etc.) and stores the whole processing chain with parameters in +pickle format. The parameters for each step will be calculated sequentially and the data specified for the parameter estimation of a step will +be processed by the earlier steps. Therefore the data for the different steps can be different. For example when estimating LDA in the above chain, the data given by ```$lda_scp``` will first be processed by ```mean-subtract``` whose parameters were estimated by ```$mean1_scp``` which could be a different dataset. +In scenarios where unlabeled domain adaptation data is available, we want to use this data for the first mean subtraction while still using the out domain data for LDA estimation. This CANNOT be achieved by specifying the processing chain +``` +mean-subtract --scp $indomain_scp | length-norm | lda --scp $lda_scp --utt2spk $utt2spk --dim $lda_dim | length-norm +``` +since this would have the consequence that in LDA estimation, the data (```$lda_scp```) would be subjected to mean subtraction +using the mean of the indomain data (```$indomain_scp```). To solve this, we have an additional script ```wespeaker/bin/update_embd_proc.py``` used as follows +``` +new_link="mean-subtract --scp $indomain_scp" +python wespeaker/bin/update_embd_proc.py --in_path $preprocessing_path_cts_aug --out_path $preprocessing_path_sre18_unlab --link_no_to_remove 0 --new_link "$new_link" +``` +where ```$preprocessing_path_cts_aug``` is the path to the pickled original processing chain and ```$preprocessing_path_sre18_unlab``` is the path to the new pickled processing chain. +The script will remove link 0, e.g. ```mean-subtract --scp $mean1_scp``` and replace it with ```mean-subtract --scp $indomain_scp```. + + +### Regarding extractor training data pruning + +Similarly to ```../v2``` and Kaldi's sre16 recipe, we discard some of the training utterances based on duration as well as training speakers based on their number of utterances. +This is controlled in stage 9 of ```local/prepare_data.sh```. It is quite flexible but currently a bit messy and some consequences of the settings are not obvious. Therefore some explanation is provided here. +There are three "blocks" in stage 9: +* The first block discards all utterances shorter or equal to some specified duration (currently set to 5s) according to VOICED DURATION. +* The second block discards all utterances shorter or equal to some specified duration (currently set to 5s) according to TOTAL DURATION, i.e., ignoring VAD info. +* The third block discards all speakers that has less than or equal to a specified number of utterances. (Currently set to 2, i.e. speaker with 3 or more utterances are kept.) +It is possible to set the thresholds differently for the different sets. IMPORTANT: The pruning in block 1 is based on ```data/data_set_name/utt2voice_dur``` which is calculated +from the VAD info, so if a recording does not have any speech, it will not be present in utt2voice_dur and therefore discarded in this block even if the duration threshold is +set to e.g. -1. If we want such utterances to be kept for one set we should not run this block for the set (as currently is the case for voxceleb). The current setup is as follows: + 1. Apply block one to CTS but not Voxceleb + 2. Apply block two to Voxceleb but not CTS. (Applying this stage to CTS would not have an effect if the thresholds are the same since the total duration is always larger or equal to the voiced duration.) + 3. Apply stage three to both CTS and VoxCeleb. + + This means Voxceleb recordings are kept even if they have no speech accordng to VAD. The later shard creation stage applies VAD if available, otherwise keeps the file as it is. So Voxceleb recording with no speech according to VAD will NOT be discarded (but there are only around 70 of them which is unlikely to have any effect on the trained system.). Also, there is a risk that pruning according to total duration while applying VAD in shard creation could result in recordings shorter than "num_frms". These will be zero padded at training time so there will be no crash but this is probably also suboptimal. +These is setting are arguably somewhat weird. Applying block one also to voxceleb (and not using block two at all) would be more reasonable but it seems to degrade the performance due to discarding too many files. A better solution than the current would be to try with smaller thresholds than 5s but we have had not had time to explore this yet. Also, it would be reasonable to discard recordings with no speech according to VAD in the shard creation stage. However, when no VAD is available for a file, the shard creation code does not know whether this is because no speech was detected for this file according to VAD, or because VAD was not ran for this file. Since we want to have the possibility to keep recordings for which the latter is the case, we have it this way (it could for example be considered not to use VAD for voxceleb at all, in which case we need to avoid discarding these files at the shard creation stage). A more flexible and clear solution is needed and we will work on this for future updates. + + +### Some data statistics +| | CTS #utt | CTS #spk | CTS #utt | CTS #spk | comment| +| --- | --- | --- | --- | --- | --- | +|Original data | 605760 | 6867 | 1245525 | 7245 | | +|exclud recording with nospeech acording to VAD| 605704 | 6867 | 1245455 | 7245 | VAD is a bit random so these numbers could vary slightly, especially for voxceleb. | +|After filtering according voiced duration | 604774 | 6867 | 816411 | 7245 | Accordingly, here too. We don't use this for voxceleb in the current settings. | +|After filtering according total duration | - | - | 868326 | 7245 | Haven't checked this for CTS. + +No speaker are discarded in block three with the current setting. + + +### Things to explore +Very few things have been tuned. For example the following could be low-hanging fruits: +* The above mentioned pruning rules +* Utterance durations of the training segments. +* Shall voxceleb be included? Is applying the GSM codec a good idea? (Note that GSM codec is applied in the data preparation stage while augmentation is applied at training time, i.e, GSM codec comes before augmentations. This is not so realistic, since in reality noise and reverberation comes before the data is recorded and encoded. However, it is consistent with CTS where we also apply augmentations at the already encoded audio since it was encoded at recording time.) +* The other architectures. + +We will tune this futher in the future. We are also happy to hear about any such results obtained by others. \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/conf/resnet.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/conf/resnet.yaml new file mode 100644 index 0000000000000000000000000000000000000000..83294c06c0ccd8f8a4d7f12ff13fa43c491ad7e4 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/conf/resnet.yaml @@ -0,0 +1,81 @@ +### train configuration + +exp_dir: exp/ResNet34-TSTP-emb256-fbank40-num_frms200-aug0.6-spFalse-saFalse-Softmax-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 256 + num_workers: 7 # Total number of cores will be (this +1)*num_gpus + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 780000 + shuffle: True + shuffle_args: + shuffle_size: 1500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 300 + resample_rate: 8000 + speed_perturb: False + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 64 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ResNet34 # ResNet18, ResNet34, ResNet50, ResNet101, ResNet152 +model_init: null +model_args: + feat_dim: 64 + embed_dim: 256 + pooling_func: "TSTP" # TSTP, ASTP, MQMHASTP + two_emb_layer: False +projection_args: + project_type: "softmax" # add_margin, arc_margin, sphere, softmax, arc_margin_intertopk_subcenter + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/create_preproc_embd_lists.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/create_preproc_embd_lists.sh new file mode 100644 index 0000000000000000000000000000000000000000..8f868ec0e35f0d7c0a734fa34972ba70f02b3c6c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/create_preproc_embd_lists.sh @@ -0,0 +1,119 @@ +#!/bin/bash + +# Copyright (c) 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# The preprocessed embeddings are already stored but we need to create the lists +# as score.sh wants them. + +exp_dir=$1 +data=data + +# We have three different preprocessors for which we need to prepare the lists +# embd_proc_cts_aug.pkl # LDA and cts_aug mean subtraction +# embd_proc_sre16_major.pkl # LDA and sre16_major mean subtracion (Only used for SRE16) +# embd_proc_sre18_dev_unlabeled.pkl # LDA and sre18_dev_unlabeled mean subtracion (Only used for SRE18) + + +### !!! +# Note that xvector2 is only a hack for BUT + +################################################################## +# CTS AUG for all sets +echo "mean vector of enroll" +python tools/vector_mean.py \ + --spk2utt ${data}/sre16/eval/enrollment/spk2utt \ + --xvector_scp $exp_dir/embeddings/sre16/eval/enrollment/xvector_proc_embd_proc_cts_aug.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre16/eval/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.ark + +python tools/vector_mean.py \ + --spk2utt ${data}/sre18/dev/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre18/dev/enrollment/xvector_proc_embd_proc_cts_aug.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre18/dev/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.ark + +python tools/vector_mean.py \ + --spk2utt ${data}/sre18/eval/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre18/eval/enrollment/xvector_proc_embd_proc_cts_aug.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre18/eval/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.ark + +python tools/vector_mean.py \ + --spk2utt ${data}/sre21/dev/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre21/dev/enrollment/xvector_proc_embd_proc_cts_aug.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre21/dev/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.ark + +python tools/vector_mean.py \ + --spk2utt ${data}/sre21/eval/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre21/eval/enrollment/xvector_proc_embd_proc_cts_aug.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre21/eval/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.ark + + +# Create one scp with both enroll and test since this is expected by some scripts +cat ${exp_dir}/embeddings/sre16/eval/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.scp \ + ${exp_dir}/embeddings/sre16/eval/test/xvector_proc_embd_proc_cts_aug.scp \ + > ${exp_dir}/embeddings/sre16/eval/xvector_proc_embd_proc_cts_aug.scp + +cat ${exp_dir}/embeddings/sre18/dev/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.scp \ + ${exp_dir}/embeddings/sre18/dev/test/xvector_proc_embd_proc_cts_aug.scp \ + > ${exp_dir}/embeddings/sre18/dev/xvector_proc_embd_proc_cts_aug.scp + +cat ${exp_dir}/embeddings/sre18/eval/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.scp \ + ${exp_dir}/embeddings/sre18/eval/test/xvector_proc_embd_proc_cts_aug.scp \ + > ${exp_dir}/embeddings/sre18/eval/xvector_proc_embd_proc_cts_aug.scp + +cat ${exp_dir}/embeddings/sre21/dev/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.scp \ + ${exp_dir}/embeddings/sre21/dev/test/xvector_proc_embd_proc_cts_aug.scp \ + > ${exp_dir}/embeddings/sre21/dev/xvector_proc_embd_proc_cts_aug.scp + +cat ${exp_dir}/embeddings/sre21/eval/enrollment/enroll_spk_xvector_proc_embd_proc_cts_aug.scp \ + ${exp_dir}/embeddings/sre21/eval/test/xvector_proc_embd_proc_cts_aug.scp \ + > ${exp_dir}/embeddings/sre21/eval/xvector_proc_embd_proc_cts_aug.scp + + +################################################################## +# sre16_major for sre16 eval +echo "mean vector of enroll" +python tools/vector_mean.py \ + --spk2utt ${data}/sre16/eval/enrollment/spk2utt \ + --xvector_scp $exp_dir/embeddings/sre16/eval/enrollment/xvector_proc_embd_proc_sre16_major.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre16/eval/enrollment/enroll_spk_xvector_proc_embd_proc_sre16_major.ark + +# Create one scp with both enroll and test since this is expected by some scripts +cat ${exp_dir}/embeddings/sre16/eval/enrollment/enroll_spk_xvector_proc_embd_proc_sre16_major.scp \ + ${exp_dir}/embeddings/sre16/eval/test/xvector_proc_embd_proc_sre16_major.scp \ + > ${exp_dir}/embeddings/sre16/eval/xvector_proc_embd_proc_sre16_major.scp + + +################################################################## +# sre18_dev_unlabeled for sre18 dev/eval +echo "mean vector of enroll" +python tools/vector_mean.py \ + --spk2utt ${data}/sre18/dev/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre18/dev/enrollment/xvector_proc_embd_proc_sre18_dev_unlabeled.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre18/dev/enrollment/enroll_spk_xvector_proc_embd_proc_sre18_dev_unlabeled.ark + +python tools/vector_mean.py \ + --spk2utt ${data}/sre18/eval/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre18/eval/enrollment/xvector_proc_embd_proc_sre18_dev_unlabeled.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre18/eval/enrollment/enroll_spk_xvector_proc_embd_proc_sre18_dev_unlabeled.ark + +# Create one scp with both enroll and test since this is expected by some scripts +cat ${exp_dir}/embeddings/sre18/dev/enrollment/enroll_spk_xvector_proc_embd_proc_sre18_dev_unlabeled.scp \ + ${exp_dir}/embeddings/sre18/dev/test/xvector_proc_embd_proc_sre18_dev_unlabeled.scp \ + > ${exp_dir}/embeddings/sre18/dev/xvector_proc_embd_proc_sre18_dev_unlabeled.scp + +cat ${exp_dir}/embeddings/sre18/eval/enrollment/enroll_spk_xvector_proc_embd_proc_sre18_dev_unlabeled.scp \ + ${exp_dir}/embeddings/sre18/eval/test/xvector_proc_embd_proc_sre18_dev_unlabeled.scp \ + > ${exp_dir}/embeddings/sre18/eval/xvector_proc_embd_proc_sre18_dev_unlabeled.scp + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/download_data.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/download_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..e3e12109d7ed1f7ec2d7ea7813708f1496c35d02 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/download_data.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +download_dir=data/download_data + +. tools/parse_options.sh || exit 1 + +[ ! -d ${download_dir} ] && mkdir -p ${download_dir} + +if [ ! -f ${download_dir}/musan.tar.gz ]; then + echo "Downloading musan.tar.gz ..." + wget --no-check-certificate https://openslr.elda.org/resources/17/musan.tar.gz -P ${download_dir} + md5=$(md5sum ${download_dir}/musan.tar.gz | awk '{print $1}') + [ $md5 != "0c472d4fc0c5141eca47ad1ffeb2a7df" ] && echo "Wrong md5sum of musan.tar.gz" && exit 1 +fi + +if [ ! -f ${download_dir}/rirs_noises.zip ]; then + echo "Downloading rirs_noises.zip ..." + wget --no-check-certificate https://us.openslr.org/resources/28/rirs_noises.zip -P ${download_dir} + md5=$(md5sum ${download_dir}/rirs_noises.zip | awk '{print $1}') + [ $md5 != "e6f48e257286e05de56413b4779d8ffb" ] && echo "Wrong md5sum of rirs_noises.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox1_test_wav.zip ]; then + echo "Downloading vox1_test_wav.zip ..." + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_test_wav.zip -P ${download_dir} + md5=$(md5sum ${download_dir}/vox1_test_wav.zip | awk '{print $1}') + [ $md5 != "185fdc63c3c739954633d50379a3d102" ] && echo "Wrong md5sum of vox1_test_wav.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox1_dev_wav.zip ]; then + echo "Downloading vox1_dev_wav.zip ..." + for part in a b c d; do + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_dev_wav_parta${part} -P ${download_dir} & + done + wait + cat ${download_dir}/vox1_dev* >${download_dir}/vox1_dev_wav.zip + md5=$(md5sum ${download_dir}/vox1_dev_wav.zip | awk '{print $1}') + [ $md5 != "ae63e55b951748cc486645f532ba230b" ] && echo "Wrong md5sum of vox1_dev_wav.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox2_aac.zip ]; then + echo "Downloading vox2_aac.zip ..." + for part in a b c d e f g h; do + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox2_dev_aac_parta${part} -P ${download_dir} & + done + wait + cat ${download_dir}/vox2_dev_aac* >${download_dir}/vox2_aac.zip + md5=$(md5sum ${download_dir}/vox2_aac.zip | awk '{print $1}') + [ $md5 != "bbc063c46078a602ca71605645c2a402" ] && echo "Wrong md5sum of vox2_aac.zip" && exit 1 +fi + +echo "Download success !!!" diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/extract_sre.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/extract_sre.sh new file mode 100644 index 0000000000000000000000000000000000000000..f3e053c7aece6fcd443fe08bd209e78680744b51 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/extract_sre.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir='' +model_path='' +nj=4 +gpus="[0,1]" +data_type="shard" # shard/raw/feat +data=data +reverb_data=data/rirs/lmdb +noise_data=data/musan/lmdb +aug_plda_data=0 + +. tools/parse_options.sh +set -e + + + +#### +true && { +data_name_array=( + "cts_aug" + "sre16/major" + "sre16/eval/enrollment" + "sre16/eval/test" + "sre18/dev/enrollment/" + "sre18/dev/test/" + "sre18/dev/unlabeled/" + "sre18/eval/enrollment/" + "sre18/eval/test/" + "sre21/dev/enrollment/" + "sre21/dev/test/" + "sre21/eval/enrollment/" + "sre21/eval/test/" +) +data_list_path_array=( + "${data}/cts_aug/${data_type}.list" + "${data}/sre16/major/${data_type}.list" + "${data}/sre16/eval/enrollment/${data_type}.list" + "${data}/sre16/eval/test/${data_type}.list" + "${data}/sre18/dev/enrollment/${data_type}.list" + "${data}/sre18/dev/test/${data_type}.list" + "${data}/sre18/dev/unlabeled/${data_type}.list" + "${data}/sre18/eval/enrollment/${data_type}.list" + "${data}/sre18/eval/test/${data_type}.list" + "${data}/sre21/dev/enrollment/${data_type}.list" + "${data}/sre21/dev/test/${data_type}.list" + "${data}/sre21/eval/enrollment/${data_type}.list" + "${data}/sre21/eval/test/${data_type}.list" +) +data_scp_path_array=( + "${data}/cts_aug/wav.scp" + "${data}/sre16/major/wav.scp" + "${data}/sre16/eval/enrollment/wav.scp" + "${data}/sre16/eval/test/wav.scp" + "${data}/sre18/dev/enrollment/wav.scp" + "${data}/sre18/dev/test/wav.scp" + "${data}/sre18/dev/unlabeled/wav.scp" + "${data}/sre18/eval/enrollment/wav.scp" + "${data}/sre18/eval/test/wav.scp" + "${data}/sre21/dev/enrollment/wav.scp" + "${data}/sre21/dev/test/wav.scp" + "${data}/sre21/eval/enrollment/wav.scp" + "${data}/sre21/eval/test/wav.scp" +) # to count the number of wavs +nj_array=($nj $nj $nj $nj $nj $nj $nj $nj $nj $nj $nj $nj $nj) +batch_size_array=(1 1 1 1 1 1 1 1 1 1 1 1 1) # batch_size of test set must be 1 !!! +num_workers_array=(1 1 1 1 1 1 1 1 1 1 1 1 1) +aug_prob_array=(0.67 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0) +} + + +count=${#data_name_array[@]} + +true && { +for i in $(seq 0 $(($count - 1))); do + echo $i + wavs_num=$(wc -l ${data_scp_path_array[$i]} | awk '{print $1}') + bash tools/extract_embedding.sh --exp_dir ${exp_dir} \ + --model_path $model_path \ + --data_type ${data_type} \ + --data_list ${data_list_path_array[$i]} \ + --wavs_num ${wavs_num} \ + --store_dir ${data_name_array[$i]} \ + --batch_size ${batch_size_array[$i]} \ + --num_workers ${num_workers_array[$i]} \ + --aug_prob ${aug_prob_array[$i]} \ + --reverb_data ${reverb_data} \ + --noise_data ${noise_data} \ + --nj ${nj_array[$i]} \ + --gpus $gpus +done + +wait +} + +# Create enrollment models. This is the first order statistics. The zeroth order +# (the number of enrollment vectors) should, in principle, also be considered. +echo "mean vector of enroll" +python tools/vector_mean.py \ + --spk2utt ${data}/sre16/eval/enrollment/spk2utt \ + --xvector_scp $exp_dir/embeddings/sre16/eval/enrollment/xvector.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre16/eval/enrollment/enroll_spk_xvector.ark + +python tools/vector_mean.py \ + --spk2utt ${data}/sre18/dev/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre18/dev/enrollment/xvector.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre18/dev/enrollment/enroll_mdl_xvector.ark + +python tools/vector_mean.py \ + --spk2utt ${data}/sre18/eval/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre18/eval/enrollment/xvector.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre18/eval/enrollment/enroll_mdl_xvector.ark + +python tools/vector_mean.py \ + --spk2utt ${data}/sre21/dev/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre21/dev/enrollment/xvector.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre21/dev/enrollment/enroll_mdl_xvector.ark + +python tools/vector_mean.py \ + --spk2utt ${data}/sre21/eval/enrollment/mdl_id2utt \ + --xvector_scp $exp_dir/embeddings/sre21/eval/enrollment/xvector.scp \ + --spk_xvector_ark $exp_dir/embeddings/sre21/eval/enrollment/enroll_mdl_xvector.ark + + +# Create one scp with both enroll and test since this is expected by some scripts +cat ${exp_dir}/embeddings/sre16/eval/enrollment/enroll_spk_xvector.scp \ + ${exp_dir}/embeddings/sre16/eval/test/xvector.scp \ + > ${exp_dir}/embeddings/sre16/eval/xvector.scp + +cat ${exp_dir}/embeddings/sre18/dev/enrollment/enroll_mdl_xvector.scp \ + ${exp_dir}/embeddings/sre18/dev/test/xvector.scp \ + > ${exp_dir}/embeddings/sre18/dev/xvector.scp + +cat ${exp_dir}/embeddings/sre18/eval/enrollment/enroll_mdl_xvector.scp \ + ${exp_dir}/embeddings/sre18/eval/test/xvector.scp \ + > ${exp_dir}/embeddings/sre18/eval/xvector.scp + +cat ${exp_dir}/embeddings/sre21/dev/enrollment/enroll_mdl_xvector.scp \ + ${exp_dir}/embeddings/sre21/dev/test/xvector.scp \ + > ${exp_dir}/embeddings/sre21/dev/xvector.scp + +cat ${exp_dir}/embeddings/sre21/eval/enrollment/enroll_mdl_xvector.scp \ + ${exp_dir}/embeddings/sre21/eval/test/xvector.scp \ + > ${exp_dir}/embeddings/sre21/eval/xvector.scp + + + +echo "Embedding dir is (${exp_dir}/embeddings)." + + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/filter_utt_accd_dur.py b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/filter_utt_accd_dur.py new file mode 100644 index 0000000000000000000000000000000000000000..fff80d970af290e310f27fdb4903009cb7a1331d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/filter_utt_accd_dur.py @@ -0,0 +1,35 @@ +# Copyright (c) 2023 Zhengyang Chen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import fire + + +def main(wav_scp, utt2voice_dur, filter_wav_scp, dur_thres=5.0): + + utt2voice_dur_dict = {} + with open(utt2voice_dur, "r") as f: + for line in f: + utt, dur = line.strip().split() + utt2voice_dur_dict[utt] = float(dur) + + with open(wav_scp, "r") as f, open(filter_wav_scp, "w") as fw: + for line in f: + utt = line.strip().split()[0] + if utt in utt2voice_dur_dict: + if utt2voice_dur_dict[utt] > dur_thres: + fw.write(line) + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/generate_sre_aug.py b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/generate_sre_aug.py new file mode 100644 index 0000000000000000000000000000000000000000..e5fdfb14f3e65fba2c9d61d6a680f9451d93f0c4 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/generate_sre_aug.py @@ -0,0 +1,56 @@ +# Copyright (c) 2023 Zhengyang Chen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import fire + + +def main(ori_dir, aug_dir, aug_copy_num=2): + + if not os.path.exists(aug_dir): + os.makedirs(aug_dir) + + read_wav_scp = os.path.join(ori_dir, 'wav.scp') + aug_wav_scp = os.path.join(aug_dir, 'wav.scp') + read_utt2spk = os.path.join(ori_dir, 'utt2spk') + aug_utt2spk = os.path.join(aug_dir, 'utt2spk') + read_vad = os.path.join(ori_dir, 'vad') + store_vad = os.path.join(aug_dir, 'vad') + + with open(read_wav_scp, 'r') as f, open(aug_wav_scp, 'w') as wf: + for line in f: + line = line.strip().split() + utt, other_info = line[0], ' '.join(line[1:]) + for i in range(aug_copy_num + 1): + wf.write(utt + '_copy-' + str(i) + ' ' + other_info + '\n') + + with open(read_utt2spk, 'r') as f, open(aug_utt2spk, 'w') as wf: + for line in f: + line = line.strip().split() + utt, spk = line[0], line[1] + for i in range(aug_copy_num + 1): + wf.write(utt + '_copy-' + str(i) + ' ' + spk + '\n') + + with open(read_vad, 'r') as f, open(store_vad, 'w') as wf: + for line in f: + line = line.strip().split() + seg, utt, vad = line[0], line[1], ' '.join(line[2:]) + for i in range(aug_copy_num + 1): + new_seg = seg + '_copy-' + str(i) + new_utt = utt + '_copy-' + str(i) + wf.write(new_seg + ' ' + new_utt + ' ' + vad + '\n') + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/m4a2wav.pl b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/m4a2wav.pl new file mode 100644 index 0000000000000000000000000000000000000000..11e10acb8b267661dbcc013209b23cb6ebc05f25 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/m4a2wav.pl @@ -0,0 +1,64 @@ +#!/usr/bin/perl +# +# Copyright 2018 Ewald Enzinger +# 2022 Hongji Wang +# +# Usage: m4a2wav.pl /export/voxceleb2_m4a dev /export/voxceleb2_wav +# +# Note: This script requires ffmpeg to be installed and its location included in $PATH. + +if (@ARGV != 3) { + print STDERR "Usage: $0 \n"; + print STDERR "e.g. $0 /export/voxceleb2_m4a dev /export/voxceleb2_wav\n"; + exit(1); +} + +# Check that ffmpeg is installed. +if (`which ffmpeg` eq "") { + die "Error: this script requires that ffmpeg is installed."; +} + +($database_m4a, $dataset, $database_wav) = @ARGV; + +if ("$dataset" ne "dev" && "$dataset" ne "test") { + die "dataset parameter must be 'dev' or 'test'!"; +} + +opendir my $dh, "$database_m4a/$dataset/aac" or die "Cannot open directory: $!"; +my @spkr_dirs = grep {-d "$database_m4a/$dataset/aac/$_" && ! /^\.{1,2}$/} readdir($dh); +closedir $dh; + +if (system("mkdir -p $database_wav/$dataset") != 0) { + die "Error making directory $database_wav/$dataset"; +} + +open(fp, ">", "$database_wav/$dataset/m4a2wav_$dataset.sh") or die "Could not open the output file $database_wav/m4a2wav.sh"; + +foreach (@spkr_dirs) { + my $spkr_id = $_; + + opendir my $dh, "$database_m4a/$dataset/aac/$spkr_id/" or die "Cannot open directory: $!"; + my @rec_dirs = grep {-d "$database_m4a/$dataset/aac/$spkr_id/$_" && ! /^\.{1,2}$/} readdir($dh); + closedir $dh; + + foreach (@rec_dirs) { + my $rec_id = $_; + + opendir my $dh, "$database_m4a/$dataset/aac/$spkr_id/$rec_id/" or die "Cannot open directory: $!"; + my @files = map{s/\.[^.]+$//;$_}grep {/\.m4a$/} readdir($dh); + closedir $dh; + + foreach (@files) { + my $name = $_; + if ( not -e "$database_wav/$dataset/aac/$spkr_id/$rec_id"){ + system("mkdir -p $database_wav/$dataset/aac/$spkr_id/$rec_id"); + } + my $wav = "ffmpeg -v 8 -i $database_m4a/$dataset/aac/$spkr_id/$rec_id/$name.m4a -f wav -acodec pcm_s16le $database_wav/$dataset/aac/$spkr_id/$rec_id/$name.wav"; + print fp "$wav", "\n"; + } + } +} +close(fp) or die; + +# generate wav +#system("sh $database_wav/$dataset/m4a2wav_$dataset.sh"); diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/make_sre16_eval.pl b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/make_sre16_eval.pl new file mode 100644 index 0000000000000000000000000000000000000000..6e50e8a0338fce2d5f424b1e33712dfacf2920c7 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/make_sre16_eval.pl @@ -0,0 +1,208 @@ +#!/usr/bin/perl +use warnings; #sed replacement for -w perl parameter +use File::Basename; + +# Copyright 2017 David Snyder +# 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# Apache 2.0 +# + +# This script is taken from the Kaldi SRE16 recipe. For the Wespeaker recipe, we +# have done a few very minor changes, namely: +# 1. The path to the keys tar file are provided as an additional input argument. +# 2. The produced wav.scp will use ffmpeg instead of sph2pipe. +# 3. Some changes in paths to fit wespeaker recipe. +# 4. Warning if wav files have no meta data. Mainly happens if the directory +# searched for wav files contains files that are not in the original data. +# 5 Formatting to fit Wespeaker's requirement. + +if (@ARGV != 3) { + print STDERR "Usage: $0 " , + " \n"; + print STDERR "e.g. $0 /export/corpora/SRE/R149_0_1 data/\n"; + exit(1); +} + +($db_base, $evalset_keys, $out_dir) = @ARGV; + +# Handle enroll +$out_dir_enroll = "$out_dir/sre16/eval/enroll"; +if (system("mkdir -p $out_dir_enroll")) { + die "Error making directory $out_dir_enroll"; +} + +$tmp_dir_enroll = "$out_dir_enroll/tmp"; +if (system("mkdir -p $tmp_dir_enroll") != 0) { + die "Error making directory $tmp_dir_enroll"; +} + +open(SPKR, ">$out_dir_enroll/utt2spk") + || die "Could not open the output file $out_dir_enroll/utt2spk"; +open(WAV, ">$out_dir_enroll/wav.scp") + || die "Could not open the output file $out_dir_enroll/wav.scp"; +open(META, "<$db_base/docs/sre16_eval_enrollment.tsv") + or die "cannot open wav list"; +%utt2fixedutt = (); +while () { + $line = $_; + @toks = split(" ", $line); + $spk = $toks[0]; + $utt = $toks[1]; + if ($utt ne "segment") { + print SPKR "${spk}-${utt} $spk\n"; + $utt2fixedutt{$utt} = "${spk}-${utt}"; + } +} + +# Using cmd here and a few other places to satisfy the 80 char. requirement. +my $cmd1="find $db_base/data/enrollment/ -name '*.sph'". + " > $tmp_dir_enroll/sph.list"; +if (system($cmd1) != 0) { + die "Error getting list of sph files"; +} + +open(WAVLIST, "<$tmp_dir_enroll/sph.list") or die "cannot open wav list"; + +while() { + chomp; + $sph = $_; + @t = split("/",$sph); + @t1 = split("[./]",$t[$#t]); + $utt=$utt2fixedutt{$t1[0]}; + if ($utt) { + print WAV "$utt", + " ffmpeg -nostdin -i $sph -ac 1 -ar 8000 -f wav pipe:1 |\n"; + }else { + print("WARNING $t1[0] not in meta data. Will not be used.\n"); + } +} +close(WAV) || die; +close(SPKR) || die; + +# Handle test +$out_dir_test= "$out_dir/sre16/eval/test"; +if (system("mkdir -p $out_dir_test")) { + die "Error making directory $out_dir_test"; +} + +$tmp_dir_test = "$out_dir_test/tmp"; +if (system("mkdir -p $tmp_dir_test") != 0) { + die "Error making directory $tmp_dir_test"; +} + + +if (system("cp $evalset_keys $out_dir_test")) { + die "Error copying sre16 keys."; +} + +my $key_name = basename( $evalset_keys ); + +if (system("tar -xvf $out_dir_test/$key_name -C $out_dir_test")) { + die "Could not untar sre16 keys."; +} + + +open(SPKR, ">$out_dir_test/utt2spk") + || die "Could not open the output file $out_dir_test/utt2spk"; +open(WAV, ">$out_dir_test/wav.scp") + || die "Could not open the output file $out_dir_test/wav.scp"; +open(TRIALS, ">$out_dir_test/trials") + || die "Could not open the output file $out_dir_test/trials"; +open(TGL_TRIALS, ">$out_dir_test/trials_tgl") + || die "Could not open the output file $out_dir_test/trials_tgl"; +open(YUE_TRIALS, ">$out_dir_test/trials_yue") + || die "Could not open the output file $out_dir_test/trials_yue"; + +my $cmd2="find $db_base/data/test/ -name '*.sph' > $tmp_dir_test/sph.list"; +if (system($cmd2) != 0) { + die "Error getting list of sph files"; +} + + +open(KEY, "<$out_dir_test/R149_0_1/docs/sre16_eval_trial_key.tsv") + || die "Could not open trials file", + " $out_dir_test/R149_0_1/docs/sre16_eval_trial_key.tsv."; +open(SEG_KEY, "<$out_dir_test/R149_0_1/docs/sre16_eval_segment_key.tsv") + || die "Could not open trials file", + " $out_dir_test/R149_0_1/docs/sre16_eval_segment_key.tsv."; +open(LANG_KEY, "<$out_dir_test/R149_0_1/metadata/calls.tsv") + || die " Could not open trials file", + " $out_dir_test/R149_0_1/metadata/calls.tsv."; +open(WAVLIST, "<$tmp_dir_test/sph.list") or die "cannot open wav list"; + +%utt2call = (); +while() { + chomp; + $line = $_; + @toks = split(" ", $line); + $utt = $toks[0]; + $call = $toks[1]; + if ($utt ne "segment") { + $utt2call{$utt} = $call; + } +} +close(SEG_KEY) || die; + +%call2lang = (); +while() { + chomp; + $line = $_; + @toks = split(" ", $line); + $call = $toks[0]; + $lang = $toks[1]; + $call2lang{$call} = $lang; +} +close(LANG_KEY) || die; + +while() { + chomp; + $sph = $_; + @t = split("/",$sph); + @t1 = split("[./]",$t[$#t]); + $utt=$t1[0]; + print WAV "$utt"," ffmpeg -nostdin -i $sph -ac 1 -ar 8000 -f wav pipe:1 |\n"; + print SPKR "$utt $utt\n"; +} +close(WAV) || die; +close(SPKR) || die; + +while () { + $line = $_; + @toks = split(" ", $line); + $spk = $toks[0]; + $utt = $toks[1]; + $call = $utt2call{$utt}; + $target_type = $toks[3]; + if ($utt ne "segment") { + print TRIALS "${spk} ${utt} ${target_type}\n"; + if ($call2lang{$call} eq "tgl") { + print TGL_TRIALS "${spk} ${utt} ${target_type}\n"; + } elsif ($call2lang{$call} eq "yue") { + print YUE_TRIALS "${spk} ${utt} ${target_type}\n"; + } else { + die "Unexpected language $call2lang{$call} for utterance $utt."; + } + } +} + +close(TRIALS) || die; +close(TGL_TRIALS) || die; +close(YUE_TRIALS) || die; + +my $cmd3="tools/utt2spk_to_spk2utt.pl". + " $out_dir_enroll/utt2spk >$out_dir_enroll/spk2utt"; +if (system($cmd3) != 0) { + die "Error creating spk2utt file in directory $out_dir_enroll"; +} + +my $cmd4="tools/utt2spk_to_spk2utt.pl $out_dir_test/utt2spk >$out_dir_test/spk2utt"; +if (system($cmd4) != 0) { + die "Error creating spk2utt file in directory $out_dir_test"; +} + +if (system("tools/fix_data_dir.sh $out_dir_enroll") != 0) { + die "Error fixing data dir $out_dir_enroll"; +} +if (system("tools/fix_data_dir.sh $out_dir_test") != 0) { + die "Error fixing data dir $out_dir_test"; +} diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/make_sre16_unlabeled.pl b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/make_sre16_unlabeled.pl new file mode 100644 index 0000000000000000000000000000000000000000..4766cde14bbcfe425cbbb56df2623d2a34af95a7 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/make_sre16_unlabeled.pl @@ -0,0 +1,111 @@ +#!/usr/bin/perl +use warnings; #sed replacement for -w perl parameter +# Copyright 2017 David Snyder +# 2024 Johan Rohdin (rohdin@fit.vutbr.cz): Just some minor changes +# in paths to fit the Wespeaker recipe organization as well +# formatting to fit Wespeaker's requirements. +# Apache 2.0 + +if (@ARGV != 2) { + print STDERR "Usage: $0 ", " + \n"; + print STDERR "e.g. $0", + "/export/corpora/SRE/LDC2016E46_SRE16_Call_My_Net_Training_Data data/\n"; + exit(1); +} + +($db_base, $out_dir) = @ARGV; + +# Handle major subset. +$out_dir_major = "$out_dir/sre16/major"; +if (system("mkdir -p $out_dir_major")) { + die "Error making directory $out_dir_major"; +} + +$tmp_dir_major = "$out_dir_major/tmp"; +if (system("mkdir -p $tmp_dir_major") != 0) { + die "Error making directory $tmp_dir_major"; +} + +open(SPKR, ">$out_dir_major/utt2spk") + || die "Could not open the output file $out_dir_major/utt2spk"; +open(WAV, ">$out_dir_major/wav.scp") + || die "Could not open the output file $out_dir_major/wav.scp"; + +my $cmd1="find $db_base/data/unlabeled/major/ -name '*.sph'". + " > $tmp_dir_major/sph.list"; +if (system($cmd1) != 0) { + die "Error getting list of sph files"; +} + +open(WAVLIST, "<$tmp_dir_major/sph.list") or die "cannot open wav list"; + +while() { + chomp; + $sph = $_; + @t = split("/",$sph); + @t1 = split("[./]",$t[$#t]); + $utt=$t1[0]; + print WAV "$utt"," ffmpeg -nostdin -i $sph -ac 1 -ar 8000 -f wav pipe:1 |\n"; + print SPKR "$utt $utt\n"; +} + +close(WAV) || die; +close(SPKR) || die; + +# Handle minor subset. +$out_dir_minor= "$out_dir/sre16/minor"; +if (system("mkdir -p $out_dir_minor")) { + die "Error making directory $out_dir_minor"; +} + +$tmp_dir_minor = "$out_dir_minor/tmp"; +if (system("mkdir -p $tmp_dir_minor") != 0) { + die "Error making directory $tmp_dir_minor"; +} + +open(SPKR, ">$out_dir_minor/utt2spk") + || die "Could not open the output file $out_dir_minor/utt2spk"; +open(WAV, ">$out_dir_minor/wav.scp") + || die "Could not open the output file $out_dir_minor/wav.scp"; + +my $cmd2="find $db_base/data/unlabeled/minor/ -name '*.sph'". + " > $tmp_dir_minor/sph.list"; +if (system($cmd2) != 0) { + die "Error getting list of sph files"; +} + +open(WAVLIST, "<$tmp_dir_minor/sph.list") + or die "cannot open wav list"; + +while() { + chomp; + $sph = $_; + @t = split("/",$sph); + @t1 = split("[./]",$t[$#t]); + $utt=$t1[0]; + print WAV "$utt"," ffmpeg -nostdin -i $sph -ac 1 -ar 8000 -f wav pipe:1 |\n"; + print SPKR "$utt $utt\n"; +} +close(WAV) || die; +close(SPKR) || die; + +my $cmd3="tools/utt2spk_to_spk2utt.pl $out_dir_major/utt2spk". + ">$out_dir_major/spk2utt"; +if (system($cmd3) != 0) { + die "Error creating spk2utt file in directory $out_dir_major"; +} + +my $cmd4="tools/utt2spk_to_spk2utt.pl $out_dir_minor/utt2spk". + " > $out_dir_minor/spk2utt"; +if (system($cmd4) != 0) { + die "Error creating spk2utt file in directory $out_dir_minor"; +} + +if (system("tools/fix_data_dir.sh $out_dir_major") != 0) { + die "Error fixing data dir $out_dir_major"; +} + +if (system("tools/fix_data_dir.sh $out_dir_minor") != 0) { + die "Error fixing data dir $out_dir_minor"; +} diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/make_system_sad.py b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/make_system_sad.py new file mode 100644 index 0000000000000000000000000000000000000000..2e32b556ce78e5612119d6cf720a02cd51349703 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/make_system_sad.py @@ -0,0 +1,147 @@ +# Copyright (c) 2022 Xu Xiang +# 2023 Zhengyang Chen +# 2024 Johan Rohdin +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["OPENBLAS_NUM_THREADS"] = "1" +os.environ["MKL_NUM_THREADS"] = "1" +os.environ["VECLIB_MAXIMUM_THREADS"] = "1" +os.environ["NUMEXPR_NUM_THREADS"] = "1" + +import sys +import io +import functools +import concurrent.futures +import argparse +import importlib +import torchaudio +import subprocess + +import torch + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--repo-path', required=True, + help='VAD model repo path') + parser.add_argument('--scp', required=True, help='wav scp') + parser.add_argument('--min-duration', required=True, + type=float, help='min duration') + args = parser.parse_args() + + return args + + +@functools.lru_cache(maxsize=1) +def load_wav( + wav_rxfilename, +): + """ This function reads audio file and return data in pytorch tensor. + "lru_cache" holds recently loaded audio so that can be called + many times on the same audio file. + OPTIMIZE: controls lru_cache size for random access, + considering memory size + """ + if wav_rxfilename.endswith('|'): + # input piped command + p = subprocess.Popen(wav_rxfilename[:-1], shell=True, + stdout=subprocess.PIPE) + data, samplerate = torchaudio.load(io.BytesIO(p.stdout.read())) + elif wav_rxfilename == '-': + # stdin + data, samplerate = torchaudio.load(sys.stdin) + else: + # normal wav file + data, samplerate = torchaudio.load(wav_rxfilename) + return data.squeeze(0), samplerate + + +def read_scp(scp): + utt_wav_pair = [] + for line in open(scp, 'r'): + segs = line.strip().split() + if len(segs) > 2: + utt, wav = segs[0], ' '.join(segs[1:]) + else: + utt, wav = segs[0], segs[1] + utt_wav_pair.append((utt, wav)) + + return utt_wav_pair + + +def silero_vad(utt_wav_pair, repo_path, min_duration, + sampling_rate=8000, threshold=0.25): + + def module_from_file(module_name, file_path): + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + utils_vad = module_from_file("utils_vad", + os.path.join(repo_path, "utils_vad.py")) + model = utils_vad.init_jit_model( + os.path.join(repo_path, 'files/silero_vad.jit')) + + utt, wav = utt_wav_pair + + wav_f = wav + + wav, sr = load_wav(wav) + assert sr == sampling_rate, "Audio file {} has wrong sampling rate \ + ({} instead of {})".format(wav_f, sr, sampling_rate) + + speech_timestamps = utils_vad.get_speech_timestamps( + wav, model, sampling_rate=sampling_rate, + threshold=threshold) + + vad_result = "" + for item in speech_timestamps: + begin = item['start'] / sampling_rate + end = item['end'] / sampling_rate + if end - begin >= min_duration: + vad_result += "{}-{:08d}-{:08d} {} {:.3f} {:.3f}\n".format( + utt, int(begin * 1000), int(end * 1000), utt, begin, end) + + return vad_result + + +def main(): + args = get_args() + + vad = functools.partial(silero_vad, + repo_path=args.repo_path, + min_duration=args.min_duration) + utt_wav_pair_list = read_scp(args.scp) + # with concurrent.futures.ProcessPoolExecutor() as executor: + # print(''.join(executor.map(vad, utt_wav_pair_list)), end='') + # It seems the pool doesn't work well so split into chunks of max size n + # (e.g. 10000). Splitting like this also has the consequence that the VAD + # is printed after processing n files instead of after processing all files. + n = 10000 + utt_wav_pair_list_of_list = [utt_wav_pair_list[i * n:(i + 1) * n] + for i in range((len(utt_wav_pair_list) + n - 1) // n)] + for lol in utt_wav_pair_list_of_list: + with concurrent.futures.ProcessPoolExecutor() as executor: + print(''.join(executor.map(vad, lol)), end='') + + +if __name__ == '__main__': + torch.set_num_threads(1) + + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_cts_superset.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_cts_superset.sh new file mode 100644 index 0000000000000000000000000000000000000000..b93f83872621a20b7716996b7e0538ecfa7ade0c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_cts_superset.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -o pipefail + +export LC_ALL=C + + +data_cts=data/cts/ +cts_superset_dir="" +wav_dir=wav/cts/ + +. tools/parse_options.sh || exit 1 + +echo $cts_superset_dir + +if [ ! -f $cts_superset_dir/docs/cts_superset_segment_key.tsv ];then + echo "ERROR: $cts_superset_dir/docs/cts_superset_segment_key.tsv does not exist." + exit 1 +fi + +mkdir -p $data_cts + + +echo -n "" > ${data_cts}/wav.scp +for x in $(tail -n +2 $cts_superset_dir/docs/cts_superset_segment_key.tsv | cut -f 1 | sed "s:\.sph::" );do + echo "$x ffmpeg -nostdin -i ${cts_superset_dir}/data/${x}.sph -ar 8000 -f wav pipe:1 |" >> $data_cts/wav.scp +done + + +tail -n +2 $cts_superset_dir/docs/cts_superset_segment_key.tsv | cut -f 1,3 --output-delimiter=" " | sed "s:\.sph::" | sort > ${data_cts}/utt2spk + +tools/utt2spk_to_spk2utt.pl ${data_cts}/utt2spk > ${data_cts}/spk2utt diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_data.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..6e5c787925b95b1581d5362c55dcd0c2b8882869 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_data.sh @@ -0,0 +1,307 @@ +#!/bin/bash + +# Copyright (c) 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +stage=-1 +stop_stage=-1 +#sre_data_dir= +data=data + +### +sre16_unlab_dir="" +sre16_evalset_dir="" +sre16_evalset_keys="" +### +sre18_devset_dir="" +sre18_evalset_dir="" +sre18_evalset_keys="" +### +sre21_devset_dir="" +sre21_evalset_dir="" +sre21_evalset_keys="" +### +cts_superset_dir="" +### +voxceleb_dir="" + +compute_total_utterance_duration=true # Whether to compute the total utterance duration, i.e., including no speech parts + # Can be used as an addition filtering requirement. Currently only supported for + # VoxCeleb. +compute_vad_for_voxceleb=true +include_voxceleb_vad_in_train_data=true # If false, only CTS vad will be inluded which means that VAD will not be applied for VoxCeleb during training. + +. tools/parse_options.sh || exit 1 + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + mkdir -p external_tools + # Download voice activity detection model pretrained by Silero Team + wget -c https://github.com/snakers4/silero-vad/archive/refs/tags/v4.0.zip -O external_tools/silero-vad-v4.0.zip + unzip -o external_tools/silero-vad-v4.0.zip -d external_tools +fi + + +### SRE16 +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + # We use the scripts from the Kaldi SRE16 recipe with some minor modifications. + + # Prepare NIST SRE 2016 evaluation data. + local/make_sre16_eval.pl $sre16_evalset_dir $sre16_evalset_keys data + + # Prepare unlabeled Cantonese and Tagalog development data. This dataset + # was distributed to SRE participants. + local/make_sre16_unlabeled.pl $sre16_unlab_dir data +fi + + +### SRE18 +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Preparing SRE18" + local/prepare_sre18.sh --stage 1 --stop_stage 1 --sre18_dev_dir $sre18_devset_dir --sre18_eval_dir $sre18_evalset_dir --sre18_eval_keys_file $sre18_evalset_keys --data_dir $data/sre18 +fi + + +### SRE21 +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Preparing SRE21" + local/prepare_sre21.sh --stage 1 --stop_stage 1 --sre21_dev_dir $sre21_devset_dir --sre21_eval_dir $sre21_evalset_dir --sre21_eval_keys_file $sre21_evalset_keys --data_dir $data/sre21 +fi + + +### CTS +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "Preparing CTS" + local/prepare_cts_superset.sh --cts_superset_dir $cts_superset_dir --data_cts $data/cts --wav_dir `pwd`/wav/cts + + + # Only mixer data. Used for backend training. Create only lists here. + # The data directory will be created later, after VAD. + awk -F"\t" '{if($7 == "mx3" || $7 == "mx45" || $7 == "mx6"){print $0} }' ${cts_superset_dir}/docs/cts_superset_segment_key.tsv \ + > data/cts_superset_segment_key_mx3456.tsv + cut -f 1 data/cts_superset_segment_key_mx3456.tsv | sed s:\\.sph$:: > data/mx_3456.list + +fi + + +### VoxCeleb +# We are using all of VoxCeleb 1 and the training (aka "development") part of VoxCeleb 2. +# (The test part of VoxCeleb 2) may have some overlap with VoxCeleb 1. See +# https://www.robots.ox.ac.uk/~vgg/publications/2019/Nagrani19/nagrani19.pdf, Table 4.) +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + + echo "Preparing VoxCeleb" + if [[ $voxceleb_dir == "" ]];then + echo "Preparing Voxceleb, rirs and Musan" + voxceleb_dir=${data}_vox + mkdir ${voxceleb_dir} + local/prepare_vox.sh --stage 1 --stop_stage 4 --data ${data}_vox + fi + + if [[ ! -d $voxceleb_dir/vox1 || ! -d $voxceleb_dir/vox2_dev ]];then + echo "ERROR: Problem with Voxceleb data directory." + exit 1 + fi + + # Downsample VoxCeleb and apply GSM. We create a new wav.scp with this command in the + # extraction chain rather than creating the new wav files explicitly. + sox_command='-t gsm -r 8000 - | sox -t gsm -r 8000 - -t wav -r 8000 -c 1 -e signed-integer -' + for dset in vox1 vox2_dev;do + tools/copy_data_dir.sh $voxceleb_dir/$dset $data/${dset}_gsmfr + awk -v sc="$sox_command" '{print $1 " sox " $2 " " sc " |" }' $voxceleb_dir/$dset/wav.scp > $data/${dset}_gsmfr/wav.scp + done + + # Combine all Voxceleb data + tools/combine_data.sh data/vox_gsmfr data/vox1_gsmfr/ data/vox2_dev_gsmfr/ + + # Copy rirs and musan from voxceleb. We don't need to downsample as this will be + # done on-the-fly. If the direcotires already contain the data in lmdb format + # we just link it. Otherwise we copy it and let later stages create the lmdb + # format data here. Since we don't want to affect the original data. + for x in rirs musan;do + if [ -d $voxceleb_dir/$x/lmdb ];then + ln -s $voxceleb_dir/$x $data/ + else + mkdir $data/$x + cp -r $voxceleb_dir/$x/wav.scp $data/$x/wav.scp + fi + done + +fi + + +if [ ${stage} -le 7 ] && [ ${stop_stage} -ge 7 ]; then + + echo "Get vad segmentation for dataset." + true && { + # Set VAD min duration + min_duration=0.25 + for dset in vox_gsmfr cts sre18/dev/test sre18/dev/enrollment sre18/dev/unlabeled sre18/eval/test sre18/eval/enrollment sre21/dev/test sre21/dev/enrollment sre21/eval/test sre21/eval/enrollment sre16_major sre16/eval/enrollment sre16/eval/test; do + python3 local/make_system_sad.py \ + --repo-path external_tools/silero-vad-4.0 \ + --scp ${data}/${dset}/wav.scp \ + --min-duration $min_duration > ${data}/${dset}/vad + cp -r ${data}/${dset} ${data}/${dset}-bk # Since VAD is quite time-consuming, it is good to have a backup. + done + } + + true && { + # We may consider to use only the mixer portion of the CTS data for backen training + # as it may be closer to the SRE data. + + tools/subset_data_dir.sh --utt-list data/mx_3456.list data/cts data/mx_3456 + tools/filter_scp.pl -f 2 ${data}/mx_3456/wav.scp ${data}/cts/vad > ${data}/mx_3456/vad + + + # For PLDA training, it is better to augment the training data + python3 local/generate_sre_aug.py --ori_dir ${data}/mx_3456 \ + --aug_dir ${data}/mx_3456_aug \ + --aug_copy_num 2 + + tools/utt2spk_to_spk2utt.pl ${data}/mx_3456_aug/utt2spk > ${data}/mx_3456_aug/spk2utt + } + + true && { + # We may consider to use only the mixer portion of the CTS data for backend training + # as it may be closer to the SRE data. + + # For PLDA training, it is better to augment the training data + python3 local/generate_sre_aug.py --ori_dir ${data}/cts \ + --aug_dir ${data}/cts_aug \ + --aug_copy_num 2 + + tools/utt2spk_to_spk2utt.pl ${data}/cts_aug/utt2spk > ${data}/cts_aug/spk2utt + } + +fi + +if [ ${stage} -le 8 ] && [ ${stop_stage} -ge 8 ]; then + + true && { + for dset in cts vox_gsmfr; do + echo $dset + if [ -f ${data}/${dset}/vad ] && ( [ $dset != "vox_gsmfr" ] || $compute_vad_for_voxceleb ) ;then + echo "Using VAD info" + python3 local/utt2voice_duration.py \ + --vad_file ${data}/${dset}/vad \ + --utt2voice_dur ${data}/${dset}/utt2voice_dur + cp ${data}/${dset}/utt2voice_dur ${data}/${dset}-bk/ # Good to have backup also of this + fi + done + } + + true && { + # The below need to be improved to work for a general wav.scp. It only works for the specif format of voxceleb wav.scp + # at the moment. + for dset in vox_gsmfr; do + if $compute_total_utterance_duration; then + # We may, for example, avoid applying VAD on VoxCeleb in which case we need this. + # Note that the durations are estimated on the original wave file, before sox + # downsampling and GSM codec is applied. + echo "Using soxi" + + cut -f3 -d" " ${data}/${dset}/wav.scp | awk '{ print "soxi -D " $0 }' > ${data}/${dset}/soxi_cmd.sh + split -a 4 -d -n l/12 ${data}/${dset}/soxi_cmd.sh ${data}/${dset}/soxi_cmd.split. + for i in {0000..11}; do + bash ${data}/${dset}/soxi_cmd.split.$i > ${data}/${dset}/soxi_cmd.split.$i.out & + done + wait + + for i in {0000..11}; do cat ${data}/${dset}/soxi_cmd.split.$i.out; done > ${data}/${dset}/dur_tmp + cut -f1 -d" " ${data}/${dset}/wav.scp > ${data}/${dset}/utt_tmp + paste -d " " ${data}/${dset}/utt_tmp ${data}/${dset}/dur_tmp > ${data}/${dset}/utt2dur + + rm ${data}/${dset}/soxi_cmd.* ${data}/${dset}/vox_gsmfr/dur_tmp ${data}/${dset}/utt_tmp + + cp ${data}/${dset}/utt2dur ${data}/${dset}-bk/ # Good to have backup also of this + fi + done + } +fi + +if [ ${stage} -le 9 ] && [ ${stop_stage} -ge 9 ]; then + + declare -A voice_dur_threshold=( ["cts"]=5.0 ["vox_gsmfr"]=0.0 ) # Note that a threshold of 0.0 still means that utterances with no speech + # according to VAD will be discarded at this stage. So if we want to keep + # them, we should skip block 1 for the set instead. + declare -A dur_threshold=( ["cts"]=0.0 ["vox_gsmfr"]=5.0 ) + + declare -A uttPerSpk_threshold=( ["cts"]=2 ["vox_gsmfr"]=2 ) # Kept if more than this threshold. (I.e. equality not sufficient.) + + true && { + # Following the Kaldi recipe: https://github.com/kaldi-asr/kaldi/blob/71f38e62cad01c3078555bfe78d0f3a527422d75/egs/sre16/v2/run.sh#L189 + # We filter out the utterances with duration less than 5s + echo "Stage 9, block 1" + echo "Applying filtering based on voice duration " + #for dset in cts vox_gsmfr; do + for dset in cts; do + n_utt_before=$( wc -l ${data}/${dset}/utt2spk | cut -f1 -d " " ) + n_spk_before=$( wc -l ${data}/${dset}/spk2utt | cut -f1 -d " " ) + python3 local/filter_utt_accd_dur.py \ + --wav_scp ${data}/${dset}/wav.scp \ + --utt2voice_dur ${data}/${dset}/utt2voice_dur \ + --filter_wav_scp ${data}/${dset}/filter_wav.scp \ + --dur_thres ${voice_dur_threshold[$dset]} + mv ${data}/${dset}/wav.scp ${data}/${dset}/wav.scp.bak + mv ${data}/${dset}/filter_wav.scp ${data}/${dset}/wav.scp + tools/fix_data_dir.sh ${data}/${dset} + echo " $dset " + echo " #utt / #spk before: $n_utt_before / $n_spk_before " + n_utt_after=$( wc -l ${data}/${dset}/utt2spk | cut -f1 -d " " ) + n_spk_after=$( wc -l ${data}/${dset}/spk2utt | cut -f1 -d " " ) + echo " #utt / #spk after: $n_utt_after / $n_spk_after " + done + } + echo "Stage 9, block 2" + echo "Applying filtering based on the whole utterance duration (including non-speech parts) " + #for dset in cts vox_gsmfr; do + for dset in vox_gsmfr; do + n_utt_before=$( wc -l ${data}/${dset}/utt2spk | cut -f1 -d " " ) + n_spk_before=$( wc -l ${data}/${dset}/spk2utt | cut -f1 -d " " ) + python3 local/filter_utt_accd_dur.py \ + --wav_scp ${data}/${dset}/wav.scp \ + --utt2voice_dur ${data}/${dset}/utt2dur \ + --filter_wav_scp ${data}/${dset}/filter_wav.scp \ + --dur_thres ${dur_threshold[$dset]} + mv ${data}/${dset}/wav.scp ${data}/${dset}/wav.scp.bak + mv ${data}/${dset}/filter_wav.scp ${data}/${dset}/wav.scp + tools/fix_data_dir.sh ${data}/${dset} + echo " $dset " + echo " #utt / #spk before: $n_utt_before / $n_spk_before " + n_utt_after=$( wc -l ${data}/${dset}/utt2spk | cut -f1 -d " " ) + n_spk_after=$( wc -l ${data}/${dset}/spk2utt | cut -f1 -d " " ) + echo " #utt / #spk after: $n_utt_after / $n_spk_after " + done + + + # Similarly, following the Kaldi recipe, + # we throw out speakers with fewer than 3 utterances. + echo "Stage 9, block 3" + for dset in cts vox_gsmfr; do + #tools/fix_data_dir.sh ${data}/${dset} + cp ${data}/${dset}/spk2utt ${data}/${dset}/spk2utt.bak + awk -v thr=${uttPerSpk_threshold[$dset]} '{if(NF>thr){print $0}}' ${data}/${dset}/spk2utt.bak > ${data}/${dset}/spk2utt + tools/spk2utt_to_utt2spk.pl ${data}/${dset}/spk2utt > ${data}/${dset}/utt2spk + tools/fix_data_dir.sh ${data}/${dset} + done + + ./tools/combine_data.sh data/cts_vox data/cts/ data/vox_gsmfr + if $include_voxceleb_vad_in_train_data;then + cat data/cts/vad data/vox_gsmfr/vad > data/cts_vox/vad + else + cat data/cts/vad > data/cts_vox/vad + fi +fi + + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_sre18.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_sre18.sh new file mode 100644 index 0000000000000000000000000000000000000000..2952b0e31245d327f74581e4e097a0fb0b97defd --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_sre18.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# Copyright (c) 2023 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +set -o pipefail + +export LC_ALL=C + + +sre18_dev_dir="" +sre18_eval_dir="" +sre18_eval_keys_file="" +data_dir=data/sre18 +wav_dir=wav/ +stage=1 +stop_stage=1 + +. tools/parse_options.sh || exit 1 + +echo "sre18 dev dir: $sre18_dev_dir" +echo "sre18 eval dir: $sre18_eval_dir" +echo "sre18 eval keys file: $sre18_eval_keys_file" + +declare -A set2dir=( ["dev"]=$sre18_dev_dir ["eval"]=$sre18_eval_dir ) +declare -A set2subset=( ["dev"]="enrollment test unlabeled" ["eval"]="enrollment test" ) + + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + + + for z in dev eval;do + src_dir=${set2dir[$z]} + + echo "Processing SRE 18 $z set from $src_dir" + + + true && { + for s in ${set2subset[$z]};do + + tgt_dir=$data_dir/$z/$s + echo " - $s set. Storing in $tgt_dir" + mkdir -p $tgt_dir + + if [ -f $tgt_dir/wav.scp ];then + rm $tgt_dir/wav.scp + fi + + # Create the wav files + for x in $( ls $src_dir/data/${s}/ );do + name=$(basename $x .sph) + if [ $name != $x ];then + # suffix is .sph + echo "$name ffmpeg -nostdin -i $src_dir/data/${s}/$x -ar 8000 -f wav pipe:1 |" >> $tgt_dir/wav.scp + else + name=$(basename $x .flac) + if [ $name != $x ];then + # suffix is .flac + # From http://trac.ffmpeg.org/wiki/audio%20types:"The default for muxing + # into WAV files is pcm_s16le." so the below should be ok. + echo "$name ffmpeg -nostdin -i $src_dir/data/${s}/$x -ar 8000 -f wav pipe:1 |" >> $tgt_dir/wav.scp + else + echo "ERROR: Invalid suffix in file $x" + exit 1 + fi + fi + done + done + } + + # Mappings for "enrollment models" <-> "utterances" + # The evaluation consider enrollment "models" rather than enrollment "speakers". Possibly several models could be + # from the same speaker. There speaker ID of the models are not known. So we can't create "spk2utt" and utt2spk". + # For test data there is no such mappings either. + grep -v modelid $src_dir/docs/sre18_${z}_enrollment.tsv | cut -f1,2 | sed "s:\t: :" > $data_dir/$z/enrollment/enrollment.txt + cat $data_dir/$z/enrollment/enrollment.txt | sed "s:.sph$::" | sed "s:.flac$: :" \ + | awk '{print $2 " " $1}' > $data_dir/$z/enrollment/utt2mdl_id + # No utterance is used in more than one mdl so utt2mdl_id makes sense. + ./tools/utt2spk_to_spk2utt.pl $data_dir/$z/enrollment/utt2mdl_id > $data_dir/$z/enrollment/mdl_id2utt + + true && { + # Trial list and keys. Not available in the eval directory so the specified file is used. + if [ $z == "eval" ];then + cp $sre18_eval_keys_file $data_dir/$z/ + key_name=$( basename $sre18_eval_keys_file .tbz2) + tar -xvf $data_dir/$z/${key_name}.tbz2 -C $data_dir/$z/ + key_file=$data_dir/$z/LDC2018E51/docs/sre18_eval_trial_key.tsv + else + key_file=$src_dir/docs/sre18_dev_trial_key.tsv + fi + } + + tail -n+2 $key_file | cut -f1,2,4 | sed "s:\.sph::" | sed "s:\.flac::" | sed "s:\t: :g" > $data_dir/$z/sre18_${z}_trials + + + done +fi + + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_sre21.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_sre21.sh new file mode 100644 index 0000000000000000000000000000000000000000..80bacacc6b5b1f5bcd1c55ca2d1fedc86a757a96 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_sre21.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# Copyright (c) 2023 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +set -o pipefail + +export LC_ALL=C + + +sre21_dev_dir="" +sre21_eval_dir="" +sre21_eval_keys_file="" +data_dir=data/sre21 +wav_dir=wav/ #sre21_eval +stage=1 +stop_stage=1 + +. tools/parse_options.sh || exit 1 + +echo "sre21 dev dir: $sre21_dev_dir" +echo "sre21 eval dir: $sre21_eval_dir" +echo "sre21 eval keys file: $sre21_eval_keys_file" + +declare -A set2dir=( ["dev"]=$sre21_dev_dir ["eval"]=$sre21_eval_dir ) + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + + + for z in dev eval;do + src_dir=${set2dir[$z]} + + echo "Processing SRE 21 $z set from $src_dir" + + true && { + for s in enrollment test;do + + tgt_dir=$data_dir/$z/$s + echo " - $s set. Storing in $tgt_dir" + mkdir -p $tgt_dir + + if [ -f $tgt_dir/wav.scp ];then + rm $tgt_dir/wav.scp + fi + + # Create the wav files + for x in $( ls $src_dir/data/audio/${s}/ );do + name=$(basename $x .sph) + if [ $name != $x ];then + # suffix is .sph + echo "$name ffmpeg -nostdin -i $src_dir/data/audio/${s}/$x -ar 8000 -f wav pipe:1 |" >> $tgt_dir/wav.scp + else + name=$(basename $x .flac) + if [ $name != $x ];then + # suffix is .flac + # From http://trac.ffmpeg.org/wiki/audio%20types:"The default for muxing + # into WAV files is pcm_s16le." so the below should be ok. + echo "$name ffmpeg -nostdin -i $src_dir/data/audio/${s}/$x -ar 8000 -f wav pipe:1 |" >> $tgt_dir/wav.scp + else + echo "ERROR: Invalid suffix in file $x" + exit 1 + fi + fi + done + done + + + # Mappings for "enrollment models" <-> "utterances" + # The evaluation consider enrollment "models" rather than enrollment "speakers". Possibly several models could be + # from the same speaker. There speaker ID of the models are not known. So we can't create "spk2utt" and utt2spk". + # For test data there is no such mappings either. + grep -v modelid $src_dir/docs/sre21_audio_${z}_enrollment.tsv | sed "s:\t: :" > $data_dir/$z/enrollment/enrollment.txt + cat $data_dir/$z/enrollment/enrollment.txt | sed "s:.sph$::" | sed "s:.flac$: :" \ + | awk '{print $2 " " $1}' > $data_dir/$z/enrollment/utt2mdl_id + # No utterance is used in more than one mdl so utt2mdl_id makes sense. + ./tools/utt2spk_to_spk2utt.pl $data_dir/$z/enrollment/utt2mdl_id > $data_dir/$z/enrollment/mdl_id2utt + } + + # Trial list and keys. Not available in the eval directory so the specified file is used. + if [ $z == "eval" ];then + cp $sre21_eval_keys_file $data_dir/$z/ + key_name=$( basename $sre21_eval_keys_file ) + echo "tar -xvf $data_dir/$z/$key_name -C $data_dir/$z/" + tar -xvf $data_dir/$z/$key_name -C $data_dir/$z/ + key_file=$data_dir/$z/sre21/releases/LDC2021E10/docs/sre21_audio_eval_trial_key.tsv + else + key_file=$src_dir/docs/sre21_audio_dev_trial_key.tsv + fi + + + tail -n+2 $key_file | cut -f1,2,3 | sed "s:\.sph::" | sed "s:\.flac::" | sed "s:\t: :g" > $data_dir/$z/sre21_${z}_trials + + + done +fi + + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_vox.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_vox.sh new file mode 100644 index 0000000000000000000000000000000000000000..b957eaf95028fec384a6b72053ed7aebdce916ba --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/prepare_vox.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +stage=-1 +stop_stage=-1 +data=data + +. tools/parse_options.sh || exit 1 + +data=`realpath ${data}` +download_dir=${data}/download_data +rawdata_dir=${data}/raw_data + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Download musan.tar.gz, rirs_noises.zip, vox1_test_wav.zip, vox1_dev_wav.zip, and vox2_aac.zip." + echo "This may take a long time. Thus we recommand you to download all archives above in your own way first." + + ./local/download_data.sh --download_dir ${download_dir} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Decompress all archives ..." + echo "This could take some time ..." + + for archive in musan.tar.gz rirs_noises.zip vox1_test_wav.zip vox1_dev_wav.zip vox2_aac.zip; do + [ ! -f ${download_dir}/$archive ] && echo "Archive $archive not exists !!!" && exit 1 + done + [ ! -d ${rawdata_dir} ] && mkdir -p ${rawdata_dir} + + if [ ! -d ${rawdata_dir}/musan ]; then + tar -xzvf ${download_dir}/musan.tar.gz -C ${rawdata_dir} + fi + + if [ ! -d ${rawdata_dir}/RIRS_NOISES ]; then + unzip ${download_dir}/rirs_noises.zip -d ${rawdata_dir} + fi + + if [ ! -d ${rawdata_dir}/voxceleb1 ]; then + mkdir -p ${rawdata_dir}/voxceleb1/test ${rawdata_dir}/voxceleb1/dev + unzip ${download_dir}/vox1_test_wav.zip -d ${rawdata_dir}/voxceleb1/test + unzip ${download_dir}/vox1_dev_wav.zip -d ${rawdata_dir}/voxceleb1/dev + fi + + if [ ! -d ${rawdata_dir}/voxceleb2_m4a ]; then + mkdir -p ${rawdata_dir}/voxceleb2_m4a + unzip ${download_dir}/vox2_aac.zip -d ${rawdata_dir}/voxceleb2_m4a + fi + + echo "Decompress success !!!" +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Convert voxceleb2 wav format from m4a to wav using ffmpeg." + echo "This could also take some time ..." + + if [ ! -d ${rawdata_dir}/voxceleb2_wav ]; then + ./local/m4a2wav.pl ${rawdata_dir}/voxceleb2_m4a dev ${rawdata_dir}/voxceleb2_wav + # Here we use 8 parallel jobs + cat ${rawdata_dir}/voxceleb2_wav/dev/m4a2wav_dev.sh | xargs -P 8 -i sh -c "{}" + fi + + echo "Convert m4a2wav success !!!" +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Prepare wav.scp for each dataset ..." + export LC_ALL=C # kaldi config + + mkdir -p ${data}/musan ${data}/rirs ${data}/vox1 ${data}/vox2_dev + # musan + find ${rawdata_dir}/musan -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' >${data}/musan/wav.scp + # rirs + find ${rawdata_dir}/RIRS_NOISES/simulated_rirs -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' >${data}/rirs/wav.scp + # vox1 + find ${rawdata_dir}/voxceleb1 -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' | sort >${data}/vox1/wav.scp + awk '{print $1}' ${data}/vox1/wav.scp | awk -F "/" '{print $0,$1}' >${data}/vox1/utt2spk + ./tools/utt2spk_to_spk2utt.pl ${data}/vox1/utt2spk >${data}/vox1/spk2utt + if [ ! -d ${data}/vox1/trials ]; then + echo "Download trials for vox1 ..." + mkdir -p ${data}/vox1/trials + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test.txt -O ${data}/vox1/trials/vox1-O.txt + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_hard.txt -O ${data}/vox1/trials/vox1-H.txt + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_all.txt -O ${data}/vox1/trials/vox1-E.txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test2.txt -O ${data}/vox1/trials/vox1-O\(cleaned\).txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_hard2.txt -O ${data}/vox1/trials/vox1-H\(cleaned\).txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_all2.txt -O ${data}/vox1/trials/vox1-E\(cleaned\).txt + # transform them into kaldi trial format + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1/trials/vox1-O\(cleaned\).txt >${data}/vox1/trials/vox1_O_cleaned.kaldi + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1/trials/vox1-H\(cleaned\).txt >${data}/vox1/trials/vox1_H_cleaned.kaldi + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1/trials/vox1-E\(cleaned\).txt >${data}/vox1/trials/vox1_E_cleaned.kaldi + fi + # vox2 + find ${rawdata_dir}/voxceleb2_wav -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' | sort >${data}/vox2_dev/wav.scp + awk '{print $1}' ${data}/vox2_dev/wav.scp | awk -F "/" '{print $0,$1}' >${data}/vox2_dev/utt2spk + ./tools/utt2spk_to_spk2utt.pl ${data}/vox2_dev/utt2spk >${data}/vox2_dev/spk2utt + + echo "Success !!!" +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score.sh new file mode 100644 index 0000000000000000000000000000000000000000..1d84e00ca666a83f5ceed2579f4764534227c302 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenhzhengyang117@gmail.com) +# 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#exp_dir= +#trials="trials trials_tgl trials_yue" +#data=data + + +trials="" +xvectors="" +cal_mean_dir="" +exp_dir="" + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +echo " - trials $trials" +echo " - xvectors $xvectors" +echo " - cal_mean dir $cal_mean_dir" +echo " - exp_dir $exp_dir" + + +scores_dir=${exp_dir}/scores + +echo $cal_mean_dir + +if [ -z $cal_mean_dir ];then + cal_mean_string="--cal_mean False --cal_mean_dir xxxx" # For the moment, score.py requires something to be input to --cal_mean_dir + output_name=$(basename $xvectors | sed "s:xvector::" | sed "s:.scp::" | sed "s:^_::") # Changes xvector_proc_embd_proc_sre16_major.scp -> proc_embd_proc_sre16_major +else # xvector.scp -> '' (empty string) + cal_mean_string="--cal_mean True --cal_mean_dir $cal_mean_dir" + output_name="mean_$(echo $cal_mean_dir | sed "s:.*embeddings/::" | sed -e "s:/:_:g")" # Name will be e.g. mean_sre16_major if sre16/major data is used +fi # for mean subtraction. + + +echo $cal_mean_string +echo $output_name + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "apply cosine scoring ..." + mkdir -p ${exp_dir}/scores + for x in $(echo $trials | tr "," " "); do + echo "Trials $x" + python wespeaker/bin/score.py \ + --exp_dir ${exp_dir} \ + --eval_scp_path $xvectors \ + $cal_mean_string \ + ${x} + xx=$(basename $x) + mv ${scores_dir}/${xx}.score ${scores_dir}/${xx}.${output_name}_cos.score + done +fi + + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "compute metrics (EER/minDCF) ..." + for x in $(echo $trials | tr "," " "); do + xx=$(basename $x) + echo $xx + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${xx}.${output_name}_cos.score \ + 2>&1 | tee ${scores_dir}/${xx}.${output_name}_cos.result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${xx}.${output_name}_cos.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score_plda.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score_plda.sh new file mode 100644 index 0000000000000000000000000000000000000000..ac79a160c3c2c456f88282190f85dbb3fb9473d1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score_plda.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +# Copyright (c) 2023 Shuai Wang (wsstriving@gmail.com) +# 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +exp_dir="exp/ResNet34-TSTP-emb256-fbank40-num_frms200-aug0.6-spFalse-saFalse-Softmax-SGD-epoch10/" +data=data +trials="${data}/sre16/eval/trials ${data}/sre16/eval/trials_tgl ${data}/sre16/eval/trials_yue" +aug_plda_data=0 + +enroll_scp=sre16/eval/enrollment/xvector.scp +test_scp=sre16/eval/test/xvector.scp +utt2spk=data/sre16/eval/enrollment/utt2spk +preprocessing_chain='length-norm' +preprocessing_path="${exp_dir}/embd_proc.pkl" + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +if [ $aug_plda_data = 0 ];then + sre_plda_data=cts +else + sre_plda_data=cts_aug +fi + +echo "preprocessing_path $preprocessing_path" +preproc_name=$(basename $preprocessing_path .pkl) +echo "preproc_name $preproc_name" + + + +# Kaldi PLDA cts_aug, cts_aug mean, speaker mean last, no lnorm in PLDA +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Preparing preprocessing chain for backend " + python wespeaker/bin/prep_embd_proc.py \ + --chain "$preprocessing_chain" \ + --path $preprocessing_path + echo "Backend preprocessor prepared" +fi + + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Applying preprocessing on PLDA training data." + python wespeaker/bin/apply_embd_proc.py \ + --path $preprocessing_path \ + --input ${exp_dir}/embeddings/${sre_plda_data}/xvector.scp \ + --output ${exp_dir}/embeddings/${sre_plda_data}/xvector_proc_$preproc_name.ark,scp +fi + + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "train the plda model ..." + python wespeaker/bin/train_plda.py \ + --exp_dir ${exp_dir} \ + --scp_path ${exp_dir}/embeddings/${sre_plda_data}/xvector_proc_$preproc_name.scp \ + --utt2spk ${data}/${sre_plda_data}/utt2spk \ + --indim 100 \ + --iter 10 + echo "plda training finished" +fi + + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Applying preprocessing on evaluation and adaptation data." + for x in $enroll_scp $test_scp $indomain_scp;do + #new_x=$(echo $x | sed "s:\.scp:_proc\.ark,scp:") + new_x=$(echo $x | sed "s:\.scp:_proc_$preproc_name\.ark,scp:") + echo "Processing in: $x" + echo "Processing out: $new_x" + python wespeaker/bin/apply_embd_proc.py \ + --path $preprocessing_path \ + --input ${exp_dir}/embeddings/$x \ + --output ${exp_dir}/embeddings/$new_x + done +fi + + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "apply plda scoring ..." + mkdir -p ${exp_dir}/scores + + enroll_scp=$(echo $enroll_scp | sed "s:\.scp:_proc_$preproc_name\.scp:") + test_scp=$(echo $test_scp | sed "s:\.scp:_proc_$preproc_name\.scp:") + + for x in $(echo $trials | tr "," " "); do + xx=$(basename $x) + echo "scoring on " $x + python wespeaker/bin/eval_plda.py \ + --enroll_scp_path ${exp_dir}/embeddings/$enroll_scp \ + --test_scp_path ${exp_dir}/embeddings/$test_scp \ + --utt2spk $utt2spk \ + --trial ${x} \ + --score_path ${exp_dir}/scores/${xx}.proc_${preproc_name}_plda.score \ + --model_path ${exp_dir}/plda + done +fi + + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "compute metrics (EER/minDCF) ..." + scores_dir=${exp_dir}/scores + for x in $(echo $trials | tr "," " "); do + xx=$(basename $x) + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${xx}.proc_${preproc_name}_plda.score \ + 2>&1 | tee ${scores_dir}/${xx}.proc_${preproc_name}_plda.result + # 2>&1 | tee -a ${scores_dir}/${xx}_plda_result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${xx}.proc_${preproc_name}_plda.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score_plda_adapt.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score_plda_adapt.sh new file mode 100644 index 0000000000000000000000000000000000000000..533b548456fa22bdf8c8b5e237070d1e20a0b750 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score_plda_adapt.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# Copyright (c) 2023 Shuai Wang (wsstriving@gmail.com) +# 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +exp_dir=exp/ResNet34-TSTP-emb256-fbank40-num_frms200-aug0.6-spFalse-saFalse-Softmax-SGD-epoch10/ +data=data +trials="${data}/sre16/eval/trials ${data}/sre16/eval/trials_tgl ${data}/sre16/eval/trials_yue" +aug_plda_data=0 + +enroll_scp=sre16/eval/enrollment/xvector.scp +test_scp=sre16/eval/test/xvector.scp +indomain_scp=sre16/major/xvector.scp # For adaptation +utt2spk=data/sre16/eval/enrollment/utt2spk +preprocessing_path=${exp_dir}/embd_proc_sre16_major.pkl + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +if [ $aug_plda_data = 0 ];then + sre_plda_data=sre +else + sre_plda_data=sre_aug +fi + +preproc_name=$(basename $preprocessing_path .pkl) + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Applying preprocessing on evaluation and adaptation data." + for x in $enroll_scp $test_scp $indomain_scp;do + #new_x=$(echo $x | sed "s:\.scp:_proc\.ark,scp:") + new_x=$(echo $x | sed "s:\.scp:_proc_$preproc_name\.ark,scp:") + echo "Processing in: $x" + echo "Processing out: $new_x" + python wespeaker/bin/apply_embd_proc.py \ + --path $preprocessing_path \ + --input ${exp_dir}/embeddings/$x \ + --output ${exp_dir}/embeddings/$new_x + done +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "adapt the plda model ..." + + indomain_scp=$(echo $indomain_scp | sed "s:\.scp:_proc_$preproc_name\.scp:") + + python wespeaker/bin/adapt_plda.py \ + -mo ${exp_dir}/plda \ + -ma ${exp_dir}/plda_adapt \ + -ad ${exp_dir}/embeddings/$indomain_scp \ + -ws 0.75 \ + -as 0.25 + echo "plda adapted finished" +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "apply plda scoring ..." + + enroll_scp=$(echo $enroll_scp | sed "s:\.scp:_proc_$preproc_name\.scp:") + test_scp=$(echo $test_scp | sed "s:\.scp:_proc_$preproc_name\.scp:") + + mkdir -p ${exp_dir}/scores + for x in $(echo $trials | tr "," " "); do + xx=$(basename $x) + echo "scoring on " $x + python wespeaker/bin/eval_plda.py \ + --enroll_scp_path ${exp_dir}/embeddings/$enroll_scp \ + --test_scp_path ${exp_dir}/embeddings/$test_scp \ + --utt2spk $utt2spk \ + --trial ${x} \ + --score_path ${exp_dir}/scores/${xx}.proc_${preproc_name}_plda_adapt.score \ + --model_path ${exp_dir}/plda_adapt + done +fi +#--indomain_scp ${exp_dir}/embeddings/$indomain_scp \ Note: This option was used before the new code for preprocessing. +# With this code, all preprocessing takes place in the preprocessing chain. So we don't include it in the above code anymore. + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "compute metrics (EER/minDCF) ..." + scores_dir=${exp_dir}/scores + for x in $(echo $trials | tr "," " "); do + xx=$(basename $x) + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${xx}.proc_${preproc_name}_plda_adapt.score \ + 2>&1 | tee ${scores_dir}/${xx}.proc_${preproc_name}_plda_adapt.result + #2>&1 | tee -a ${scores_dir}/${xx}_plda_adapt_result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${xx}.proc_${preproc_name}_plda_adapt.score + done +fi + + + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score_plda_org.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score_plda_org.sh new file mode 100644 index 0000000000000000000000000000000000000000..a601ad5f66677cf5df7b6b0ddd48539a4b0f95ab --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/score_plda_org.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Copyright (c) 2023 Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +exp_dir= +data=data +trials="${data}/sre16/eval/trials ${data}/sre16/eval/trials_tgl ${data}/sre16/eval/trials_yue" +aug_plda_data=0 + +enroll_scp=sre16/eval/enrollment/xvector.scp +test_scp=sre16/eval/test/xvector.scp +indomain_scp=sre16/major/xvector.scp # For mean subtraction +utt2spk=data/sre16/eval/enrollment/utt2spk + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +if [ $aug_plda_data = 0 ];then + sre_plda_data=cts +else + sre_plda_data=cts_aug +fi + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "train the plda model ..." + python wespeaker/bin/train_plda.py \ + --exp_dir ${exp_dir} \ + --scp_path ${exp_dir}/embeddings/${sre_plda_data}/xvector.scp \ + --utt2spk ${data}/${sre_plda_data}/utt2spk \ + --indim 256 \ + --iter 200 + echo "plda training finished" +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "apply plda scoring ..." + mkdir -p ${exp_dir}/scores + for x in $(echo $trials | tr "," " "); do + xx=$(basename $x) + echo "scoring on " $x + python wespeaker/bin/eval_plda.py \ + --enroll_scp_path ${exp_dir}/embeddings/$enroll_scp \ + --test_scp_path ${exp_dir}/embeddings/$test_scp \ + --indomain_scp ${exp_dir}/embeddings/$indomain_scp \ + --utt2spk $utt2spk \ + --trial ${x} \ + --score_path ${exp_dir}/scores/${xx}.pldascore \ + --model_path ${exp_dir}/plda + done +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "compute metrics (EER/minDCF) ..." + scores_dir=${exp_dir}/scores + for x in $(echo $trials | tr "," " "); do + xx=$(basename $x) + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${xx}.pldascore \ + 2>&1 | tee -a ${scores_dir}/${xx}_plda_result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${xx}.pldascore + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/utt2voice_duration.py b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/utt2voice_duration.py new file mode 100644 index 0000000000000000000000000000000000000000..2dd7d1c137ad374c56f7495df9b563d871e70d8b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/local/utt2voice_duration.py @@ -0,0 +1,36 @@ +# Copyright (c) 2023 Zhengyang Chen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import fire +from collections import OrderedDict + + +def main(vad_file, utt2voice_dur): + utt2voice_dur_dict = OrderedDict() + + with open(vad_file, 'r') as f: + for line in f.readlines(): + segs = line.strip().split() + utt, start, end = segs[-3], float(segs[-2]), float(segs[-1]) + if utt not in utt2voice_dur_dict: + utt2voice_dur_dict[utt] = 0.0 + utt2voice_dur_dict[utt] += end - start + + with open(utt2voice_dur, 'w') as f: + for utt, duration in utt2voice_dur_dict.items(): + f.write('{} {}\n'.format(utt, duration)) + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/path.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/path.sh new file mode 100644 index 0000000000000000000000000000000000000000..b90a5154cf4a8f25b9a42851c59393f07dacb350 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/path.sh @@ -0,0 +1,5 @@ +export PATH=$PWD:$PATH + +# NOTE(kan-bayashi): Use UTF-8 in Python to avoid UnicodeDecodeError when LC_ALL=C +export PYTHONIOENCODING=UTF-8 +export PYTHONPATH=../../../:$PYTHONPATH diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/qsub_extract.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/qsub_extract.sh new file mode 100644 index 0000000000000000000000000000000000000000..e3073c232bd90b21427bce303fa06cb9a9e4878d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/qsub_extract.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +#$ -cwd +#$ -V +#$ -N extract_embd +#$ -o extract_embd.out +#$ -e extract_embd.err +#$ -l gpu=4,ram_free=10G,mem_free=10G,core=2,matylda6=2,scratch=0.5,gpu_ram=16G +#$ -q long.q@@gpu + +cd /mnt/matylda6/rohdin/expts/wespeaker/wespeaker_private_test2/examples/sre/v3 +unset PYTHONPATH +unset PYTHONHOME + +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +__conda_setup="$('/mnt/matylda5/iplchot/python_public/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then + eval "$__conda_setup" +else + if [ -f "/mnt/matylda5/iplchot/python_public/anaconda3/etc/profile.d/conda.sh" ]; then + . "/mnt/matylda5/iplchot/python_public/anaconda3/etc/profile.d/conda.sh" + else + export PATH="$PATH:/mnt/matylda5/iplchot/python_public/anaconda3/bin" + fi +fi +unset __conda_setup +# <<< conda initialize <<< + +conda activate /mnt/matylda6/rohdin/conda/wespeaker_20240220/ +which python +export PATH=$PATH:/mnt/matylda6/rohdin/software/kaldi_20210625/tools/sph2pipe/ + +./run.sh > logs/run.sh.stage4.log.1 2>&1 + + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/qsub_train.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/qsub_train.sh new file mode 100644 index 0000000000000000000000000000000000000000..d6a6815d27e5ea38f8090fdafe4a6c63d8d91148 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/qsub_train.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# +#$ -cwd +#$ -V +#$ -N train_xvec +#$ -o train_xvec.out +#$ -e train_xvec.err +#$ -pe smp 16 +#$ -l gpu=0.125,ram_free=1.25G,mem_free=1.25G,matylda6=0.625,gpu_ram=16G +#$ -q long.q@@gpu +cd /mnt/matylda6/rohdin/expts/wespeaker/wespeaker_private_test2/examples/sre/v3/ # Need to change your training directory. + +unset PYTHONPATH +unset PYTHONHOME + +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +__conda_setup="$('/mnt/matylda5/iplchot/python_public/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +if [ $? -eq 0 ]; then + eval "$__conda_setup" +else + if [ -f "/mnt/matylda5/iplchot/python_public/anaconda3/etc/profile.d/conda.sh" ]; then + . "/mnt/matylda5/iplchot/python_public/anaconda3/etc/profile.d/conda.sh" + else + export PATH="$PATH:/mnt/matylda5/iplchot/python_public/anaconda3/bin" + fi +fi +unset __conda_setup +# <<< conda initialize <<< + +conda activate /mnt/matylda6/rohdin/conda/wespeaker_20240220/ +./run.sh > logs/run.sh.stage3.log.1 2>&1 + + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..fe6a82e2e90c90b8f4f7ea07e49c63fd816ae340 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/sre/v3/run.sh @@ -0,0 +1,542 @@ +#!/bin/bash + +# Copyright 2022 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2024 Johan Rohdin (rohdin@fit.vutbr.cz) + +. ./path.sh || exit 1 + +# Stages +# 1. Data preparation +# 2. Shard / raw list creation +# 3. Training +# 4. Model averaging, embedding extraction +# 5. Export model +# 6. Cosine scoring using cts_aug, sre16_major, sre18_dev_unlabeled for mean subtraction but no other embedding processing +# 7. PLDA scoring, including length-norm, lda and subtraction of the above mentioned sets. See details at the stage. +# 8. Adapted PLDA scoring. Same embedding processing as above. +# 9. Cosine scoring with same embedding processing as above. +# 10. Summarization of results. + +stage=1 +stop_stage=1 + +HOST_NODE_ADDR="localhost:29400" +num_nodes=1 +job_id=2024 + +data=data +data_type="shard" # shard/raw + +# whether augment the PLDA data +aug_plda_data=1 + +config=conf/resnet.yaml +exp_dir=exp/ResNet34-TSTP-emb256-fbank64-num_frms200-aug0.6-spFalse-saFalse-Softmax-SGD-epoch10 + +# gpus="[0,1]" # For slurm, just specify this according to the number of GPUs you have. +num_gpus_train=2 # If this variable is defined, safe_gpu will be used to select the free GPUs. + # If so, it will override whatever may have been specified in gpus="[x,...] + # Typically, you would want to use this option for SGE. + # If this variable is not set, or set to '', the script will assume that + # the GPUs to use are specified in the variable "gpus" as above. + +num_gpus_extract=4 # We may want to use a different value for extraction. + +num_avg=10 +checkpoint= + + +. tools/parse_options.sh || exit 1 + +############################################################################################ +# The names of various lists are not consistent across sets. Therefore we need some mappings. + +# Different sets may use different backend adaptation sets, therefore we need several trial +# lists. Using "," instead of space as separator is a bit ugly but it seems parse_options.sh +# cannot process an argument with space properly. +declare -A trials=( ["sre16_eval"]='data/sre16/eval/trials,data/sre16/eval/trials_yue,data/sre16/eval/trials_tgl' + ["sre18_dev"]="data/sre18/dev/sre18_dev_trials" + ["sre18_eval"]="data/sre18/eval/sre18_eval_trials" + ["sre21_dev"]="data/sre21/dev/sre21_dev_trials" + ["sre21_eval"]="data/sre21/eval/sre21_eval_trials" ) + +declare -A enr_scp=( ["sre16_eval"]='sre16/eval/enrollment/xvector.scp' + ["sre18_dev"]="sre18/dev/enrollment/xvector.scp" + ["sre18_eval"]="sre18/eval/enrollment/xvector.scp" + ["sre21_dev"]="sre21/dev/enrollment/xvector.scp" + ["sre21_eval"]="sre21/eval/enrollment/xvector.scp" ) + +declare -A test_scp=( ["sre16_eval"]='sre16/eval/test/xvector.scp' + ["sre18_dev"]="sre18/dev/test/xvector.scp" + ["sre18_eval"]="sre18/eval/test/xvector.scp" + ["sre21_dev"]="sre21/dev/test/xvector.scp" + ["sre21_eval"]="sre21/eval/test/xvector.scp" ) + +declare -A utt2mdl=( ["sre16_eval"]='data/sre16/eval/enrollment/utt2spk' + ["sre18_dev"]="data/sre18/dev/enrollment/utt2mdl_id" + ["sre18_eval"]="data/sre18/eval/enrollment/utt2mdl_id" + ["sre21_dev"]="data/sre21/dev/enrollment/utt2mdl_id" + ["sre21_eval"]="data/sre21/eval/enrollment/utt2mdl_id" ) + +declare -A mdl2utt=( ["sre16_eval"]='data/sre16/eval/enrollment/spk2utt' + ["sre18_dev"]="data/sre18/dev/enrollment/mdl_id2utt" + ["sre18_eval"]="data/sre18/eval/enrollment/mdl_id2utt" + ["sre21_dev"]="data/sre21/dev/enrollment/mdl_id2utt" + ["sre21_eval"]="data/sre21/eval/enrollment/mdl_id2utt" ) + +declare -A xvectors=( ["sre16_eval"]="sre16/eval/xvector.scp" + ["sre18_dev"]="sre18/dev/xvector.scp" + ["sre18_eval"]="sre18/eval/xvector.scp" + ["sre21_dev"]="sre21/dev/xvector.scp" + ["sre21_eval"]="sre21/eval/xvector.scp" ) +############################################################################################ + + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Prepare datasets ..." + + + ###################################################################################### + ### Test sets. Please specify paths + # SRE16 should be prepared by the Kaldi recipe and the path should be specified here: + #sre_data_dir=/mnt/matylda4/burget/kaldi-trunk/kaldi/egs/sre16/v2/data/ + # Will be used by ./local/prepare_data.sh below. (only wav.scp, utt2spk and spk2utt files are needed.) + sre16_unlab_dir=/mnt/matylda2/data/NIST/sre16/LDC2016E46_SRE16_Call_My_Net_Training_Data + sre16_evalset_dir=/mnt/matylda2/data/NIST/sre16/R149_0_1 + # Eval keys are not in the above directory since they were distributed after the evaluation. + sre16_evalset_keys=/mnt/matylda2/data/NIST/sre16/download/sre16_evaluation_key.tar.bz2 + + # SRE18 + sre18_devset_dir=/mnt/matylda2/data/NIST/sre18/LDC2018E46_2018_NIST_Speaker_Recognition_Evaluation_Development_Set + sre18_evalset_dir=/mnt/matylda2/data/LDC/LDC2018E51_2018_NIST_Speaker_Recognition_Evaluation_Test_Set/ + # Eval keys are not in the above directory since they were distributed after the evaluation. + sre18_evalset_keys=/mnt/matylda2/data/NIST/sre18/LDC2018E51_eval_segment_key.tbz2 + + # SRE21 + sre21_devset_dir=/mnt/matylda2/data/LDC/LDC2021E09_sre21_dev_set/ + sre21_evalset_dir=/mnt/matylda2/data/LDC/LDC2021E10_sre21_eval_set/ + # Eval keys are not in the above directory since they were distributed after the evaluation. + sre21_evalset_keys=/mnt/matylda2/data/NIST/sre21/download/sre21_test_key.tgz + + + ###################################################################################### + ### Training sets + # CTS + cts_superset_dir=/mnt/matylda2/data/LDC/LDC2021E08_SRE-CTS-Superset/ + + # VoxCeleb + voxceleb_dir="/mnt/matylda6/rohdin/expts/wespeaker/wespeaker/examples/voxceleb/v2/data/" + + # This script is based on ../v2/local/prepare_data.sh + # Copies SRE16 relevant files, extracts VAD for all files, does some pruning of the training set. + ./local/prepare_data.sh --stage 1 --stop_stage 10 --data ${data} \ + --sre16_unlab_dir ${sre16_unlab_dir} --sre16_evalset_dir ${sre16_evalset_dir} --sre16_evalset_keys ${sre16_evalset_keys} \ + --sre18_devset_dir ${sre18_devset_dir} --sre18_evalset_dir ${sre18_evalset_dir} --sre18_evalset_keys ${sre18_evalset_keys} \ + --sre21_devset_dir ${sre21_devset_dir} --sre21_evalset_dir ${sre21_evalset_dir} --sre21_evalset_keys ${sre21_evalset_keys} \ + --cts_superset_dir ${cts_superset_dir} --voxceleb_dir ${voxceleb_dir} +fi + + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + + true && { + echo "Convert train data to ${data_type}..." + for dset in cts_vox; do + python tools/make_shard_list.py --num_utts_per_shard 1000 \ + --num_threads 12 \ + --prefix shards \ + --shuffle \ + --vad_file ${data}/$dset/vad \ + ${data}/$dset/wav.scp ${data}/$dset/utt2spk \ + ${data}/$dset/shards ${data}/$dset/shard.list + done + } + + true && { + echo "Convert data for PLDA backend training and evaluation to raw format..." + if [ $aug_plda_data = 0 ];then + sre_plda_data=cts + else + sre_plda_data=cts_aug + fi + + # Raw format for backend and evaluation data + for dset in ${sre_plda_data} sre16/major sre16/eval/enrollment sre16/eval/test \ + sre18/dev/enrollment sre18/dev/test sre18/dev/unlabeled sre18/eval/enrollment sre18/eval/test \ + sre21/dev/enrollment sre21/dev/test sre21/eval/enrollment sre21/eval/test;do + + # The below requires utt2spk to be present. So create a "dummy" one if we don't have it. + # This is for example the case with sre21 eval data. + if [ ! -f $data/$dset/utt2spk ];then + awk '{print $1 " unk"}' ${data}/${dset}/wav.scp > ${data}/${dset}/utt2spk + fi + + python tools/make_raw_list.py --vad_file ${data}/$dset/vad \ + ${data}/$dset/wav.scp \ + ${data}/$dset/utt2spk ${data}/$dset/raw.list + done + } + + true && { + # Convert all musan and rirs data to LMDB if they don't already exist. + for x in rirs musan;do + if [ ! -d $data/$x/lmdb ];then + python tools/make_lmdb.py ${data}/$x/wav.scp ${data}/$x/lmdb + fi + done + } + +fi + + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Start training ..." + if [ ! -z $num_gpus_train ];then + gpus=$(python -c "from sys import argv; from safe_gpu import safe_gpu; safe_gpu.claim_gpus(int(argv[1])); print( safe_gpu.gpu_owner.devices_taken )" $num_gpus_train | sed "s: ::g") + else + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + fi + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/bin/train.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + --data_type "${data_type}" \ + --train_data ${data}/cts_vox/${data_type}.list \ + --train_label ${data}/cts_vox/utt2spk \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + ${checkpoint:+--checkpoint $checkpoint} +fi + + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + + false && { + echo "Do model average ..." + avg_model=$exp_dir/models/avg_model.pt + python wespeaker/bin/average_model.py \ + --dst_model $avg_model \ + --src_path $exp_dir/models \ + --num ${num_avg} + + model_path=$avg_model + if [[ $config == *repvgg*.yaml ]]; then + echo "convert repvgg model ..." + python wespeaker/models/convert_repvgg.py \ + --config $exp_dir/config.yaml \ + --load $avg_model \ + --save $exp_dir/models/convert_model.pt + model_path=$exp_dir/models/convert_model.pt + fi + } + + avg_model=$exp_dir/models/avg_model.pt + model_path=$avg_model + + echo "Extract embeddings ..." + avg_model=$exp_dir/models/avg_model.pt + model_path=$avg_model + gpus=$(python -c "from sys import argv; from safe_gpu import safe_gpu; safe_gpu.claim_gpus(int(argv[1])); print( safe_gpu.gpu_owner.devices_taken )" $num_gpus_extract | sed "s: ::g" ) + echo $gpus + local/extract_sre.sh \ + --exp_dir $exp_dir --model_path $model_path \ + --nj $num_gpus_extract --gpus $gpus --data_type raw --data ${data} \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + --aug_plda_data ${aug_plda_data} +fi + + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "Export the final model ..." + python wespeaker/bin/export_jit.py \ + --config $exp_dir/config.yaml \ + --checkpoint $exp_dir/models/avg_model.pt \ + --output_file $exp_dir/models/final.zip +fi + + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "### --- Score using Cosine Distance --- ###" + + # Use SRE16 unlabeled data for mean subraction + echo "### --- Mean: SRE16 unlabeled ("SRE16 Major") --- ###" + true && { + for dset in sre16_eval;do + echo " * $dset" + local/score.sh \ + --stage 1 --stop-stage 2 \ + --trials ${trials[$dset]} \ + --xvectors $exp_dir/embeddings/${xvectors[$dset]} \ + --cal_mean_dir ${exp_dir}/embeddings/sre16/major \ + --exp_dir $exp_dir + done + } + + # Use SRE18 unlabeled data for mean subraction + echo "### --- Mean: SRE18 Unlabeled --- ###" + true && { + for dset in sre18_eval sre18_dev;do + echo " * $dset" + local/score.sh \ + --stage 1 --stop-stage 2 \ + --trials ${trials[$dset]} \ + --xvectors $exp_dir/embeddings/${xvectors[$dset]} \ + --cal_mean_dir ${exp_dir}/embeddings/sre18/dev/unlabeled \ + --exp_dir $exp_dir + done + } + + # Use backend training data for mean subraction + echo "### --- Mean: SRE --- ###" + true && { + for dset in sre16_eval sre18_eval sre18_dev sre21_eval sre21_dev;do + echo " * $dset" + local/score.sh \ + --stage 1 --stop-stage 2 \ + --trials ${trials[$dset]} \ + --xvectors $exp_dir/embeddings/${xvectors[$dset]} \ + --cal_mean_dir ${exp_dir}/embeddings/cts_aug \ + --exp_dir $exp_dir + done + } + +fi + + +if [ ${stage} -le 7 ] && [ ${stop_stage} -ge 7 ]; then + echo "### --- Score with PLDA --- ###" + echo "### --- Mean: PLDA training set (cts_aug) --- ###" + + # Here we specify the embedding preprocessing to be used before backend modelling/scoring. + mean1_scp=${exp_dir}/embeddings/${sre_plda_data}/cts_aug/xvector.scp + lda_scp=${exp_dir}/embeddings/${sre_plda_data}/cts_aug/xvector.scp + utt2spk=${data}/cts_aug/utt2spk + lda_dim=100 + preprocessing_chain="mean-subtract --scp $mean1_scp | length-norm | lda --scp $lda_scp --utt2spk $utt2spk --dim $lda_dim | length-norm" + preprocessing_path_cts_aug=${exp_dir}/embd_proc_cts_aug.pkl + + # Run stage 1-6 here to train the embedding preprocessing chain and the PLDA model as well + # as to evaluate SRE16 which is the default set to evaluate if no eval set is provided. + true && { + local/score_plda.sh \ + --stage 1 --stop-stage 6 \ + --data ${data} \ + --exp_dir $exp_dir \ + --aug_plda_data ${aug_plda_data} \ + --preprocessing_chain "$preprocessing_chain" \ + --preprocessing_path "$preprocessing_path_cts_aug" + } + # Score the other sets. We need only stage 4-6 for this. + true && { + for dset in sre18_eval sre18_dev sre21_eval sre21_dev;do + local/score_plda.sh \ + --stage 4 --stop-stage 6 \ + --data ${data} \ + --exp_dir $exp_dir \ + --enroll_scp ${enr_scp[$dset]} \ + --test_scp ${test_scp[$dset]} \ + --aug_plda_data ${aug_plda_data} \ + --preprocessing_path "$preprocessing_path" \ + --preprocessing_path "$preprocessing_path_cts_aug" \ + --utt2spk ${utt2mdl[$dset]} \ + --trials ${trials[$dset]} + done + } + + # Score using SRE 16 unlab mean. We should not retrain the backend again, i.e. stage 2-3 + # but we do need to update the embedding preprocessing chain. + mean1_scp=${exp_dir}/embeddings/sre16/major/xvector.scp + new_link="mean-subtract --scp $mean1_scp " + preprocessing_path_sre16_major=${exp_dir}/embd_proc_sre16_major.pkl + + # The following command replaces link 0 (cts_aug mean subtraction) with a new link (sre16 major mean subtraction) + python wespeaker/bin/update_embd_proc.py --in_path $preprocessing_path_cts_aug --out_path $preprocessing_path_sre16_major --link_no_to_remove 0 --new_link "$new_link" + + echo "### --- Mean: SRE16 Major --- ###" + true && { + local/score_plda.sh \ + --stage 4 --stop-stage 6 \ + --data ${data} \ + --exp_dir $exp_dir \ + --preprocessing_path "$preprocessing_path_sre16_major" + } + + # Similarly for SRE18 + mean1_scp=${exp_dir}/embeddings/sre18/dev/unlabeled/xvector.scp + new_link="mean-subtract --scp $mean1_scp " + preprocessing_path_sre18_unlab=${exp_dir}/embd_proc_sre18_dev_unlabeled.pkl + + python wespeaker/bin/update_embd_proc.py --in_path $preprocessing_path_cts_aug --out_path $preprocessing_path_sre18_unlab --link_no_to_remove 0 --new_link "$new_link" + + echo "### --- Mean: SRE18 Unlabeled --- ###" + true && { + for dset in sre18_eval sre18_dev;do + local/score_plda.sh \ + --stage 4 --stop-stage 6 \ + --data ${data} \ + --exp_dir $exp_dir \ + --preprocessing_path "$preprocessing_path_sre18_unlab" \ + --enroll_scp ${enr_scp[$dset]} \ + --test_scp ${test_scp[$dset]} \ + --utt2spk ${utt2mdl[$dset]} \ + --trials ${trials[$dset]} + done + } +fi + + +if [ ${stage} -le 8 ] && [ ${stop_stage} -ge 8 ]; then + + echo "Score with adapted PLDA ..." + + # --indomain_scp is by default sre16/major/xvector.scp in local/score_plda_adapt.sh. + # It is used for adaptation. Note that in other recipes, indomain_scp is passed to + # wespeaker/bin/eval_plda.py insdide local/score_plda_adapt.sh in which case it will + # be used for mean subtraction before scoring. In this recipe, mean subtraction is, + # however, part of the backend preprocessing chain and is therefore not used in + # wespeaker/bin/eval_plda.py. + + echo "### --- Mean: SRE16 Major --- ###" + true && { + local/score_plda_adapt.sh \ + --stage 1 --stop-stage 4 \ + --data ${data} \ + --exp_dir $exp_dir \ + --preprocessing_path ${exp_dir}/embd_proc_sre16_major.pkl \ + --aug_plda_data ${aug_plda_data} + } + + preprocessing_path_sre18_unlab=${exp_dir}/embd_proc_sre18_dev_unlabeled.pkl + echo "### --- Mean: SRE18 Unlabeled --- ###" + # Stage 1 is only needed to be run once per domain so we could have set stage 1-4 for + # sre18_eval and stage 1,3,4 for sre18_dev but since stage 2 is very fast we keep it + # in order to keep the script clean. + true && { + for dset in sre18_eval sre18_dev;do + + local/score_plda_adapt.sh \ + --stage 1 --stop-stage 4 \ + --data ${data} \ + --exp_dir $exp_dir \ + --aug_plda_data ${aug_plda_data} \ + --enroll_scp ${enr_scp[$dset]} \ + --test_scp ${test_scp[$dset]} \ + --preprocessing_path "$preprocessing_path_sre18_unlab" \ + --indomain_scp sre18/dev/unlabeled/xvector.scp \ + --utt2spk ${utt2mdl[$dset]} \ + --trials ${trials[$dset]} + done + } +fi + + +if [ ${stage} -le 9 ] && [ ${stop_stage} -ge 9 ]; then + echo "### --- Score using Cosine Distance --- ###" + + # The preprocessed embeddings are already stored but we need to create the lists as + # score.sh wants them. This is a bit messy and therefore kept in a separate script. + ./local/create_preproc_embd_lists.sh $exp_dir + + # Note that cal_mean_dir should not be provided since the embedding preprocessing includes mean subtration. + + # Use SRE16 unlabeled data for mean subraction + echo "### --- Mean: SRE16 unlabeled ("SRE16 Major") --- ###" + true && { + preproc_name=embd_proc_sre16_major + for dset in sre16_eval;do + # The xvector list for the relevant preprocessing chain. + new_xvectors=$(echo $exp_dir/embeddings/${xvectors[$dset]} | sed "s:\.scp:_proc_$preproc_name\.scp:") + echo " * $new_xvectors" + local/score.sh \ + --stage 1 --stop-stage 2 \ + --trials ${trials[$dset]} \ + --xvectors $new_xvectors \ + --exp_dir $exp_dir + done + + } + + # Use SRE18 unlabeled data for mean subraction + echo "### --- Mean: SRE18 Unlabeled --- ###" + true && { + preproc_name=embd_proc_sre18_dev_unlabeled + for dset in sre18_eval sre18_dev;do + new_xvectors=$(echo $exp_dir/embeddings/${xvectors[$dset]} | sed "s:\.scp:_proc_$preproc_name\.scp:") + echo " * $new_xvectors" + local/score.sh \ + --stage 1 --stop-stage 2 \ + --trials ${trials[$dset]} \ + --xvectors $new_xvectors \ + --exp_dir $exp_dir + done + } + + # Use backend training data for mean subraction + echo "### --- Mean: SRE --- ###" + true && { + preproc_name=embd_proc_cts_aug + for dset in sre16_eval sre18_eval sre18_dev sre21_eval sre21_dev;do + new_xvectors=$(echo $exp_dir/embeddings/${xvectors[$dset]} | sed "s:\.scp:_proc_$preproc_name\.scp:") + echo " * $new_xvectors" + local/score.sh \ + --stage 1 --stop-stage 2 \ + --trials ${trials[$dset]} \ + --xvectors $new_xvectors \ + --exp_dir $exp_dir + done + } + +fi + + + +if [ ${stage} -le 10 ] && [ ${stop_stage} -ge 10 ]; then + # Summarize results + echo "" + echo "----------------------------------------------------" + echo "### --- Summary of results (EER / minDCF0.01)--- ###" + echo "----------------------------------------------------" + # Make the header + eval_data='system' + for dset in sre16_eval sre18_dev sre18_eval sre21_dev sre21_eval;do + for x in $(echo ${trials[$dset]} | tr "," " "); do + xx=$(basename $x) + eval_data="$eval_data, $xx " + done + done + echo $eval_data > results_summary.txt + # Collect the results + for sys in mean_cts_aug_cos mean_sre16_major_cos mean_sre18_dev_unlabeled_cos \ + proc_embd_proc_cts_aug_cos proc_embd_proc_sre16_major_cos proc_embd_proc_sre18_dev_unlabeled_cos \ + proc_embd_proc_cts_aug_plda proc_embd_proc_sre16_major_plda proc_embd_proc_sre18_dev_unlabeled_plda \ + proc_embd_proc_sre16_major_plda_adapt proc_embd_proc_sre18_dev_unlabeled_plda_adapt;do + res="$sys," + for dset in sre16_eval sre18_dev sre18_eval sre21_dev sre21_eval;do + for x in $(echo ${trials[$dset]} | tr "," " "); do + xx=$(basename $x) + eval_data="$eval_data $xx " + if [ -e ${exp_dir}/scores/${xx}.${sys}.result ];then + res="$res $(grep EER ${exp_dir}/scores/${xx}.${sys}.result | sed 's:.* = ::')" + res="$res / $(grep minDCF ${exp_dir}/scores/${xx}.${sys}.result | sed 's:.* = ::')," + else + res="$res - -," + fi + done + done + echo -e $res >> results_summary.txt + done + column -t -s"," results_summary.txt + echo "" + echo "-------------------------------------------------------" + echo "### --- CSV for copy-paste to google sheet etc. --- ###" + echo "-------------------------------------------------------" + tail -n+2 results_summary.txt | sed "s:/:,:g" | sed "s: :,:g"| sed -r "s:,+:,:g" + + +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1a8fd9091743076bc3aa0e8136df666c01892dfe --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/README.md @@ -0,0 +1,10 @@ +This is a **WeSpeaker** recipe for the Voxceleb 1&2 dataset. VoxCeleb is an audio-visual dataset consisting of short clips of human speech, extracted from interview videos uploaded to YouTube. See https://www.robots.ox.ac.uk/~vgg/data/voxceleb/ for more detailed information. + +The following recipes are provided: +* v1: **Fully-Supervised** train on Voxceleb 1 development set and evaluate on Voxceleb1-O trials. + +* v2: **Fully-Supervised** train on Voxceleb 2 development set and evaluate on three official trials. + +* v2_deprecated: Deprecated version of fully-supervised train on Voxceleb dataset (deprecated IO). + +* v3: **Self-Supervised** train on Voxceleb 2 development set and evaluate on three official trials, including SimCLR, MoCo and DINO. diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fabce8ba2ab14b4ace172feb0240759b067acd76 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/README.md @@ -0,0 +1,24 @@ +## Results + +* Setup: mel80, num_frms500, epoch8, ArcMargin, aug_prob0.6, speed_perturb (no spec_aug) + +* Scoring: cosine (sub mean of vox1_dev), AS-Norm + +* Metric: EER(%) + +* 🔥 UPDATE 2024.08: We support Whisper based speaker verification framework Whisper-PMFA. Related papers: + + * [Whisper-PMFA: Partial Multi-Scale Feature Aggregation for Speaker Verification using Whisper Models ](https://arxiv.org/pdf/2408.15585) + + + +| Model | AS-Norm | Params | vox1-O-clean | +| :----------------------------------- | ------- | ------ | :----------: | +| ECAPA_TDNN_GLOB_c512-ASTP-emb192 | × | 6.19M | 2.23 | +| | √ | 6.19M | 2.00 | +| ResNet34-TSTP-emb256 | × | 6.63M | 1.99 | +| | √ | 6.63M | 1.88 | +| Whisper-PMFA | × | 478.7M | 1.62 | +| | √ | 478.7M | **1.42** | +| Whisper-PMFA with LoRA (Coming soon) | √ | 10.9M | 1.62 | + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/conf/whisper_PMFA_stage0.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/conf/whisper_PMFA_stage0.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8a5f360d57515ba67cb58c98d2911d5df8bf2a4d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/conf/whisper_PMFA_stage0.yaml @@ -0,0 +1,78 @@ +### train configuraton + +exp_dir: exp/Whisper_PMFA_large_v2_voxceleb1_mel_5s +gpus: "[0,1]" +num_avg: 1 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 4 +save_epoch_interval: 1 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 70 + num_workers: 12 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + shuffle: True + shuffle_args: + shuffle_size: 2500 + resample_rate: 16000 + speed_perturb: True + num_frms: 500 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + frontend: whisper_encoder + whisper_encoder_args: + frozen: True + n_mels: 80 + num_blocks: 24 + output_size: 1280 + n_head: 20 + layer_st: 16 + layer_ed: 23 + model_path: whisper_hub/large-v2.pt + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: Whisper_PMFA_large_v2 +model_init: null +model_args: + embed_dim: 192 +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.2 + final_margin: 0.2 + increase_start_epoch: 0 + fix_start_epoch: 4 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.0025 + final_lr: 0.00113 + warm_up_epoch: 0 + warm_from_zero: False diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/conf/whisper_PMFA_stage1.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/conf/whisper_PMFA_stage1.yaml new file mode 100644 index 0000000000000000000000000000000000000000..738525bbbf13e72f5bf9717d33973185e5257871 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/conf/whisper_PMFA_stage1.yaml @@ -0,0 +1,77 @@ +### train configuraton + +exp_dir: exp/Whisper_PMFA_large_v2_voxceleb1_mel_5s +gpus: "[0,1]" +num_avg: 1 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 8 +save_epoch_interval: 1 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 15 + num_workers: 12 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + shuffle: True + shuffle_args: + shuffle_size: 2500 + resample_rate: 16000 + speed_perturb: True + num_frms: 500 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + frontend: whisper_encoder + whisper_encoder_args: + frozen: False + n_mels: 80 + num_blocks: 24 + output_size: 1280 + n_head: 20 + layer_st: 16 + layer_ed: 23 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: Whisper_PMFA_large_v2 +model_init: null +model_args: + embed_dim: 192 +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.2 + final_margin: 0.2 + increase_start_epoch: 0 + fix_start_epoch: 8 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.0025 + final_lr: 0.00073 + warm_up_epoch: 0 + warm_from_zero: False diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/download_data.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/download_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..61f58914001cd2bc205645d4324c8a6e749fd1cf --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/download_data.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +download_dir=data/download_data + +. tools/parse_options.sh || exit 1 + +[ ! -d ${download_dir} ] && mkdir -p ${download_dir} + +if [ ! -f ${download_dir}/musan.tar.gz ]; then + echo "Downloading musan.tar.gz ..." + wget --no-check-certificate https://openslr.elda.org/resources/17/musan.tar.gz -P ${download_dir} + md5=$(md5sum ${download_dir}/musan.tar.gz | awk '{print $1}') + [ $md5 != "0c472d4fc0c5141eca47ad1ffeb2a7df" ] && echo "Wrong md5sum of musan.tar.gz" && exit 1 +fi + +if [ ! -f ${download_dir}/rirs_noises.zip ]; then + echo "Downloading rirs_noises.zip ..." + wget --no-check-certificate https://us.openslr.org/resources/28/rirs_noises.zip -P ${download_dir} + md5=$(md5sum ${download_dir}/rirs_noises.zip | awk '{print $1}') + [ $md5 != "e6f48e257286e05de56413b4779d8ffb" ] && echo "Wrong md5sum of rirs_noises.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox1_test_wav.zip ]; then + echo "Downloading vox1_test_wav.zip ..." + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_test_wav.zip -P ${download_dir} + md5=$(md5sum ${download_dir}/vox1_test_wav.zip | awk '{print $1}') + [ $md5 != "185fdc63c3c739954633d50379a3d102" ] && echo "Wrong md5sum of vox1_test_wav.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox1_dev_wav.zip ]; then + echo "Downloading vox1_dev_wav.zip ..." + for part in a b c d; do + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_dev_wav_parta${part} -P ${download_dir} & + done + wait + cat ${download_dir}/vox1_dev* >${download_dir}/vox1_dev_wav.zip + md5=$(md5sum ${download_dir}/vox1_dev_wav.zip | awk '{print $1}') + [ $md5 != "ae63e55b951748cc486645f532ba230b" ] && echo "Wrong md5sum of vox1_dev_wav.zip" && exit 1 +fi + + +echo "Download success !!!" diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/download_whisper.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/download_whisper.sh new file mode 100644 index 0000000000000000000000000000000000000000..d0bf7a6b85b362a5d7c201cbf6d1ff88fc9b3c7e --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/download_whisper.sh @@ -0,0 +1,13 @@ +download_dir=data/whisper_pretrained_model + +. tools/parse_options.sh || exit 1 + +[ ! -d ${download_dir} ] && mkdir -p ${download_dir} + +if [ ! -f ${download_dir}/large-v2.pt ]; then + echo "Downloading large-v2.pt ..." + wget --no-check-certificate https://openaipublic.azureedge.net/main/whisper/models/81f7c96c852ee8fc832187b0132e569d6c3065a3252ed18e56effd0b6a73e524/large-v2.pt -P ${download_dir} + md5=$(md5sum ${download_dir}/large-v2.pt | awk '{print $1}') + [ $md5 != "668764447eeda98eeba5ef7bfcb4cc3d" ] && echo "Wrong md5sum of musan.tar.gz" && exit 1 +fi + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/extract_vox.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/extract_vox.sh new file mode 100644 index 0000000000000000000000000000000000000000..613012c1a437870cf0cdd16af795bc1cfe9a4121 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/extract_vox.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir='' +model_path='' +nj=4 +gpus="[0,1]" +data_type="shard" # shard/raw/feat +data=data + +. tools/parse_options.sh +set -e + +data_name_array=("vox1_dev" "vox1_test") +data_list_path_array=("${data}/vox1_dev/${data_type}.list" "${data}/vox1_test/${data_type}.list") +data_scp_path_array=("${data}/vox1_dev/wav.scp" "${data}/vox1_test/wav.scp") # to count the number of wavs +nj_array=($nj $nj) +batch_size_array=(16 1) # batch_size of test set must be 1 !!! +num_workers_array=(4 1) +count=${#data_name_array[@]} + +for i in $(seq 0 $(($count - 1))); do + wavs_num=$(wc -l ${data_scp_path_array[$i]} | awk '{print $1}') + bash tools/extract_embedding.sh --exp_dir ${exp_dir} \ + --model_path $model_path \ + --data_type ${data_type} \ + --data_list ${data_list_path_array[$i]} \ + --wavs_num ${wavs_num} \ + --store_dir ${data_name_array[$i]} \ + --batch_size ${batch_size_array[$i]} \ + --num_workers ${num_workers_array[$i]} \ + --nj ${nj_array[$i]} \ + --gpus $gpus & +done + +wait + +echo "Embedding dir is (${exp_dir}/embeddings)." diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/prepare_data.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/prepare_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..6b55499e5c40c4d981c220ad31f32bbae79979a2 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/prepare_data.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +stage=-1 +stop_stage=-1 +data=data + +. tools/parse_options.sh || exit 1 + +data=`realpath ${data}` +download_dir=${data}/download_data +rawdata_dir=${data}/raw_data + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Download musan.tar.gz, rirs_noises.zip, vox1_test_wav.zip, and vox1_dev_wav.zip." + echo "This may take a long time. Thus we recommand you to download all archives above in your own way first." + + ./local/download_data.sh --download_dir ${download_dir} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Decompress all archives ..." + echo "This could take some time ..." + + for archive in musan.tar.gz rirs_noises.zip vox1_test_wav.zip vox1_dev_wav.zip; do + [ ! -f ${download_dir}/$archive ] && echo "Archive $archive not exists !!!" && exit 1 + done + [ ! -d ${rawdata_dir} ] && mkdir -p ${rawdata_dir} + + if [ ! -d ${rawdata_dir}/musan ]; then + tar -xzvf ${download_dir}/musan.tar.gz -C ${rawdata_dir} + fi + + if [ ! -d ${rawdata_dir}/RIRS_NOISES ]; then + unzip ${download_dir}/rirs_noises.zip -d ${rawdata_dir} + fi + + if [ ! -d ${rawdata_dir}/voxceleb1 ]; then + mkdir -p ${rawdata_dir}/voxceleb1/test ${rawdata_dir}/voxceleb1/dev + unzip ${download_dir}/vox1_test_wav.zip -d ${rawdata_dir}/voxceleb1/test + unzip ${download_dir}/vox1_dev_wav.zip -d ${rawdata_dir}/voxceleb1/dev + fi + + echo "Decompress success !!!" +fi + + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Prepare wav.scp for each dataset ..." + export LC_ALL=C # kaldi config + + mkdir -p ${data}/musan ${data}/rirs ${data}/vox1_dev ${data}/vox1_test + # musan + find ${rawdata_dir}/musan -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' >${data}/musan/wav.scp + # rirs + find ${rawdata_dir}/RIRS_NOISES/simulated_rirs -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' >${data}/rirs/wav.scp + # vox1 dev + find ${rawdata_dir}/voxceleb1/dev -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' | sort >${data}/vox1_dev/wav.scp + awk '{print $1}' ${data}/vox1_dev/wav.scp | awk -F "/" '{print $0,$1}' >${data}/vox1_dev/utt2spk + ./tools/utt2spk_to_spk2utt.pl ${data}/vox1_dev/utt2spk >${data}/vox1_dev/spk2utt + # vox1 test + find ${rawdata_dir}/voxceleb1/test -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' | sort >${data}/vox1_test/wav.scp + awk '{print $1}' ${data}/vox1_test/wav.scp | awk -F "/" '{print $0,$1}' >${data}/vox1_test/utt2spk + ./tools/utt2spk_to_spk2utt.pl ${data}/vox1_test/utt2spk >${data}/vox1_test/spk2utt + + if [ ! -d ${data}/vox1_test/trials ]; then + echo "Download trials for vox1_test ..." + mkdir -p ${data}/vox1_test/trials + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test.txt -O ${data}/vox1_test/trials/vox1-O.txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test2.txt -O ${data}/vox1_test/trials/vox1-O\(cleaned\).txt + # transform them into kaldi trial format + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1_test/trials/vox1-O\(cleaned\).txt >${data}/vox1_test/trials/vox1_O_cleaned.kaldi + fi + + echo "Success !!!" +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/score.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/score.sh new file mode 100644 index 0000000000000000000000000000000000000000..b4f89129439229239ea32c39001fc1b1912f8957 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/score.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir= +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +data=data + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "apply cosine scoring ..." + mkdir -p ${exp_dir}/scores + trials_dir=${data}/vox1_test/trials + for x in $trials; do + echo $x + python wespeaker/bin/score.py \ + --exp_dir ${exp_dir} \ + --eval_scp_path ${exp_dir}/embeddings/vox1_test/xvector.scp \ + --cal_mean True \ + --cal_mean_dir ${exp_dir}/embeddings/vox1_dev \ + ${trials_dir}/${x} + done +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "compute metrics (EER/minDCF) ..." + scores_dir=${exp_dir}/scores + for x in $trials; do + python wespeaker/bin/compute_metrics.py \ + --p_target 0.05 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${x}.score \ + 2>&1 | tee -a ${scores_dir}/vox1_cos_result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${x}.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/score_norm.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/score_norm.sh new file mode 100644 index 0000000000000000000000000000000000000000..48a4b6c5efee4cc188ab9c31d7a9751937ff69cc --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/local/score_norm.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +score_norm_method="asnorm" # asnorm/snorm +cohort_set=vox2_dev +top_n=100 +exp_dir= +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +data=data + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + echo "compute mean xvector" + python tools/vector_mean.py \ + --spk2utt ${data}/${cohort_set}/spk2utt \ + --xvector_scp $exp_dir/embeddings/${cohort_set}/xvector.scp \ + --spk_xvector_ark $exp_dir/embeddings/${cohort_set}/spk_xvector.ark +fi + +output_name=${cohort_set}_${score_norm_method} +[ "${score_norm_method}" == "asnorm" ] && output_name=${output_name}${top_n} +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + echo "compute norm score" + for x in $trials; do + python wespeaker/bin/score_norm.py \ + --score_norm_method $score_norm_method \ + --top_n $top_n \ + --trial_score_file $exp_dir/scores/${x}.score \ + --score_norm_file $exp_dir/scores/${output_name}_${x}.score \ + --cohort_emb_scp ${exp_dir}/embeddings/${cohort_set}/spk_xvector.scp \ + --eval_emb_scp ${exp_dir}/embeddings/vox1_test/xvector.scp \ + --mean_vec_path ${exp_dir}/embeddings/vox1_dev/mean_vec.npy + done +fi + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + echo "compute metrics" + for x in ${trials}; do + scores_dir=${exp_dir}/scores + python wespeaker/bin/compute_metrics.py \ + --p_target 0.05 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${output_name}_${x}.score \ + 2>&1 | tee -a ${scores_dir}/vox1_${score_norm_method}${top_n}_result + + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${output_name}_${x}.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/path.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/path.sh new file mode 100644 index 0000000000000000000000000000000000000000..e7917ccb32b3a746a5eebbcfa445aa280bb39ed0 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/path.sh @@ -0,0 +1,5 @@ +export PATH=$PWD:$PATH + +# NOTE(kan-bayashi): Use UTF-8 in Python to avoid UnicodeDecodeError when LC_ALL=C +export PYTHONIOENCODING=UTF-8 +export PYTHONPATH=../../../:$PWD:$PYTHONPATH diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..0b12d006d204f68fa73fd04567fc8d485d6040f3 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v1/Whisper-PMFA/run.sh @@ -0,0 +1,138 @@ +#!/bin/bash + +# Copyright 2024 Yiyang Zhao (zhaoyy22@mails.tsinghua.edu.cn) +# 2024 Hongji Wang (jijijiang77@gmail.com) + +. ./path.sh || exit 1 + +stage=-1 +stop_stage=-1 + +HOST_NODE_ADDR="localhost:29400" +num_nodes=1 +job_id=2024 + +data=data +data_type="raw" # shard/raw +model=whisper_PMFA_large_v2 + +exp_dir=exp/Whisper_PMFA_large_v2_voxceleb1_mel_5s +gpus="[0,1]" +num_avg=1 +checkpoint= + +trials="vox1_O_cleaned.kaldi" + +score_norm_method="asnorm" # asnorm/snorm +top_n=300 + +. tools/parse_options.sh || exit 1 +if ! pip show openai-whisper > /dev/null 2>&1; then + pip install openai-whisper==20231117 +fi + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Preparing datasets ..." + ./local/prepare_data.sh --stage 1 --stop_stage 3 --data ${data} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Covert train and test data to ${data_type}..." + for dset in vox1_dev vox1_test; do + if [ $data_type == "shard" ]; then + python tools/make_shard_list.py --num_utts_per_shard 1000 \ + --num_threads 16 \ + --prefix shards \ + --shuffle \ + ${data}/$dset/wav.scp ${data}/$dset/utt2spk \ + ${data}/$dset/shards ${data}/$dset/shard.list + else + python tools/make_raw_list.py ${data}/$dset/wav.scp \ + ${data}/$dset/utt2spk ${data}/$dset/raw.list + fi + done + # Convert all musan data to LMDB + python tools/make_lmdb.py ${data}/musan/wav.scp ${data}/musan/lmdb + # Convert all rirs data to LMDB + python tools/make_lmdb.py ${data}/rirs/wav.scp ${data}/rirs/lmdb +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Start training with frozen whisper parameter..." + config=conf/whisper_PMFA_stage0.yaml + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/bin/train.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + --data_type "${data_type}" \ + --train_data ${data}/vox1_dev/${data_type}.list \ + --train_label ${data}/vox1_dev/utt2spk \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + --model ${model} +fi + + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Start training with all parameter..." + + if [ -f ${exp_dir}/"config.yaml" ]; then + mv ${exp_dir}/"config.yaml" ${exp_dir}/"config_stage0.yaml" + fi + if [ -f ${exp_dir}/models/"final_model.pt" ]; then + mv ${exp_dir}/models/"final_model.pt" ${exp_dir}/models/"final_model_stage0.pt" + fi + + config=conf/whisper_PMFA_stage1.yaml + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + checkpoint=${exp_dir}/models/model_4.pt + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/bin/train.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + --data_type "${data_type}" \ + --train_data ${data}/vox1_dev/${data_type}.list \ + --train_label ${data}/vox1_dev/utt2spk \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + --model ${model} \ + --checkpoint ${checkpoint} +fi + + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + model_path=$exp_dir/models/final_model.pt + echo "Extract embeddings ..." + local/extract_vox.sh \ + --exp_dir $exp_dir --model_path $model_path \ + --nj 2 --gpus $gpus --data_type raw --data ${data} +fi + + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "Score ..." + local/score.sh \ + --stage 1 --stop-stage 2 \ + --exp_dir $exp_dir \ + --data ${data} \ + --trials "$trials" +fi + +if [ ${stage} -le 7 ] && [ ${stop_stage} -ge 7 ]; then + echo "Score norm ..." + local/score_norm.sh \ + --stage 1 --stop-stage 3 \ + --score_norm_method $score_norm_method \ + --cohort_set vox1_dev \ + --top_n $top_n \ + --exp_dir $exp_dir \ + --data ${data} \ + --trials "$trials" +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/README.md new file mode 100644 index 0000000000000000000000000000000000000000..91fe1c74980544b7f27756b8417aeb9e1930a163 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/README.md @@ -0,0 +1,163 @@ +## Results + +* Setup: fbank80, num_frms200, epoch150, ArcMargin, aug_prob0.6, speed_perturb (no spec_aug) +* Scoring: cosine (sub mean of vox2_dev), AS-Norm, [QMF](https://arxiv.org/pdf/2010.11255) +* Metric: EER(%) +* 🔥 UPDATE 2024.09.03: We support the SimAM_ResNet pretrained on VoxBlink2 and Finetuned on Voxceleb2! +* 🔥 UPDATE 2024.08.27: We support SSL models as the feature front-end, take a look at the WavLM recipe! +* UPDATE 2022.07.19: We apply the same setups as the winning system of CNSRC 2022 (see [cnceleb](https://github.com/wenet-e2e/wespeaker/tree/master/examples/cnceleb/v2) recipe for details), and obtain significant performance improvement. + * LR scheduler warmup from 0 + * Remove one embedding layer in ResNet models + * Add large margin fine-tuning strategy (LM) + +| Model | Params | Flops | LM | AS-Norm | QMF | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:------|:------:|:------|:--:|:-------:|:---:|:------------:|:------------:|:------------:| +| XVEC-TSTP-emb512 | 4.61M | 0.53G | × | × | × | 1.989 | 1.950 | 3.412 | +| | | | × | √ | × | 1.834 | 1.846 | 3.124 | +| | | | √ | × | × | 1.749 | 1.721 | 2.944 | +| | | | √ | √ | × | 1.590 | 1.641 | 2.726 | +| ECAPA_TDNN_GLOB_c512-ASTP-emb192 | 6.19M | 1.04G | × | × | × | 1.069 | 1.209 | 2.310 | +| | | | × | √ | × | 0.957 | 1.128 | 2.105 | +| | | | √ | × | × | 0.878 | 1.072 | 2.007 | +| | | | √ | √ | × | 0.782 | 1.005 | 1.824 | +| ECAPA_TDNN_GLOB_c1024-ASTP-emb192 | 14.65M | 2.65G | × | × | × | 0.856 | 1.072 | 2.059 | +| | | | × | √ | × | 0.808 | 0.990 | 1.874 | +| | | | √ | × | × | 0.798 | 0.993 | 1.883 | +| | | | √ | √ | × | 0.728 | 0.929 | 1.721 | +| | | | √ | √ | √ | 0.707 | 0.894 | 1.615 | +| ResNet34-TSTP-emb256 | 6.63M | 4.55G | × | × | × | 0.867 | 1.049 | 1.959 | +| | | | × | √ | × | 0.787 | 0.964 | 1.726 | +| | | | × | √ | √ | 0.718 | 0.911 | 1.606 | +| | | | √ | × | × | 0.797 | 0.937 | 1.695 | +| | | | √ | √ | × | 0.723 | 0.867 | 1.532 | +| | | | √ | √ | √ | 0.659 | 0.821 | 1.437 | +| ResNet221-TSTP-emb256 | 23.79M | 21.29G | × | × | × | 0.569 | 0.774 | 1.464 | +| | | | × | √ | × | 0.479 | 0.707 | 1.290 | +| | | | √ | × | × | 0.580 | 0.729 | 1.351 | +| | | | √ | √ | × | 0.505 | 0.676 | 1.213 | +| ResNet293-TSTP-emb256 | 28.62M | 28.10G | × | × | × | 0.595 | 0.756 | 1.433 | +| | | | × | √ | × | 0.537 | 0.701 | 1.276 | +| | | | √ | × | × | 0.532 | 0.707 | 1.311 | +| | | | √ | √ | × | 0.447 | 0.657 | 1.183 | +| | | | √ | √ | √ | **0.425** | **0.641** | **1.146** | +| RepVGG_TINY_A0 | 6.26M | 4.65G | × | × | × | 0.909 | 1.034 | 1.943 | +| | | | × | √ | × | 0.824 | 0.953 | 1.709 | +| CAM++ | 7.18M | 1.15G | × | × | × | 0.803 | 0.932 | 1.860 | +| | | | × | √ | × | 0.718 | 0.879 | 1.735 | +| | | | √ | x | × | 0.707 | 0.845 | 1.664 | +| | | | √ | √ | × | 0.659 | 0.803 | 1.569 | +| ERes2Net34_Base | 7.88M | 3.43G | × | × | × | 0.914 | 1.065 | 1.986 | +| | | | × | √ | × | 0.803 | 0.976 | 1.787 | +| | | | √ | x | × | 0.824 | 0.968 | 1.776 | +| | | | √ | √ | × | 0.744 | 0.896 | 1.603 | +| Res2Net34_Base | 4.68M | 1.77G | × | × | × | 1.351 | 1.347 | 2.478 | +| | | | × | √ | × | 1.234 | 1.232 | 2.162 | +| Gemini_DFResNet114 | 6.53M | 5.42G | × | × | × | 0.787 | 0.963 | 1.760 | +| | | | × | √ | × | 0.707 | 0.889 | 1.546 | +| | | | √ | x | × | 0.771 | 0.906 | 1.599 | +| | | | √ | √ | × | 0.638 | 0.839 | 1.427 | +| SimAM_ResNet34 (VoxBlink2 Pretrain) | 25.2M | | √ | x | × | 0.415 | 0.615 | 1.121 | +| | | | √ | √ | × | 0.372 | 0.581 | 1.049 | +| | | | √ | √ | √ | 0.372 | 0.559 | 0.997 | +| SimAM_ResNet100 (VoxBlink2 Pretrain) | 50.2M | | √ | x | × | 0.229 | 0.458 | 0.868 | +| | | | √ | √ | × | 0.207 | 0.424 | 0.804 | +| | | | √ | √ | √ | 0.202 | 0.421 | 0.795 | +| XI_VEC_ECAPA_TDNN_c512 | 5.9M | 1.04G | x | x | × | 0.995 | 1.130 | 2.169 | +| | | | × | √ | × | 0.883 | 1.056 | 1.976 | +| | | | √ | × | × | 0.909 | 1.000 | 1.855 | +| | | | √ | √ | × | 0.787 | 0.930 | 1.693 | + + +## PLDA results +If you are interested in the PLDA scoring (which is inferior to the simple cosine scoring under the margin based setting), simply run: + +```bash +local/score_plda.sh --stage 1 --stop-stage 3 --exp_dir exp_name +``` + +The results on ResNet34 (large margin, no asnorm) are: + +| Scoring method | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:--------------:|:------------:|:------------:|:------------:| +| PLDA | 1.207 | 1.350 | 2.528 | + + +## WavLM results + +* Pre-trained frontend: the [WavLM](https://arxiv.org/abs/2110.13900) Large model, multilayer features are used +* Speaker model: ECAPA_TDNN_GLOB_c512-ASTP-emb192 +* Training strategy: Frozen => Joint ft => Joint lmft + +```bash +bash run_wavlm.sh --stage 3 --stop_stage 9 +``` + +| Training strategy | AS-Norm | QMF | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:------------------|:-------:|:---:|:------------:|:------------:|:------------:| +| Frozen | × | × | 0.595 | 0.719 | 1.501 | +| | √ | × | 0.548 | 0.656 | 1.355 | +| | √ | √ | 0.489 | 0.619 | 1.224 | +| Frozen => Joint ft | × | × | 0.542 | 0.635 | 1.355 | +| | √ | × | 0.521 | 0.594 | 1.237 | +| | √ | √ | 0.494 | 0.576 | 1.205 | +| Frozen => Joint ft => Joint lmft | × | × | 0.521 | 0.626 | 1.344 | +| | √ | × | 0.495 | 0.588 | 1.247 | +| | √ | √ | **0.415** | **0.551** | **1.118** | + + + + +### W2V-BERT 2.0 Results + +* Pre-trained frontend: the [W2V-BERT 2.0](https://arxiv.org/abs/2108.06209) model +* Speaker model: w2v-bert2-LoRA-adapter-MFA +* Training strategy: Frozen (LoRA) =\> Joint ft =\> Joint lmft + + + +```bash +bash run_w2v.sh --stage 3 --stop_stage 9 +``` + +| Training strategy | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:------------------|:------------:|:------------:|:------------:| +| Frozen (LoRA) | 0.335 | 0.509 | 1.062 | +| Frozen (LoRA) =\> Joint ft | 0.319 | 0.492 | 1.036 | +| Frozen (LoRA) =\> Joint ft =\> Joint lmft | **0.250** | **0.398** | **0.838** | + +> **Note:** The results above are reproduced on **VoxCeleb** only. +> They use whole-utterance cosine scoring with Vox2-dev mean normalization, without AS-Norm, QMF, or calibration. +> +> We also verified our implementation by loading the author's official checkpoint (trained on **VoxCeleb + VoxBlink**) and performing inference. The comparison below confirms that our inference pipeline matches the official performance. + +| Source | Strategy | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:-------|:--------:|:------------:|:------------:|:------------:| +| Author's GitHub | LMFT | 0.14 | 0.31 | 0.73 | +| **This PR** (w/ Author's Ckpt) | LMFT (w/ AS-Norm & QMF) | **0.138** | **0.285** | **0.625** | + +## HAM-Softmax Results + +HAM-Softmax is an additive margin softmax based on Poincare ball hyperbolic distance. See the [paper](https://arxiv.org/abs/2601.19709) and [official implementation](https://github.com/PunkMale/HAM-Softmax). + +All results use margin warmup 0.0 -> 0.2, speed perturb, aug_prob=0.6, no spec_aug, model averaging, AS-Norm + calibration. + +| Model | Loss | Curvature | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:------|:----:|:---------:|:------------:|:------------:|:------------:| +| ECAPA_TDNN_GLOB_c512-ASTP-emb192 | AAM-Softmax | - | 0.909 | 1.075 | 1.976 | +| ECAPA_TDNN_GLOB_c512-ASTP-emb192 | HAM-Softmax | 3.0 | 0.872 | 1.040 | 1.856 | +| ResNet34-TSTP-emb256 | AAM-Softmax | - | 0.691 | 0.895 | 1.596 | +| ResNet34-TSTP-emb256 | HAM-Softmax | 3.0 | 0.702 | 0.886 | 1.547 | + +## ReDimNet2 Results + +* See the [Paper](https://arxiv.org/abs/2603.11841) and [Official Implementation](https://github.com/PalabraAI/redimnet2) +* Model: ReDimNet2B6 +* Frontend: TFMel (n_mels=72, hop_length=160, win_length=400, ...) +* Training strategy: SphereFace2 => Large margin fine-tuning +* Note: batch size significantly affects performance; a large batch size is recommended + +| Source | LM | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:-------|:--:|:------------:|:------------:|:------------:| +| Author's GitHub | √ | 0.29 | 0.52 | 0.99 | +| This PR (w/ Official Ckpt) | √ | **0.276** | 0.518 | 1.003 | +| This PR (trained from scratch) | √ | 0.330 | **0.502** | **0.985** | \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/campplus.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/campplus.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6edecc46306ad11013a0b5ba3bab1cacba62889a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/campplus.yaml @@ -0,0 +1,82 @@ +### train configuraton + +exp_dir: exp/CAMPPlus-TSTP-emb512-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: CAMPPlus +model_init: null +model_args: + feat_dim: 80 + embed_dim: 512 + pooling_func: "TSTP" # the default pooling_func in CAM++ is TSTP +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/campplus_lm.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/campplus_lm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0a41afc9acd32fdeb8ebafc16e64ad6833d94ad --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/campplus_lm.yaml @@ -0,0 +1,88 @@ +### Large margin fine-tuning configuration +# +# The large margin fine-tuning operation is often used in speaker +# verification challenge system to further improve the performance. +# In this fine-tuning stage, large margin and longer segment will +# be used. + +exp_dir: exp/CAMPPlus-TSTP-emb512-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150-LM +gpus: "[0,1]" +num_avg: 1 +enable_amp: False # whether enable automatic mixed precision training +do_lm: True + +seed: 42 +num_epochs: 5 +save_epoch_interval: 1 # save model per epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 64 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 600 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: CAMPPlus +model_init: null +model_args: + feat_dim: 80 + embed_dim: 512 + pooling_func: "TSTP" # the default pooling_func in CAM++ is TSTP +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.5 + final_margin: 0.5 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 1.0e-4 + final_lr: 2.5e-5 + warm_up_epoch: 1 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4602918f6040eba8806b14518d1521f878f67e57 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn.yaml @@ -0,0 +1,83 @@ +### train configuraton + +exp_dir: exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + frontend: "fbank" # fbank, s3prl + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ECAPA_TDNN_GLOB_c512 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 192 + pooling_func: "ASTP" # the default pooling_func in ECAPA_TDNN is ASTP +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_WavLM_frozen.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_WavLM_frozen.yaml new file mode 100644 index 0000000000000000000000000000000000000000..826b3d4616296ace036b305a4d9c9d8ea4787c10 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_WavLM_frozen.yaml @@ -0,0 +1,91 @@ +### train configuraton + +exp_dir: exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-WavLM_Large_frozen-num_frms150-aug0.6-spTrue-saFalse-ArcMargin_intertopk_subcenter-SGD-epoch150 +gpus: "[0,1,2,3,4,5,6,7]" +num_avg: 10 +enable_amp: True # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 256 + num_workers: 16 + pin_memory: False + prefetch_factor: 16 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 50 + max_num_frames: 400 + resample_rate: 16000 + speed_perturb: True + num_frms: 150 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + frontend: "s3prl" # fbank, s3prl + s3prl_args: + upstream_args: + name: "wavlm_large" + download_dir: ./s3prl_hub + multilayer_feature: True + layer: -1 + frozen: True + frame_shift: 20 + frame_length: 20 + cmvn: True + cmvn_args: + norm_mean: True + norm_var: False + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ECAPA_TDNN_GLOB_c512 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: -1 # equals to the output_size of the frontend (will be initialized before training) + embed_dim: 192 + pooling_func: "ASTP" # the default pooling_func in ECAPA_TDNN is ASTP +projection_args: + project_type: "arc_margin_intertopk_subcenter" # add_margin, arc_margin, sphere, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00001 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_WavLM_joint_ft.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_WavLM_joint_ft.yaml new file mode 100644 index 0000000000000000000000000000000000000000..69961309a4d585325a907587c76097c7eedfb82b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_WavLM_joint_ft.yaml @@ -0,0 +1,92 @@ +### train configuraton + +exp_dir: exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-WavLM_Large_joint_ft-num_frms150-aug0.6-spTrue-saFalse-ArcMargin_intertopk_subcenter-SGD-epoch20 +gpus: "[0,1,2,3,4,5,6,7]" +num_avg: 3 +enable_amp: True # whether enable automatic mixed precision training +do_lm: False + +seed: 42 +num_epochs: 20 +save_epoch_interval: 1 # save model every epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 64 + num_workers: 8 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 50 + max_num_frames: 400 + resample_rate: 16000 + speed_perturb: True + num_frms: 150 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + frontend: "s3prl" # fbank, s3prl + s3prl_args: + upstream_args: + name: "wavlm_large" + download_dir: ./s3prl_hub + multilayer_feature: True + layer: -1 + frozen: False + frame_shift: 20 + frame_length: 20 + cmvn: True + cmvn_args: + norm_mean: True + norm_var: False + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ECAPA_TDNN_GLOB_c512 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: -1 # equals to the output_size of the frontend (will be initialized before training) + embed_dim: 192 + pooling_func: "ASTP" # the default pooling_func in ECAPA_TDNN is ASTP +projection_args: + project_type: "arc_margin_intertopk_subcenter" # add_margin, arc_margin, sphere, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.2 + final_margin: 0.2 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.001 + final_lr: 0.00025 + warm_up_epoch: 1 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_WavLM_joint_lmft.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_WavLM_joint_lmft.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8fd1b854b9e3241726f0beb3ac03e9e53f1346b5 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_WavLM_joint_lmft.yaml @@ -0,0 +1,92 @@ +### train configuraton + +exp_dir: exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-WavLM_Large_joint_lmft-num_frms300-aug0.6-spTrue-saFalse-ArcMargin_intertopk_subcenter-SGD-epoch10 +gpus: "[0,1,2,3,4,5,6,7]" +num_avg: 1 +enable_amp: True # whether enable automatic mixed precision training +do_lm: True + +seed: 42 +num_epochs: 20 +save_epoch_interval: 1 # save model every epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 32 + num_workers: 8 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 50 + max_num_frames: 400 + resample_rate: 16000 + speed_perturb: True + num_frms: 300 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + frontend: "s3prl" # fbank, s3prl + s3prl_args: + upstream_args: + name: "wavlm_large" + download_dir: ./s3prl_hub + multilayer_feature: True + layer: -1 + frozen: False + frame_shift: 20 + frame_length: 20 + cmvn: True + cmvn_args: + norm_mean: True + norm_var: False + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ECAPA_TDNN_GLOB_c512 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: -1 # equals to the output_size of the frontend (will be initialized before training) + embed_dim: 192 + pooling_func: "ASTP" # the default pooling_func in ECAPA_TDNN is ASTP +projection_args: + project_type: "arc_margin_intertopk_subcenter" # add_margin, arc_margin, sphere, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.5 + final_margin: 0.5 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.0001 + final_lr: 0.000025 + warm_up_epoch: 1 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_lm.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_lm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..04178cc9ea9a51176f169faf4d6467b7f8eca29b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/ecapa_tdnn_lm.yaml @@ -0,0 +1,89 @@ +### Large margin fine-tuning configuration +# +# The large margin fine-tuning operation is often used in speaker +# verification challenge system to further improve the performance. +# In this fine-tuning stage, large margin and longer segment will +# be used. + +exp_dir: exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150-LM +gpus: "[0,1]" +num_avg: 1 +enable_amp: False # whether enable automatic mixed precision training +do_lm: True + +seed: 42 +num_epochs: 5 +save_epoch_interval: 1 # save model per epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 64 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 600 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + frontend: "fbank" # fbank, s3prl + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ECAPA_TDNN_GLOB_c512 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 192 + pooling_func: "ASTP" # the default pooling_func in ECAPA_TDNN is ASTP +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.5 + final_margin: 0.5 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 1.0e-4 + final_lr: 2.5e-5 + warm_up_epoch: 1 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/eres2net.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/eres2net.yaml new file mode 100644 index 0000000000000000000000000000000000000000..50e8ccfdbf4ab6e73cd679a615ec13d3c0c4aa4b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/eres2net.yaml @@ -0,0 +1,83 @@ +### train configuraton + +exp_dir: exp/ERes2Net34_Base-TSTP-emb512-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ERes2Net34_Base # ERes2Net34_Base, ERes2Net34_Large +model_init: null +model_args: + feat_dim: 80 + embed_dim: 512 # 512, 192 + pooling_func: "TSTP" + two_emb_layer: False +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, sphereface2, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/eres2net_lm.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/eres2net_lm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..969d381c2306f5840e7acad637bb87cf8a44cbd1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/eres2net_lm.yaml @@ -0,0 +1,89 @@ +### Large margin fine-tuning configuration +# +# The large margin fine-tuning operation is often used in speaker +# verification challenge system to further improve the performance. +# In this fine-tuning stage, large margin and longer segment will +# be used. + +exp_dir: exp/ERes2Net34_Base-TSTP-emb512-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150-LM +gpus: "[0,1]" +num_avg: 1 +enable_amp: False # whether enable automatic mixed precision training +do_lm: True + +seed: 42 +num_epochs: 5 +save_epoch_interval: 1 # save model per epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 32 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 600 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ERes2Net34_Base # ERes2Net34_Base, ERes2Net34_Large +model_init: null +model_args: + feat_dim: 80 + embed_dim: 512 # 512, 192 + pooling_func: "TSTP" + two_emb_layer: False +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.5 + final_margin: 0.5 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 1.0e-4 + final_lr: 2.5e-5 + warm_up_epoch: 1 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/gemini_dfresnet_adam.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/gemini_dfresnet_adam.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f493fddec2c76810da9976b29d74f87db2809e8b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/gemini_dfresnet_adam.yaml @@ -0,0 +1,81 @@ +### train configuraton + +exp_dir: exp/Gemini_DF_ResNet114-TSTP-emb256-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-AdamW-epoch165 +gpus: "[0,1]" +num_avg: 2 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 165 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 8 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: Gemini_DF_ResNet114 # Gemini_DF_ResNet60 Gemini_DF_ResNet114 GemGemini_DF_ResNet183 Gemini_DF_ResNet237 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 256 + pooling_func: "TSTP" # TSTP, ASTP, MQMHASTP + two_emb_layer: False +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, sphereface2, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.2 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: False + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: AdamW +optimizer_args: + weight_decay: 0.05 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.000125 + final_lr: 0.000001 + warm_up_epoch: 6 + warm_from_zero: False diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/gemini_dfresnet_sgd_lm.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/gemini_dfresnet_sgd_lm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3dd33bf733831e1a55cce0d94527b1357faf220d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/gemini_dfresnet_sgd_lm.yaml @@ -0,0 +1,91 @@ +### Large margin fine-tuning configuration +# +# The large margin fine-tuning operation is often used in speaker +# verification challenge system to further improve the performance. +# In this fine-tuning stage, large margin and longer segment will +# be used. + +exp_dir: exp/Gemini_DF_ResNet114-TSTP-emb256-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-AdamW-epoch165-LM +gpus: "[0,1]" +num_avg: 1 +enable_amp: False # whether enable automatic mixed precision training +do_lm: True + +seed: 42 +num_epochs: 5 +save_epoch_interval: 1 # save model per epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 32 + num_workers: 8 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 600 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: Gemini_DF_ResNet114 # ResNet18, ResNet34, ResNet50, ResNet101, ResNet152 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 256 + pooling_func: "TSTP" # TSTP, ASTP, MQMHASTP + two_emb_layer: False +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.5 + final_margin: 0.5 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 1.0e-4 + final_lr: 2.5e-5 + warm_up_epoch: 1 + warm_from_zero: True + + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/redimnet.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/redimnet.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6e8fb2958adfa389db6313fe3acd90499af42519 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/redimnet.yaml @@ -0,0 +1,86 @@ +exp_dir: exp/RedimnetB2-emb192-fbank72-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch120 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 120 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 256 + num_workers: 4 + pin_memory: false + prefetch_factor: 4 + drop_last: true + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 72 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ReDimNetB2 +model_init: null +model_args: + feat_dim: 72 + embed_dim: 192 + pooling_func: "ASTP" # TSTP, ASTP, MQMHASTP + two_emb_layer: False + + +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, sphereface2, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + update_margin: true + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 2.0e-05 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/redimnet2.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/redimnet2.yaml new file mode 100644 index 0000000000000000000000000000000000000000..37f70d03a98f270d3826e5c4e5d1735f4995e694 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/redimnet2.yaml @@ -0,0 +1,84 @@ +exp_dir: exp/ReDimNet2B6-tfmel-emb192-fbank72-num_frms300-aug0.6-spTrue-saFalse-SphereFace2-SGD-epoch120 +gpus: "[0,1]" +num_avg: 10 +enable_amp: false +data_type: shard + +seed: 42 +num_epochs: 120 +save_epoch_interval: 5 +log_batch_interval: 100 + +dataloader_args: + batch_size: 512 + num_workers: 4 + pin_memory: false + prefetch_factor: 4 + drop_last: true + +dataset_args: + frontend: tfmel + sample_num_per_epoch: 0 + shuffle: true + shuffle_args: + shuffle_size: 2500 + filter: false + resample_rate: 16000 + speed_perturb: true + num_frms: 200 + aug_prob: 0.6 + tfmel_args: + n_mels: 72 + hop_length: 160 + win_length: 400 + n_fft: 512 + f_min: 20 + f_max: 7600 + do_preemph: true + norm_signal: true + do_spec_aug: false + eps: 1.0e-8 + spec_aug: false + cmvn: false + +model: ReDimNet2B6 +model_args: + feat_dim: 72 + embed_dim: 192 + pooling_func: "ASTP" +model_init: null + +projection_args: + project_type: "sphereface2" + scale: 32.0 + easy_margin: false + do_lm: false + lanbuda: 0.7 + t: 3 + margin_type: "C" + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: true + increase_type: "exp" + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: true + weight_decay: 2.0e-05 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 6.0e-05 + scale_ratio: 12.0 + warm_from_zero: true + warm_up_epoch: 6 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/redimnet2_lm.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/redimnet2_lm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6a7405ff79f735822a3a6d3d32bfe5c313064395 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/redimnet2_lm.yaml @@ -0,0 +1,85 @@ +exp_dir: exp/ReDimNet2-tfmel-lm +gpus: "[0,1]" +num_avg: 10 +enable_amp: false +data_type: shard + +seed: 42 +num_epochs: 5 +save_epoch_interval: 1 +log_batch_interval: 100 + +dataloader_args: + batch_size: 128 + num_workers: 4 + pin_memory: false + prefetch_factor: 4 + drop_last: true + +dataset_args: + frontend: tfmel + sample_num_per_epoch: 0 + shuffle: true + shuffle_args: + shuffle_size: 2500 + filter: false + resample_rate: 16000 + speed_perturb: false + num_frms: 600 + aug_prob: 0.0 + tfmel_args: + n_mels: 72 + hop_length: 160 + win_length: 400 + n_fft: 512 + f_min: 20 + f_max: 7600 + do_preemph: true + norm_signal: true + do_spec_aug: false + eps: 1.0e-8 + spec_aug: false + cmvn: false + +model: ReDimNet2B6 +model_args: + feat_dim: 72 + embed_dim: 192 + pooling_func: "ASTP" +model_init: null + +projection_args: + project_type: "sphereface2" + scale: 32.0 + easy_margin: false + do_lm: false + lanbuda: 0.7 + t: 3 + margin_type: "C" + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.3 + final_margin: 0.3 + increase_start_epoch: 0 + fix_start_epoch: 0 + update_margin: false + increase_type: "exp" + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: true + weight_decay: 2.0e-05 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.0001 + final_lr: 0.00001 + warm_up_epoch: 0 + warm_from_zero: false + scale_ratio: 12.0 + num_epochs: 5 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/repvgg.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/repvgg.yaml new file mode 100644 index 0000000000000000000000000000000000000000..46b9ccf2954f0b09d8466715fc1b7728ef8fd163 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/repvgg.yaml @@ -0,0 +1,84 @@ +### train configuraton + +exp_dir: exp/RepVGG-TSTP-emb256-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: REPVGG_TINY_A0 # REPVGG_A0 REPVGG_A1 REPVGG_A2 REPVGG_RSBB_A0 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 256 # 512 + pooling_func: "TSTP" + deploy: False + use_se: False +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/res2net.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/res2net.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4bed12f6c6d4f2bbc0a462d19ab52f9182cce3b5 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/res2net.yaml @@ -0,0 +1,83 @@ +### train configuraton + +exp_dir: exp/Res2Net34_Base-TSTP-emb256-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: Res2Net34_Base # Res2Net34_Base, Res2Net34_Large +model_init: null +model_args: + feat_dim: 80 + embed_dim: 256 # 256, 192 + pooling_func: "TSTP" + two_emb_layer: False +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, sphereface2, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/resnet.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/resnet.yaml new file mode 100644 index 0000000000000000000000000000000000000000..198f5561c8a2580f65fd2a563306cb5e8f418b40 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/resnet.yaml @@ -0,0 +1,83 @@ +### train configuraton + +exp_dir: exp/ResNet34-TSTP-emb256-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ResNet34 # ResNet18, ResNet34, ResNet50, ResNet101, ResNet152 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 256 + pooling_func: "TSTP" # TSTP, ASTP, MQMHASTP + two_emb_layer: False +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, sphereface2, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/resnet_lm.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/resnet_lm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..987bd2bab8815a28f246227479ad67de4e7e2c12 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/resnet_lm.yaml @@ -0,0 +1,89 @@ +### Large margin fine-tuning configuration +# +# The large margin fine-tuning operation is often used in speaker +# verification challenge system to further improve the performance. +# In this fine-tuning stage, large margin and longer segment will +# be used. + +exp_dir: exp/ResNet34-TSTP-emb256-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150-LM +gpus: "[0,1]" +num_avg: 1 +enable_amp: False # whether enable automatic mixed precision training +do_lm: True + +seed: 42 +num_epochs: 5 +save_epoch_interval: 1 # save model per epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 64 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 600 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ResNet34 # ResNet18, ResNet34, ResNet50, ResNet101, ResNet152 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 256 + pooling_func: "TSTP" # TSTP, ASTP, MQMHASTP + two_emb_layer: False +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax, arc_margin_intertopk_subcenter + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.5 + final_margin: 0.5 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 1.0e-4 + final_lr: 2.5e-5 + warm_up_epoch: 1 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/w2vbert_s1_lora.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/w2vbert_s1_lora.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b11afd77126c046011b41fdf2e2d13134d55dde9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/w2vbert_s1_lora.yaml @@ -0,0 +1,105 @@ +# Wespeaker config for W2V-BERT + Adapter MFA (Stage 1: LoRA Frozen Encoder) +# Adapted from s1.yaml + +# Training configuration +exp_dir: exp/W2VBert_AdapterMFA_LoRA_frozen # Match run script +gpus: "[0,1,2,3,4,5,6,7]" # Match run script or adjust +num_avg: 1 # Average last N models for evaluation +enable_amp: True +seed: 24 + +# Epochs and logging +num_epochs: 15 +save_epoch_interval: 1 # Save every epoch +log_batch_interval: 100 # Log every 100 batches + + +# Dataloader arguments +dataloader_args: + batch_size: 64 + num_workers: 16 + pin_memory: False # Often False for raw audio + prefetch_factor: 16 # Adjust based on performance + drop_last: True + +# Dataset arguments +dataset_args: + sample_num_per_epoch: 0 # 0 means use all data in one epoch + shuffle: True + shuffle_args: + shuffle_size: 2500 # Wespeaker default buffer size + filter: True # Apply duration filtering + filter_args: + min_num_frames: 200 # ~2 seconds for 10ms hop + max_num_frames: 300 # ~3 seconds for 10ms hop + resample_rate: 16000 + speed_perturb: True # Enable speed perturbation + speed_perturb_mode: "expanded" # Match original W2V-BERT 1.0/0.9/1.1 sample expansion + num_frms: 300 # ~3 seconds target chunk size + aug_prob: 0.6667 # Close to original none/noise/reverb 3-way sampling + frontend: "w2vbert" # Use the new frontend + w2vbert_args: + model_name: "facebook/w2v-bert-2.0" # Local W2V-BERT path + frozen: True # Freeze the base W2V-BERT model + use_lora: True + lora_config_args: + r: 64 + lora_alpha: 128 + target_modules: ["linear_q", "linear_v"] # Adapter targets + lora_dropout: 0.0 + bias: "none" + # Note: w2vbert frontend handles feature extraction, no fbank_args needed + cmvn: False + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +# Model definition +model: W2VBert_Adapter_MFA # The new speaker model class +model_init: null # Or path to a pretrained model if needed initially +model_args: + feat_dim: 1024 # W2V-BERT Large hidden size + embed_dim: 256 + pooling_func: "ASP" + n_mfa_layers: -1 # Use all hidden states + adapter_dim: 128 + dropout: 0.0 + num_frontend_hidden_layers: 24 + +# Projection head (classifier) +projection_args: + project_type: "arc_margin" # ArcFace + scale: 32.0 + margin: 0.2 + easy_margin: False # ArcFace default + +# Margin scheduler (margin change during training) +# Using fixed margin for S1 +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.2 + final_margin: 0.2 + increase_start_epoch: 1 # No change + fix_start_epoch: 1 # No change + update_margin: False # Do not update margin during S1 + +# Loss function +loss: CrossEntropyLoss # Standard classification loss +loss_args: {} + +# Optimizer +optimizer: AdamW +optimizer_args: + lr: 0.0001 + weight_decay: 0.0001 + +# Scheduler +scheduler: WarmupLR_withStepDecay +scheduler_args: + warmup_step: 5 + decay_step: 5 + gamma: 0.1 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/w2vbert_s2_ft.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/w2vbert_s2_ft.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2683ba5354dff6c1fd09ef6904d6a0cb056913d8 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/w2vbert_s2_ft.yaml @@ -0,0 +1,99 @@ +# Wespeaker config for W2V-BERT + Adapter MFA +# (Stage 2: Joint Fine-tuning) +# Adapted from s2.yaml + +# Training configuration +exp_dir: exp/W2VBert_AdapterMFA_joint_ft # Match run script +gpus: "[0,1]" +num_avg: 1 # Average last N models +enable_amp: True +seed: 24 + +# Epochs and logging +num_epochs: 4 # Reduced epochs for fine-tuning +save_epoch_interval: 1 +log_batch_interval: 100 + +# Dataloader arguments +dataloader_args: + batch_size: 64 + num_workers: 16 + pin_memory: False + prefetch_factor: 16 + drop_last: True + +# Dataset arguments (same duration as S1, can adjust if needed) +dataset_args: + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 200 # ~2 seconds + max_num_frames: 300 # ~3 seconds + resample_rate: 16000 + speed_perturb: True + speed_perturb_mode: "expanded" + num_frms: 300 # ~3 seconds target chunk size + aug_prob: 0.6667 + frontend: "w2vbert" + w2vbert_args: + model_name: "facebook/w2v-bert-2.0" + frozen: False # Unfreeze the base W2V-BERT model + use_lora: False # No LoRA in this stage + lora_config_args: null + cmvn: False + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +# Model definition (load S1 model, unfreeze frontend, remove LoRA) +model: W2VBert_Adapter_MFA +model_init: null # Checkpoint will be loaded via run script +model_args: + feat_dim: 1024 # W2V-BERT Large + embed_dim: 256 + pooling_func: "ASP" + n_mfa_layers: -1 + adapter_dim: 128 + dropout: 0.0 + num_frontend_hidden_layers: 24 + +# Projection head (same structure, margin potentially updated) +projection_args: + project_type: "arc_margin" + scale: 32.0 + margin: 0.2 # Initial margin for S2 + easy_margin: False + +# Margin scheduler (fixed margin) +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.2 + final_margin: 0.2 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: False # Keep margin fixed + +# Loss function +loss: CrossEntropyLoss +loss_args: {} + +# Optimizer +optimizer: AdamW +optimizer_args: + lr: 0.00001 # Lower LR for fine-tuning + weight_decay: 0.0001 + +# Scheduler +scheduler: WarmupCosineScheduler +scheduler_args: + min_lr: 0.000005 + max_lr: 0.00001 + warmup_epoch: 0 + fix_epoch: 2 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/w2vbert_s3_lmft.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/w2vbert_s3_lmft.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2b24a1eb94c001429ac53db284faac5be5a05374 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/w2vbert_s3_lmft.yaml @@ -0,0 +1,99 @@ +# Wespeaker config for W2V-BERT + Adapter MFA +# (Stage 3: Large Margin Fine-tuning) +# Adapted from s3.yaml + +# Training configuration +exp_dir: exp/W2VBert_AdapterMFA_joint_lmft # Match run script +gpus: "[0,1]" +num_avg: 1 # Average only the last model for LMFT +enable_amp: True +seed: 24 +do_lm: True # Indicate Large Margin stage + +# Epochs and logging +num_epochs: 2 # Short LMFT stage +save_epoch_interval: 1 +log_batch_interval: 100 + +# Dataloader arguments (increase batch size if possible with longer segments) +dataloader_args: + batch_size: 32 # Original LMFT batch size + num_workers: 4 + pin_memory: False + prefetch_factor: 4 + drop_last: True + +# Dataset arguments (longer segments, no augmentation) +dataset_args: + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 500 # ~5 seconds + max_num_frames: 600 # ~6 seconds + resample_rate: 16000 + speed_perturb: False # No speed perturb in LMFT + num_frms: 600 # ~6 seconds target chunk size + aug_prob: 0.0 # No augmentation in LMFT + frontend: "w2vbert" + w2vbert_args: + model_name: "facebook/w2v-bert-2.0" + frozen: False # Keep unfrozen + use_lora: False + lora_config_args: null + cmvn: False + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +# Model definition (load S2 model) +model: W2VBert_Adapter_MFA +model_init: null # Checkpoint loaded via run script +model_args: + feat_dim: 1024 + embed_dim: 256 + pooling_func: "ASP" + n_mfa_layers: -1 + adapter_dim: 128 + dropout: 0.0 + num_frontend_hidden_layers: 24 + +# Projection head (increased margin) +projection_args: + project_type: "arc_margin" + scale: 32.0 + margin: 0.5 # Increased margin for LMFT + easy_margin: False + +# Margin scheduler (fixed large margin) +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.5 + final_margin: 0.5 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: False + +# Loss function +loss: CrossEntropyLoss +loss_args: {} + +# Optimizer +optimizer: AdamW +optimizer_args: + lr: 0.00001 # Typically same or slightly lower LR for LMFT + weight_decay: 0.0001 + +# Scheduler +scheduler: WarmupCosineScheduler +scheduler_args: + min_lr: 0.000005 + max_lr: 0.00001 + warmup_epoch: 0 + fix_epoch: 1 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/xi_vector.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/xi_vector.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3d51ec822820ca5a6f6c7f6414f7c337b2a74ce3 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/xi_vector.yaml @@ -0,0 +1,83 @@ +### train configuraton + +exp_dir: exp/XI_VEC_ECAPA_TDNN_c512-emb192-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 512 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + frontend: "fbank" # fbank, s3prl + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: XI_VEC_ECAPA_TDNN_c512 # XI_VEC_ECAPA_TDNN_c512, XI_VEC_ECAPA_TDNN_c1024, XI_VEC_XVEC +model_init: null +model_args: + feat_dim: 80 + embed_dim: 192 + pooling_func: "XI" # the default pooling_func in ECAPA_TDNN is ASTP +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/xvec.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/xvec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6d8c0fac15d11b52c15f42b663db20d022f2b7aa --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/xvec.yaml @@ -0,0 +1,82 @@ +### train configuraton + +exp_dir: exp/XVEC-TSTP-emb512-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 200 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: XVEC +model_init: null +model_args: + feat_dim: 80 + embed_dim: 512 + pooling_func: "TSTP" +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/xvec_lm.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/xvec_lm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9aa95c906bad235796df7a276096c9e36a681acc --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/conf/xvec_lm.yaml @@ -0,0 +1,88 @@ +### Large margin fine-tuning configuration +# +# The large margin fine-tuning operation is often used in speaker +# verification challenge system to further improve the performance. +# In this fine-tuning stage, large margin and longer segment will +# be used. + +exp_dir: exp/XVEC-TSTP-emb512-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150-LM +gpus: "[0,1]" +num_avg: 1 +enable_amp: False # whether enable automatic mixed precision training +do_lm: True + +seed: 42 +num_epochs: 5 +save_epoch_interval: 1 # save model per epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 64 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + filter: True + filter_args: + min_num_frames: 100 + max_num_frames: 800 + resample_rate: 16000 + speed_perturb: True + num_frms: 600 + aug_prob: 0.6 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: XVEC +model_init: null +model_args: + feat_dim: 80 + embed_dim: 512 + pooling_func: "TSTP" +projection_args: + project_type: "arc_margin" # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.5 + final_margin: 0.5 + increase_start_epoch: 1 + fix_start_epoch: 1 + update_margin: True + increase_type: "exp" # exp, linear + +loss: CrossEntropyLoss +loss_args: {} + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 1.0e-4 + final_lr: 2.5e-5 + warm_up_epoch: 1 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/download_data.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/download_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..e3e12109d7ed1f7ec2d7ea7813708f1496c35d02 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/download_data.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +download_dir=data/download_data + +. tools/parse_options.sh || exit 1 + +[ ! -d ${download_dir} ] && mkdir -p ${download_dir} + +if [ ! -f ${download_dir}/musan.tar.gz ]; then + echo "Downloading musan.tar.gz ..." + wget --no-check-certificate https://openslr.elda.org/resources/17/musan.tar.gz -P ${download_dir} + md5=$(md5sum ${download_dir}/musan.tar.gz | awk '{print $1}') + [ $md5 != "0c472d4fc0c5141eca47ad1ffeb2a7df" ] && echo "Wrong md5sum of musan.tar.gz" && exit 1 +fi + +if [ ! -f ${download_dir}/rirs_noises.zip ]; then + echo "Downloading rirs_noises.zip ..." + wget --no-check-certificate https://us.openslr.org/resources/28/rirs_noises.zip -P ${download_dir} + md5=$(md5sum ${download_dir}/rirs_noises.zip | awk '{print $1}') + [ $md5 != "e6f48e257286e05de56413b4779d8ffb" ] && echo "Wrong md5sum of rirs_noises.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox1_test_wav.zip ]; then + echo "Downloading vox1_test_wav.zip ..." + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_test_wav.zip -P ${download_dir} + md5=$(md5sum ${download_dir}/vox1_test_wav.zip | awk '{print $1}') + [ $md5 != "185fdc63c3c739954633d50379a3d102" ] && echo "Wrong md5sum of vox1_test_wav.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox1_dev_wav.zip ]; then + echo "Downloading vox1_dev_wav.zip ..." + for part in a b c d; do + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_dev_wav_parta${part} -P ${download_dir} & + done + wait + cat ${download_dir}/vox1_dev* >${download_dir}/vox1_dev_wav.zip + md5=$(md5sum ${download_dir}/vox1_dev_wav.zip | awk '{print $1}') + [ $md5 != "ae63e55b951748cc486645f532ba230b" ] && echo "Wrong md5sum of vox1_dev_wav.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox2_aac.zip ]; then + echo "Downloading vox2_aac.zip ..." + for part in a b c d e f g h; do + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox2_dev_aac_parta${part} -P ${download_dir} & + done + wait + cat ${download_dir}/vox2_dev_aac* >${download_dir}/vox2_aac.zip + md5=$(md5sum ${download_dir}/vox2_aac.zip | awk '{print $1}') + [ $md5 != "bbc063c46078a602ca71605645c2a402" ] && echo "Wrong md5sum of vox2_aac.zip" && exit 1 +fi + +echo "Download success !!!" diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/extract_vox.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/extract_vox.sh new file mode 100644 index 0000000000000000000000000000000000000000..8b3a34ec27d08f9a6b9ef5685af1e648e961337a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/extract_vox.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir='' +model_path='' +nj=4 +gpus="[0,1]" +data_type="shard" # shard/raw/feat +data=data + +. tools/parse_options.sh +set -e + +data_name_array=("vox2_dev" "vox1") +data_list_path_array=("${data}/vox2_dev/${data_type}.list" "${data}/vox1/${data_type}.list") +data_scp_path_array=("${data}/vox2_dev/wav.scp" "${data}/vox1/wav.scp") # to count the number of wavs +nj_array=($nj $nj) +batch_size_array=(16 1) # batch_size of test set must be 1 !!! +num_workers_array=(4 1) +count=${#data_name_array[@]} + +for i in $(seq 0 $(($count - 1))); do + wavs_num=$(wc -l ${data_scp_path_array[$i]} | awk '{print $1}') + bash tools/extract_embedding.sh --exp_dir ${exp_dir} \ + --model_path $model_path \ + --data_type ${data_type} \ + --data_list ${data_list_path_array[$i]} \ + --wavs_num ${wavs_num} \ + --store_dir ${data_name_array[$i]} \ + --batch_size ${batch_size_array[$i]} \ + --num_workers ${num_workers_array[$i]} \ + --nj ${nj_array[$i]} \ + --gpus $gpus & +done + +wait + +echo "Embedding dir is (${exp_dir}/embeddings)." diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/m4a2wav.pl b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/m4a2wav.pl new file mode 100644 index 0000000000000000000000000000000000000000..11e10acb8b267661dbcc013209b23cb6ebc05f25 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/m4a2wav.pl @@ -0,0 +1,64 @@ +#!/usr/bin/perl +# +# Copyright 2018 Ewald Enzinger +# 2022 Hongji Wang +# +# Usage: m4a2wav.pl /export/voxceleb2_m4a dev /export/voxceleb2_wav +# +# Note: This script requires ffmpeg to be installed and its location included in $PATH. + +if (@ARGV != 3) { + print STDERR "Usage: $0 \n"; + print STDERR "e.g. $0 /export/voxceleb2_m4a dev /export/voxceleb2_wav\n"; + exit(1); +} + +# Check that ffmpeg is installed. +if (`which ffmpeg` eq "") { + die "Error: this script requires that ffmpeg is installed."; +} + +($database_m4a, $dataset, $database_wav) = @ARGV; + +if ("$dataset" ne "dev" && "$dataset" ne "test") { + die "dataset parameter must be 'dev' or 'test'!"; +} + +opendir my $dh, "$database_m4a/$dataset/aac" or die "Cannot open directory: $!"; +my @spkr_dirs = grep {-d "$database_m4a/$dataset/aac/$_" && ! /^\.{1,2}$/} readdir($dh); +closedir $dh; + +if (system("mkdir -p $database_wav/$dataset") != 0) { + die "Error making directory $database_wav/$dataset"; +} + +open(fp, ">", "$database_wav/$dataset/m4a2wav_$dataset.sh") or die "Could not open the output file $database_wav/m4a2wav.sh"; + +foreach (@spkr_dirs) { + my $spkr_id = $_; + + opendir my $dh, "$database_m4a/$dataset/aac/$spkr_id/" or die "Cannot open directory: $!"; + my @rec_dirs = grep {-d "$database_m4a/$dataset/aac/$spkr_id/$_" && ! /^\.{1,2}$/} readdir($dh); + closedir $dh; + + foreach (@rec_dirs) { + my $rec_id = $_; + + opendir my $dh, "$database_m4a/$dataset/aac/$spkr_id/$rec_id/" or die "Cannot open directory: $!"; + my @files = map{s/\.[^.]+$//;$_}grep {/\.m4a$/} readdir($dh); + closedir $dh; + + foreach (@files) { + my $name = $_; + if ( not -e "$database_wav/$dataset/aac/$spkr_id/$rec_id"){ + system("mkdir -p $database_wav/$dataset/aac/$spkr_id/$rec_id"); + } + my $wav = "ffmpeg -v 8 -i $database_m4a/$dataset/aac/$spkr_id/$rec_id/$name.m4a -f wav -acodec pcm_s16le $database_wav/$dataset/aac/$spkr_id/$rec_id/$name.wav"; + print fp "$wav", "\n"; + } + } +} +close(fp) or die; + +# generate wav +#system("sh $database_wav/$dataset/m4a2wav_$dataset.sh"); diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/prepare_data.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/prepare_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..b957eaf95028fec384a6b72053ed7aebdce916ba --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/prepare_data.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +stage=-1 +stop_stage=-1 +data=data + +. tools/parse_options.sh || exit 1 + +data=`realpath ${data}` +download_dir=${data}/download_data +rawdata_dir=${data}/raw_data + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Download musan.tar.gz, rirs_noises.zip, vox1_test_wav.zip, vox1_dev_wav.zip, and vox2_aac.zip." + echo "This may take a long time. Thus we recommand you to download all archives above in your own way first." + + ./local/download_data.sh --download_dir ${download_dir} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Decompress all archives ..." + echo "This could take some time ..." + + for archive in musan.tar.gz rirs_noises.zip vox1_test_wav.zip vox1_dev_wav.zip vox2_aac.zip; do + [ ! -f ${download_dir}/$archive ] && echo "Archive $archive not exists !!!" && exit 1 + done + [ ! -d ${rawdata_dir} ] && mkdir -p ${rawdata_dir} + + if [ ! -d ${rawdata_dir}/musan ]; then + tar -xzvf ${download_dir}/musan.tar.gz -C ${rawdata_dir} + fi + + if [ ! -d ${rawdata_dir}/RIRS_NOISES ]; then + unzip ${download_dir}/rirs_noises.zip -d ${rawdata_dir} + fi + + if [ ! -d ${rawdata_dir}/voxceleb1 ]; then + mkdir -p ${rawdata_dir}/voxceleb1/test ${rawdata_dir}/voxceleb1/dev + unzip ${download_dir}/vox1_test_wav.zip -d ${rawdata_dir}/voxceleb1/test + unzip ${download_dir}/vox1_dev_wav.zip -d ${rawdata_dir}/voxceleb1/dev + fi + + if [ ! -d ${rawdata_dir}/voxceleb2_m4a ]; then + mkdir -p ${rawdata_dir}/voxceleb2_m4a + unzip ${download_dir}/vox2_aac.zip -d ${rawdata_dir}/voxceleb2_m4a + fi + + echo "Decompress success !!!" +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Convert voxceleb2 wav format from m4a to wav using ffmpeg." + echo "This could also take some time ..." + + if [ ! -d ${rawdata_dir}/voxceleb2_wav ]; then + ./local/m4a2wav.pl ${rawdata_dir}/voxceleb2_m4a dev ${rawdata_dir}/voxceleb2_wav + # Here we use 8 parallel jobs + cat ${rawdata_dir}/voxceleb2_wav/dev/m4a2wav_dev.sh | xargs -P 8 -i sh -c "{}" + fi + + echo "Convert m4a2wav success !!!" +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Prepare wav.scp for each dataset ..." + export LC_ALL=C # kaldi config + + mkdir -p ${data}/musan ${data}/rirs ${data}/vox1 ${data}/vox2_dev + # musan + find ${rawdata_dir}/musan -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' >${data}/musan/wav.scp + # rirs + find ${rawdata_dir}/RIRS_NOISES/simulated_rirs -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' >${data}/rirs/wav.scp + # vox1 + find ${rawdata_dir}/voxceleb1 -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' | sort >${data}/vox1/wav.scp + awk '{print $1}' ${data}/vox1/wav.scp | awk -F "/" '{print $0,$1}' >${data}/vox1/utt2spk + ./tools/utt2spk_to_spk2utt.pl ${data}/vox1/utt2spk >${data}/vox1/spk2utt + if [ ! -d ${data}/vox1/trials ]; then + echo "Download trials for vox1 ..." + mkdir -p ${data}/vox1/trials + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test.txt -O ${data}/vox1/trials/vox1-O.txt + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_hard.txt -O ${data}/vox1/trials/vox1-H.txt + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_all.txt -O ${data}/vox1/trials/vox1-E.txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test2.txt -O ${data}/vox1/trials/vox1-O\(cleaned\).txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_hard2.txt -O ${data}/vox1/trials/vox1-H\(cleaned\).txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_all2.txt -O ${data}/vox1/trials/vox1-E\(cleaned\).txt + # transform them into kaldi trial format + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1/trials/vox1-O\(cleaned\).txt >${data}/vox1/trials/vox1_O_cleaned.kaldi + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1/trials/vox1-H\(cleaned\).txt >${data}/vox1/trials/vox1_H_cleaned.kaldi + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1/trials/vox1-E\(cleaned\).txt >${data}/vox1/trials/vox1_E_cleaned.kaldi + fi + # vox2 + find ${rawdata_dir}/voxceleb2_wav -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' | sort >${data}/vox2_dev/wav.scp + awk '{print $1}' ${data}/vox2_dev/wav.scp | awk -F "/" '{print $0,$1}' >${data}/vox2_dev/utt2spk + ./tools/utt2spk_to_spk2utt.pl ${data}/vox2_dev/utt2spk >${data}/vox2_dev/spk2utt + + echo "Success !!!" +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score.sh new file mode 100644 index 0000000000000000000000000000000000000000..50f19c6708fb9e3905ede5f67583dc9c65fe8272 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir= +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +data=data + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "apply cosine scoring ..." + mkdir -p ${exp_dir}/scores + trials_dir=${data}/vox1/trials + for x in $trials; do + echo $x + python wespeaker/bin/score.py \ + --exp_dir ${exp_dir} \ + --eval_scp_path ${exp_dir}/embeddings/vox1/xvector.scp \ + --cal_mean True \ + --cal_mean_dir ${exp_dir}/embeddings/vox2_dev \ + ${trials_dir}/${x} + done +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "compute metrics (EER/minDCF) ..." + scores_dir=${exp_dir}/scores + for x in $trials; do + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${x}.score \ + 2>&1 | tee -a ${scores_dir}/vox1_cos_result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${x}.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score_calibration.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score_calibration.sh new file mode 100644 index 0000000000000000000000000000000000000000..ac811a6ed05a9a3d6c4158d1ec305f69401a694d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score_calibration.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2024 Bing Han (hanbing97@sjtu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +score_norm_method="asnorm" # asnorm/snorm +cohort_set=vox2_dev +calibration_trial="vox2_cali.kaldi" +top_n=100 +exp_dir='' +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +data=data + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +output_name=${cohort_set}_${score_norm_method} +[ "${score_norm_method}" == "asnorm" ] && output_name=${output_name}${top_n} +trials_dir=${data}/vox1/trials + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + echo "Score calibration set" + # Compute duration + for dset in vox2_dev vox1; do + if [ ! -f ${data}/${dset}/utt2dur ]; then + python tools/wav2dur.py ${data}/${dset}/wav.scp ${data}/${dset}/utt2dur > ${data}/${dset}/dur.log + fi + done + # generate trial for calibration + if [ ! -e ${trials_dir}/${calibration_trial} ]; then + python tools/generate_calibration_trial.py --utt2dur ${data}/vox2_dev/utt2dur --trial_path ${trials_dir}/${calibration_trial} + fi + + python wespeaker/bin/score.py \ + --exp_dir ${exp_dir} \ + --eval_scp_path ${exp_dir}/embeddings/vox2_dev/xvector.scp \ + --cal_mean True \ + --cal_mean_dir ${exp_dir}/embeddings/vox2_dev \ + ${trials_dir}/${calibration_trial} + + python wespeaker/bin/score_norm.py \ + --score_norm_method $score_norm_method \ + --top_n $top_n \ + --trial_score_file $exp_dir/scores/${calibration_trial}.score \ + --score_norm_file $exp_dir/scores/${output_name}_${calibration_trial}.score \ + --cohort_emb_scp ${exp_dir}/embeddings/${cohort_set}/spk_xvector.scp \ + --eval_emb_scp ${exp_dir}/embeddings/vox2_dev/xvector.scp \ + --mean_vec_path ${exp_dir}/embeddings/vox2_dev/mean_vec.npy +fi + +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + echo "Prepare calibration factors" + # gather calibration factor + mkdir -p ${exp_dir}/scores/calibration + cat ${data}/vox1/utt2dur ${data}/vox2_dev/utt2dur > ${exp_dir}/scores/calibration/utt2dur + for x in ${calibration_trial} $trials; do + python wespeaker/bin/score_calibration.py "gather_calibration_factors" \ + --wav_dur_scp ${exp_dir}/scores/calibration/utt2dur \ + --max_dur 20 \ + --score_norm_file ${exp_dir}/scores/${output_name}_${x}.score \ + --calibration_factor_file ${exp_dir}/scores/calibration/${output_name}_${x}.calibration + done +fi + + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + echo "Train calibration model" + python wespeaker/bin/score_calibration.py "train_calibration_model" \ + --calibration_factor_file ${exp_dir}/scores/calibration/${output_name}_${calibration_trial}.calibration \ + --save_model_path ${exp_dir}/scores/calibration/calibration_model.pt +fi + +cali_output_name=cali_${output_name} +if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then + echo "Infer calibration model" + for x in ${trials}; do + python wespeaker/bin/score_calibration.py "infer_calibration" \ + --calibration_factor_file ${exp_dir}/scores/calibration/${output_name}_${x}.calibration \ + --save_model_path ${exp_dir}/scores/calibration/calibration_model.pt \ + --calibration_score_file ${exp_dir}/scores/${cali_output_name}_${x}.score + done +fi + +if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then + echo "compute metrics" + for x in ${trials}; do + scores_dir=${exp_dir}/scores + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${cali_output_name}_${x}.score \ + 2>&1 | tee -a ${scores_dir}/vox1_cali_${score_norm_method}${top_n}_result + + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${cali_output_name}_${x}.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score_norm.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score_norm.sh new file mode 100644 index 0000000000000000000000000000000000000000..028943aa61927dd0c12cf43755897ead14331d87 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score_norm.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +score_norm_method="asnorm" # asnorm/snorm +cohort_set=vox2_dev +top_n=100 +exp_dir= +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +data=data + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + echo "compute mean xvector" + python tools/vector_mean.py \ + --spk2utt ${data}/${cohort_set}/spk2utt \ + --xvector_scp $exp_dir/embeddings/${cohort_set}/xvector.scp \ + --spk_xvector_ark $exp_dir/embeddings/${cohort_set}/spk_xvector.ark +fi + +output_name=${cohort_set}_${score_norm_method} +[ "${score_norm_method}" == "asnorm" ] && output_name=${output_name}${top_n} +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + echo "compute norm score" + for x in $trials; do + python wespeaker/bin/score_norm.py \ + --score_norm_method $score_norm_method \ + --top_n $top_n \ + --trial_score_file $exp_dir/scores/${x}.score \ + --score_norm_file $exp_dir/scores/${output_name}_${x}.score \ + --cohort_emb_scp ${exp_dir}/embeddings/${cohort_set}/spk_xvector.scp \ + --eval_emb_scp ${exp_dir}/embeddings/vox1/xvector.scp \ + --mean_vec_path ${exp_dir}/embeddings/vox2_dev/mean_vec.npy + done +fi + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + echo "compute metrics" + for x in ${trials}; do + scores_dir=${exp_dir}/scores + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${output_name}_${x}.score \ + 2>&1 | tee -a ${scores_dir}/vox1_${score_norm_method}${top_n}_result + + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${output_name}_${x}.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score_plda.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score_plda.sh new file mode 100644 index 0000000000000000000000000000000000000000..8b3c14e5504f04cd3c162c9d7a14fd73493090f9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/local/score_plda.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Copyright (c) 2022 Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir= +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +data=data + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "train the plda model ..." + python wespeaker/bin/train_plda.py \ + --exp_dir ${exp_dir} \ + --scp_path ${exp_dir}/embeddings/vox2_dev/xvector.scp \ + --utt2spk ${data}/vox2_dev/utt2spk \ + --indim 256 \ + --iter 5 + echo "plda training finished" +fi + + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "apply plda scoring ..." + mkdir -p ${exp_dir}/scores + trials_dir=${data}/vox1/trials + for x in $trials; do + echo "scoring on " $x + python wespeaker/bin/eval_plda.py \ + --enroll_scp_path ${exp_dir}/embeddings/vox1/xvector.scp \ + --test_scp_path ${exp_dir}/embeddings/vox1/xvector.scp \ + --utt2spk <(cat ${data}/vox1/utt2spk | awk '{print $1, $1}') \ + --trial ${trials_dir}/${x} \ + --score_path ${exp_dir}/scores/${x}.pldascore \ + --model_path ${exp_dir}/plda + done +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "compute metrics (EER/minDCF) ..." + scores_dir=${exp_dir}/scores + for x in $trials; do + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${x}.pldascore \ + 2>&1 | tee -a ${scores_dir}/vox1_plda_result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${x}.pldascore + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/path.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/path.sh new file mode 100644 index 0000000000000000000000000000000000000000..b90a5154cf4a8f25b9a42851c59393f07dacb350 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/path.sh @@ -0,0 +1,5 @@ +export PATH=$PWD:$PATH + +# NOTE(kan-bayashi): Use UTF-8 in Python to avoid UnicodeDecodeError when LC_ALL=C +export PYTHONIOENCODING=UTF-8 +export PYTHONPATH=../../../:$PYTHONPATH diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..272996c996a5c5f016c6ffa18067f404fbeb54de --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/run.sh @@ -0,0 +1,163 @@ +#!/bin/bash + +# Copyright 2022 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) + +. ./path.sh || exit 1 + +# multi-node + multi-gpus: +# bash run.sh --stage 3 --stop-stage 3 --HOST_NODE_ADDR "xxx.xxx.xxx.xxx:port" --num_nodes num_node + +stage=-1 +stop_stage=-1 + +HOST_NODE_ADDR="localhost:29400" +num_nodes=1 +job_id=2024 + +data=data +data_type="shard" # shard/raw + +config=conf/resnet.yaml +exp_dir=exp/ResNet34-TSTP-emb256-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus="[0,1]" +num_avg=10 +checkpoint= + +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +score_norm_method="asnorm" # asnorm/snorm +top_n=300 + +# setup for large margin fine-tuning +lm_config=conf/resnet_lm.yaml + +. tools/parse_options.sh || exit 1 + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Prepare datasets ..." + ./local/prepare_data.sh --stage 2 --stop_stage 4 --data ${data} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Covert train and test data to ${data_type}..." + for dset in vox2_dev vox1; do + if [ $data_type == "shard" ]; then + python tools/make_shard_list.py --num_utts_per_shard 1000 \ + --num_threads 16 \ + --prefix shards \ + --shuffle \ + ${data}/$dset/wav.scp ${data}/$dset/utt2spk \ + ${data}/$dset/shards ${data}/$dset/shard.list + else + python tools/make_raw_list.py ${data}/$dset/wav.scp \ + ${data}/$dset/utt2spk ${data}/$dset/raw.list + fi + done + # Convert all musan data to LMDB + python tools/make_lmdb.py ${data}/musan/wav.scp ${data}/musan/lmdb + # Convert all rirs data to LMDB + python tools/make_lmdb.py ${data}/rirs/wav.scp ${data}/rirs/lmdb +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Start training ..." + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/bin/train.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + --data_type "${data_type}" \ + --train_data ${data}/vox2_dev/${data_type}.list \ + --train_label ${data}/vox2_dev/utt2spk \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + ${checkpoint:+--checkpoint $checkpoint} +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Do model average ..." + avg_model=$exp_dir/models/avg_model.pt + python wespeaker/bin/average_model.py \ + --dst_model $avg_model \ + --src_path $exp_dir/models \ + --num ${num_avg} + + model_path=$avg_model + if [[ $config == *repvgg*.yaml ]]; then + echo "convert repvgg model ..." + python wespeaker/models/convert_repvgg.py \ + --config $exp_dir/config.yaml \ + --load $avg_model \ + --save $exp_dir/models/convert_model.pt + model_path=$exp_dir/models/convert_model.pt + fi + + echo "Extract embeddings ..." + local/extract_vox.sh \ + --exp_dir $exp_dir --model_path $model_path \ + --nj 4 --gpus $gpus --data_type $data_type --data ${data} +fi + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "Score ..." + local/score.sh \ + --stage 1 --stop-stage 2 \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "Score norm ..." + local/score_norm.sh \ + --stage 1 --stop-stage 3 \ + --score_norm_method $score_norm_method \ + --cohort_set vox2_dev \ + --top_n $top_n \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + +if [ ${stage} -le 7 ] && [ ${stop_stage} -ge 7 ]; then + echo "Score calibration ..." + local/score_calibration.sh \ + --stage 1 --stop-stage 5 \ + --score_norm_method $score_norm_method \ + --calibration_trial "vox2_cali.kaldi" \ + --cohort_set vox2_dev \ + --top_n $top_n \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + +if [ ${stage} -le 8 ] && [ ${stop_stage} -ge 8 ]; then + echo "Export the best model ..." + python wespeaker/bin/export_jit.py \ + --config $exp_dir/config.yaml \ + --checkpoint $exp_dir/models/avg_model.pt \ + --output_file $exp_dir/models/final.zip +fi + +if [ ${stage} -le 9 ] && [ ${stop_stage} -ge 9 ]; then + echo "Large margin fine-tuning ..." + lm_exp_dir=${exp_dir}-LM + mkdir -p ${lm_exp_dir}/models + # Use the pre-trained average model to initialize the LM training + cp ${exp_dir}/models/avg_model.pt ${lm_exp_dir}/models/model_0.pt + bash run.sh --stage 3 --stop_stage 8 \ + --data ${data} \ + --data_type ${data_type} \ + --config ${lm_config} \ + --exp_dir ${lm_exp_dir} \ + --gpus $gpus \ + --num_avg 1 \ + --checkpoint ${lm_exp_dir}/models/model_0.pt \ + --trials "$trials" \ + --score_norm_method ${score_norm_method} \ + --top_n ${top_n} +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/run_w2v.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/run_w2v.sh new file mode 100644 index 0000000000000000000000000000000000000000..1319e64636c1dedbcb2c8c0cb090f9482991f605 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/run_w2v.sh @@ -0,0 +1,273 @@ +#!/bin/bash + +# Copyright 2025 Qituan Shangguan (2369144677@qq.com) +# Adapted from run_wavlm.sh + +. ./path.sh || exit 1 + +stage=1 # Start from stage 1 by default +stop_stage=-1 + +HOST_NODE_ADDR="localhost:29402" # Keep or adjust as needed +num_nodes=1 +job_id=2025 # Change if needed + +# Data path (assuming same data as run_wavlm.sh) +data=data +data_type="shard" # shard/raw + +# --- Configuration for W2V-BERT --- +# Stage 1: Initial training (LoRA frozen encoder) +config_s1=conf/w2vbert_s1_lora.yaml +exp_dir_s1=exp/W2VBert_AdapterMFA_LoRA_frozen + +# Stage 2: Joint Fine-tuning (full model) +config_s2=conf/w2vbert_s2_ft.yaml +exp_dir_s2=exp/W2VBert_AdapterMFA_joint_ft + +# Stage 3: Large Margin Fine-tuning (full model) +config_s3=conf/w2vbert_s3_lmft.yaml +exp_dir_s3=exp/W2VBert_AdapterMFA_joint_lmft +# --- End Configuration --- + +gpus="[0,1,2,3,4,5,6,7]" # Adjust GPU list as needed +num_avg=1 # Default averaging, adjust per stage if needed +checkpoint= + +# Evaluation settings (keep or adjust) +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +score_norm_method="asnorm" # asnorm/snorm +top_n=300 + +. tools/parse_options.sh || exit 1 + +# Stage 1: Data preparation (assumed done by run_wavlm.sh or similar) +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Stage 1: Preparing datasets (assuming already done)..." + # Optional: add data prep commands here if not done previously. + # ./local/prepare_data.sh --stage 2 --stop_stage 4 --data ${data} + echo "Skipping data preparation, assuming it is done." +fi + +# Stage 2: Data conversion (assumed done by run_wavlm.sh or similar) +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Stage 2: Converting train/test data to ${data_type} "\ +"(assuming already done)..." + # Optional: add conversion commands here if not done previously. + # for dset in vox2_dev vox1; do + # if [ $data_type == "shard" ]; then ... else ... fi + # done + # python tools/make_lmdb.py ${data}/musan/wav.scp ${data}/musan/lmdb + # python tools/make_lmdb.py ${data}/rirs/wav.scp ${data}/rirs/lmdb + echo "Skipping data conversion, assuming it is done." +fi + +# Stage 3: initial training (LoRA with frozen encoder) +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Stage 3: Start initial LoRA training (encoder frozen)..." + num_gpus=$(echo "$gpus" | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes="$num_nodes" --nproc_per_node="$num_gpus" \ + --rdzv_id="${job_id}_s1" --rdzv_backend="c10d" \ + --rdzv_endpoint="$HOST_NODE_ADDR" \ + wespeaker/bin/train.py --config "$config_s1" \ + --exp_dir "${exp_dir_s1}" \ + --gpus "$gpus" \ + --num_avg 1 \ + --data_type "${data_type}" \ + --train_data "${data}/vox2_dev/${data_type}.list" \ + --train_label "${data}/vox2_dev/utt2spk" \ + --reverb_data "${data}/rirs/lmdb" \ + --noise_data "${data}/musan/lmdb" \ + ${checkpoint:+--checkpoint "$checkpoint"} + # Use checkpoint only if resuming S1 +fi + +# Intermediate evaluation after Stage 3 (optional but recommended) +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Stage 4: Evaluate LoRA frozen model..." + avg_model_s1=${exp_dir_s1}/models/avg_model.pt + num_avg_s1=$(yq '.num_avg' "$config_s1") + echo "Averaging model for Stage 1..." + python wespeaker/bin/average_model.py \ + --dst_model "$avg_model_s1" \ + --src_path "${exp_dir_s1}/models" \ + --num "${num_avg_s1:-1}" + + echo "Extracting embeddings for Stage 1 model..." + local/extract_vox.sh \ + --exp_dir "$exp_dir_s1" --model_path "$avg_model_s1" \ + --nj 8 --gpus "$gpus" --data_type "$data_type" --data "${data}" + + echo "Scoring Stage 1 model..." + local/score.sh \ + --stage 1 --stop-stage 2 --data "${data}" \ + --exp_dir "$exp_dir_s1" --trials "$trials" + + # Optional: score normalization and calibration for S1 + local/score_norm.sh \ + --stage 1 --stop-stage 3 \ + --score_norm_method "$score_norm_method" \ + --cohort_set vox2_dev --top_n "$top_n" \ + --data "${data}" --exp_dir "$exp_dir_s1" --trials "$trials" + + local/score_calibration.sh \ + --stage 1 --stop-stage 5 \ + --score_norm_method "$score_norm_method" \ + --calibration_trial "vox2_cali.kaldi" \ + --cohort_set vox2_dev --top_n "$top_n" \ + --data "${data}" --exp_dir "$exp_dir_s1" --trials "$trials" +fi + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + # Stage 5: merge LoRA weights + echo "Stage 5: Merging LoRA weights" + + # Stage 1 directory, for example exp/W2VBert_AdapterMFA_LoRA_frozen + S1_CONFIG_PATH=${exp_dir_s1}/config.yaml + S1_CHECKPOINT_IN=${exp_dir_s1}/models/avg_model.pt + S1_CHECKPOINT_OUT=${exp_dir_s1}/models/merged_avg_model.pt + + python tools/merge_lora.py \ + --config "${S1_CONFIG_PATH}" \ + --checkpoint_in "${S1_CHECKPOINT_IN}" \ + --checkpoint_out "${S1_CHECKPOINT_OUT}" +fi + +# Stage 6: joint fine-tuning (full model) +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "Stage 6: Start joint fine-tuning (full model)..." + # Initialize S2 training from merged S1 model + init_checkpoint_s2="${exp_dir_s1}/models/merged_avg_model.pt" + if [ ! -f "$init_checkpoint_s2" ]; then + echo "Error: Merged model from Stage 1 ($init_checkpoint_s2) "\ +"not found." + echo "Run previous stages." + exit 1 + fi + + mkdir -p "${exp_dir_s2}/models" + cp "$init_checkpoint_s2" "${exp_dir_s2}/models/model_0.pt" + + num_gpus=$(echo "$gpus" | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes="$num_nodes" --nproc_per_node="$num_gpus" \ + --rdzv_id="${job_id}_s2" --rdzv_backend="c10d" \ + --rdzv_endpoint="$HOST_NODE_ADDR" \ + wespeaker/bin/train.py --config "$config_s2" \ + --exp_dir "${exp_dir_s2}" \ + --gpus "$gpus" \ + --num_avg 1 \ + --data_type "${data_type}" \ + --train_data "${data}/vox2_dev/${data_type}.list" \ + --train_label "${data}/vox2_dev/utt2spk" \ + --reverb_data "${data}/rirs/lmdb" \ + --noise_data "${data}/musan/lmdb" \ + --checkpoint "${exp_dir_s2}/models/model_0.pt" +fi + +# Intermediate evaluation after Stage 6 +if [ ${stage} -le 7 ] && [ ${stop_stage} -ge 7 ]; then + echo "Stage 7: Evaluate joint fine-tuned model..." + avg_model_s2=${exp_dir_s2}/models/avg_model.pt + num_avg_s2=$(yq '.num_avg' "$config_s2") + echo "Averaging model for Stage 2..." + python wespeaker/bin/average_model.py \ + --dst_model "$avg_model_s2" \ + --src_path "${exp_dir_s2}/models" \ + --num "${num_avg_s2:-1}" + + echo "Extracting embeddings for Stage 2 model..." + local/extract_vox.sh \ + --exp_dir "$exp_dir_s2" --model_path "$avg_model_s2" \ + --nj 8 --gpus "$gpus" --data_type "$data_type" --data "${data}" + + echo "Scoring Stage 2 model..." + local/score.sh \ + --stage 1 --stop-stage 2 --data "${data}" \ + --exp_dir "$exp_dir_s2" --trials "$trials" + + # Optional: score normalization and calibration for S2 + local/score_norm.sh \ + --stage 1 --stop-stage 3 \ + --score_norm_method "$score_norm_method" \ + --cohort_set vox2_dev --top_n "$top_n" \ + --data "${data}" --exp_dir "$exp_dir_s2" --trials "$trials" + + local/score_calibration.sh \ + --stage 1 --stop-stage 5 \ + --score_norm_method "$score_norm_method" \ + --calibration_trial "vox2_cali.kaldi" \ + --cohort_set vox2_dev --top_n "$top_n" \ + --data "${data}" --exp_dir "$exp_dir_s2" --trials "$trials" +fi + +# Stage 8: large margin fine-tuning (full model) +if [ ${stage} -le 8 ] && [ ${stop_stage} -ge 8 ]; then + echo "Stage 8: Start large margin fine-tuning (full model)..." + # Initialize S3 training from averaged S2 model + init_checkpoint_s3="${exp_dir_s2}/models/avg_model.pt" + if [ ! -f "$init_checkpoint_s3" ]; then + echo "Error: Averaged model from Stage 2 ($init_checkpoint_s3) "\ +"not found." + echo "Run previous stages." + exit 1 + fi + + mkdir -p "${exp_dir_s3}/models" + cp "$init_checkpoint_s3" "${exp_dir_s3}/models/model_0.pt" + + num_gpus=$(echo "$gpus" | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes="$num_nodes" --nproc_per_node="$num_gpus" \ + --rdzv_id="${job_id}_s3" --rdzv_backend="c10d" \ + --rdzv_endpoint="$HOST_NODE_ADDR" \ + wespeaker/bin/train.py --config "$config_s3" \ + --exp_dir "${exp_dir_s3}" \ + --gpus "$gpus" \ + --num_avg 1 \ + --data_type "${data_type}" \ + --train_data "${data}/vox2_dev/${data_type}.list" \ + --train_label "${data}/vox2_dev/utt2spk" \ + --reverb_data "${data}/rirs/lmdb" \ + --noise_data "${data}/musan/lmdb" \ + --checkpoint "${exp_dir_s3}/models/model_0.pt" +fi + +# Final evaluation after Stage 8 +if [ ${stage} -le 9 ] && [ ${stop_stage} -ge 9 ]; then + echo "Stage 9: Evaluate large margin fine-tuned model..." + avg_model_s3=${exp_dir_s3}/models/avg_model.pt + num_avg_s3=$(yq '.num_avg' "$config_s3") + echo "Averaging model for Stage 3..." + python wespeaker/bin/average_model.py \ + --dst_model "$avg_model_s3" \ + --src_path "${exp_dir_s3}/models" \ + --num "${num_avg_s3:-1}" + + echo "Extracting embeddings for Stage 3 model..." + local/extract_vox.sh \ + --exp_dir "$exp_dir_s3" --model_path "$avg_model_s3" \ + --nj 8 --gpus "$gpus" --data_type "$data_type" --data "${data}" + + echo "Scoring Stage 3 model..." + local/score.sh \ + --stage 1 --stop-stage 2 --data "${data}" \ + --exp_dir "$exp_dir_s3" --trials "$trials" + + # Optional: score normalization and calibration for S3 + local/score_norm.sh \ + --stage 1 --stop-stage 3 \ + --score_norm_method "$score_norm_method" \ + --cohort_set vox2_dev --top_n "$top_n" \ + --data "${data}" --exp_dir "$exp_dir_s3" --trials "$trials" + + local/score_calibration.sh \ + --stage 1 --stop-stage 5 \ + --score_norm_method "$score_norm_method" \ + --calibration_trial "vox2_cali.kaldi" \ + --cohort_set vox2_dev --top_n "$top_n" \ + --data "${data}" --exp_dir "$exp_dir_s3" --trials "$trials" +fi + +echo "All stages completed." diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/run_wavlm.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/run_wavlm.sh new file mode 100644 index 0000000000000000000000000000000000000000..40eed073e28a2add26c15b326b7f0e042d03e6b7 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2/run_wavlm.sh @@ -0,0 +1,163 @@ +#!/bin/bash + +# Copyright 2024 Hongji Wang (jijijiang77@gmail.com) + +. ./path.sh || exit 1 + +stage=-1 +stop_stage=-1 + +HOST_NODE_ADDR="localhost:29400" +num_nodes=1 +job_id=2024 + +data=data +data_type="shard" # shard/raw + +config=conf/ecapa_tdnn_WavLM_frozen.yaml +exp_dir=exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-WavLM_large_frozen_num_frms150-aug0.6-spTrue-saFalse-ArcMargin_intertopk_subcenter-SGD-epoch150 +gpus="[0,1,2,3,4,5,6,7]" +num_avg=10 +checkpoint= + +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +score_norm_method="asnorm" # asnorm/snorm +top_n=300 + +# setup for joint ft and lmft +joint_ft_config=conf/ecapa_tdnn_WavLM_joint_ft.yaml +joint_ft_exp_dir=exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-WavLM_Large_joint_ft-num_frms150-aug0.6-spTrue-saFalse-ArcMargin_intertopk_subcenter-SGD-epoch20 +joint_lmft_config=conf/ecapa_tdnn_WavLM_joint_lmft.yaml +joint_lmft_exp_dir=exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-WavLM_Large_joint_lmft-num_frms300-aug0.6-spTrue-saFalse-ArcMargin_intertopk_subcenter-SGD-epoch10 + +. tools/parse_options.sh || exit 1 + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Prepare datasets ..." + ./local/prepare_data.sh --stage 2 --stop_stage 4 --data ${data} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Covert train and test data to ${data_type}..." + for dset in vox2_dev vox1; do + if [ $data_type == "shard" ]; then + python tools/make_shard_list.py --num_utts_per_shard 1000 \ + --num_threads 16 \ + --prefix shards \ + --shuffle \ + ${data}/$dset/wav.scp ${data}/$dset/utt2spk \ + ${data}/$dset/shards ${data}/$dset/shard.list + else + python tools/make_raw_list.py ${data}/$dset/wav.scp \ + ${data}/$dset/utt2spk ${data}/$dset/raw.list + fi + done + # Convert all musan data to LMDB + python tools/make_lmdb.py ${data}/musan/wav.scp ${data}/musan/lmdb + # Convert all rirs data to LMDB + python tools/make_lmdb.py ${data}/rirs/wav.scp ${data}/rirs/lmdb +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Start training ..." + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/bin/train.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + --data_type "${data_type}" \ + --train_data ${data}/vox2_dev/${data_type}.list \ + --train_label ${data}/vox2_dev/utt2spk \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + ${checkpoint:+--checkpoint $checkpoint} +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Do model average ..." + avg_model=$exp_dir/models/avg_model.pt + false && python wespeaker/bin/average_model.py \ + --dst_model $avg_model \ + --src_path $exp_dir/models \ + --num ${num_avg} + + echo "Extract embeddings ..." + local/extract_vox.sh \ + --exp_dir $exp_dir --model_path $avg_model \ + --nj 8 --gpus $gpus --data_type $data_type --data ${data} +fi + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "Score ..." + local/score.sh \ + --stage 1 --stop-stage 2 \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "Score norm ..." + local/score_norm.sh \ + --stage 1 --stop-stage 3 \ + --score_norm_method $score_norm_method \ + --cohort_set vox2_dev \ + --top_n $top_n \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + +if [ ${stage} -le 7 ] && [ ${stop_stage} -ge 7 ]; then + echo "Score calibration ..." + local/score_calibration.sh \ + --stage 1 --stop-stage 5 \ + --score_norm_method $score_norm_method \ + --calibration_trial "vox2_cali.kaldi" \ + --cohort_set vox2_dev \ + --top_n $top_n \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + +if [ ${stage} -le 8 ] && [ ${stop_stage} -ge 8 ]; then + echo "Joint fine-tuning ..." + mkdir -p ${joint_ft_exp_dir}/models + # Use the average frozen model to initialize the joint-ft training + cp ${exp_dir}/models/avg_model.pt ${joint_ft_exp_dir}/models/model_0.pt + bash run_wavlm.sh --stage 3 --stop_stage 7 \ + --data ${data} \ + --data_type ${data_type} \ + --config ${joint_ft_config} \ + --exp_dir ${joint_ft_exp_dir} \ + --gpus $gpus \ + --num_avg 3 \ + --checkpoint ${joint_ft_exp_dir}/models/model_0.pt \ + --trials "$trials" \ + --score_norm_method ${score_norm_method} \ + --top_n ${top_n} +fi + +if [ ${stage} -le 9 ] && [ ${stop_stage} -ge 9 ]; then + echo "Joint LM fine-tuning ..." + [ ! -f ${joint_ft_exp_dir}/models/avg_model.pt ] && + echo "Please do joint fint-tuning first" && exit 1 + mkdir -p ${joint_lmft_exp_dir}/models + # Use the average joint_ft model to initialize the joint_lmft training + cp ${joint_ft_exp_dir}/models/avg_model.pt ${joint_lmft_exp_dir}/models/model_0.pt + bash run_wavlm.sh --stage 3 --stop_stage 7 \ + --data ${data} \ + --data_type ${data_type} \ + --config ${joint_lmft_config} \ + --exp_dir ${joint_lmft_exp_dir} \ + --gpus $gpus \ + --num_avg 1 \ + --checkpoint ${joint_lmft_exp_dir}/models/model_0.pt \ + --trials "$trials" \ + --score_norm_method ${score_norm_method} \ + --top_n ${top_n} +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/README.md new file mode 100644 index 0000000000000000000000000000000000000000..113953b19dd51dac8bd8964dd3dad74284701b5a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/README.md @@ -0,0 +1,22 @@ +## Results + +* Setup: fbank80, num_frms200, ArcMargin, aug_prob0.6, speed_perturb (no spec_aug) +* Scoring: cosine (sub mean of vox2_dev) +* Metric: EER(%) + +| Model | Params | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:------|:------:|:------------:|:------------:|:------------:| +| XVEC-TSTP-emb512 | 4.61M | 1.941 | 1.896 | 3.314 | +| ECAPA_TDNN_GLOB_c512-ASTP-emb192 | 6.19M | 1.090 | 1.207 | 2.280 | +| ECAPA_TDNN_GLOB_c1024-ASTP-emb192 | 14.65M | 1.010 | 1.070 | 1.997 | +| ResNet34-TSTP-emb256 | 6.70M | 1.000 | 1.067 | 1.990 | +| ResNet101-TSTP-emb256 | 15.95M | **0.739** | **0.863** | **1.562** | + +* Comparison among different augmentation methods based on ResNet34-TSTP-emb256 model + +| Aug method | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:-----------|:------------:|:------------:|:------------:| +| no aug | 1.329 | 1.354 | 2.335 | +| kaldi offline aug | 1.165 | 1.238 | 2.208 | +| wespeaker online aug | **1.000** | **1.067** | **1.990** | + diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/conf/ecapa_tdnn.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/conf/ecapa_tdnn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..194f7ef5710f9e29bf144b4c633ded243655e587 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/conf/ecapa_tdnn.yaml @@ -0,0 +1,65 @@ +### train configuraton + +exp_dir: exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +feature_args: + raw_wav: True + num_frms: 200 + +dataset_args: + train_scp: data/vox2_dev/wav.scp + train_label: data/vox2_dev/utt2spk + aug_prob: 0.6 # for wav augmentation only + musan_scp: data/musan/wav.scp + rirs_scp: data/rirs/wav.scp + speed_perturb: True # for wav augmentation only + spec_aug: False + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +model: ECAPA_TDNN_GLOB_c512 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 192 + pooling_func: 'ASTP' +projection_args: + project_type: 'arc_margin' # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: 'exp' # exp, linear + +loss: CrossEntropyLoss +loss_args: { } + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/conf/resnet.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/conf/resnet.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fcdb40777956041ac7a2005b68b8f28e221c31a7 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/conf/resnet.yaml @@ -0,0 +1,65 @@ +### train configuraton + +exp_dir: exp/ResNet34-TSTP-emb256-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +feature_args: + raw_wav: True + num_frms: 200 + +dataset_args: + train_scp: data/vox2_dev/wav.scp + train_label: data/vox2_dev/utt2spk + aug_prob: 0.6 + musan_scp: data/musan/wav.scp + rirs_scp: data/rirs/wav.scp + speed_perturb: True + spec_aug: False + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +model: ResNet34 # ResNet18, ResNet34, ResNet50, ResNet101, ResNet152 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 256 + pooling_func: 'TSTP' +projection_args: + project_type: 'arc_margin' # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: 'exp' # exp, linear + +loss: CrossEntropyLoss +loss_args: { } + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/conf/xvec.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/conf/xvec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..758ed1d1b83a8756b7e13661744af03953bd2a57 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/conf/xvec.yaml @@ -0,0 +1,65 @@ +### train configuraton + +exp_dir: exp/XVEC-TSTP-emb512-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +feature_args: + raw_wav: True + num_frms: 200 + +dataset_args: + train_scp: data/vox2_dev/wav.scp + train_label: data/vox2_dev/utt2spk + aug_prob: 0.6 + musan_scp: data/musan/wav.scp + rirs_scp: data/rirs/wav.scp + speed_perturb: True + spec_aug: False + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 8 + drop_last: True + +model: XVEC +model_init: null +model_args: + feat_dim: 80 + embed_dim: 512 + pooling_func: 'TSTP' +projection_args: + project_type: 'arc_margin' # add_margin, arc_margin, sphere, softmax + scale: 32.0 + easy_margin: False + +margin_scheduler: MarginScheduler +margin_update: + initial_margin: 0.0 + final_margin: 0.2 + increase_start_epoch: 20 + fix_start_epoch: 40 + update_margin: True + increase_type: 'exp' # exp, linear + +loss: CrossEntropyLoss +loss_args: { } + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/download_data.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/download_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..e3e12109d7ed1f7ec2d7ea7813708f1496c35d02 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/download_data.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +download_dir=data/download_data + +. tools/parse_options.sh || exit 1 + +[ ! -d ${download_dir} ] && mkdir -p ${download_dir} + +if [ ! -f ${download_dir}/musan.tar.gz ]; then + echo "Downloading musan.tar.gz ..." + wget --no-check-certificate https://openslr.elda.org/resources/17/musan.tar.gz -P ${download_dir} + md5=$(md5sum ${download_dir}/musan.tar.gz | awk '{print $1}') + [ $md5 != "0c472d4fc0c5141eca47ad1ffeb2a7df" ] && echo "Wrong md5sum of musan.tar.gz" && exit 1 +fi + +if [ ! -f ${download_dir}/rirs_noises.zip ]; then + echo "Downloading rirs_noises.zip ..." + wget --no-check-certificate https://us.openslr.org/resources/28/rirs_noises.zip -P ${download_dir} + md5=$(md5sum ${download_dir}/rirs_noises.zip | awk '{print $1}') + [ $md5 != "e6f48e257286e05de56413b4779d8ffb" ] && echo "Wrong md5sum of rirs_noises.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox1_test_wav.zip ]; then + echo "Downloading vox1_test_wav.zip ..." + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_test_wav.zip -P ${download_dir} + md5=$(md5sum ${download_dir}/vox1_test_wav.zip | awk '{print $1}') + [ $md5 != "185fdc63c3c739954633d50379a3d102" ] && echo "Wrong md5sum of vox1_test_wav.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox1_dev_wav.zip ]; then + echo "Downloading vox1_dev_wav.zip ..." + for part in a b c d; do + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_dev_wav_parta${part} -P ${download_dir} & + done + wait + cat ${download_dir}/vox1_dev* >${download_dir}/vox1_dev_wav.zip + md5=$(md5sum ${download_dir}/vox1_dev_wav.zip | awk '{print $1}') + [ $md5 != "ae63e55b951748cc486645f532ba230b" ] && echo "Wrong md5sum of vox1_dev_wav.zip" && exit 1 +fi + +if [ ! -f ${download_dir}/vox2_aac.zip ]; then + echo "Downloading vox2_aac.zip ..." + for part in a b c d e f g h; do + wget --no-check-certificate https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox2_dev_aac_parta${part} -P ${download_dir} & + done + wait + cat ${download_dir}/vox2_dev_aac* >${download_dir}/vox2_aac.zip + md5=$(md5sum ${download_dir}/vox2_aac.zip | awk '{print $1}') + [ $md5 != "bbc063c46078a602ca71605645c2a402" ] && echo "Wrong md5sum of vox2_aac.zip" && exit 1 +fi + +echo "Download success !!!" diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/extract_vox.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/extract_vox.sh new file mode 100644 index 0000000000000000000000000000000000000000..3e3f4c39a6a00f4027c824f7133e97817bc75b6f --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/extract_vox.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir='' +model_path='' +nj=4 +gpus="[0,1]" + +. tools/parse_options.sh +set -e + +data_path_array=("data/vox2_dev/wav.scp" "data/vox1/wav.scp") +raw_wav_array=(True True) +#data_path_array=("data/vox2_dev/feats.scp" "data/vox1/feats.scp") +#raw_wav_array=(False False) + +data_name_array=("vox2_dev" "vox1") +nj_array=($nj $nj) +batch_size_array=(16 1) # batch_size of test set must be 1 !!! +num_workers_array=(4 1) +count=${#data_name_array[@]} + +for i in $(seq 0 $(($count - 1))); do + bash tools/extract_embedding_deprecated.sh --exp_dir ${exp_dir} \ + --model_path $model_path \ + --data_scp ${data_path_array[$i]} \ + --store_dir ${data_name_array[$i]} \ + --batch_size ${batch_size_array[$i]} \ + --num_workers ${num_workers_array[$i]} \ + --raw_wav ${raw_wav_array[$i]} \ + --nj ${nj_array[$i]} \ + --gpus $gpus & +done + +wait + +echo "Embedding dir is (${exp_dir}/embeddings)." diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/m4a2wav.pl b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/m4a2wav.pl new file mode 100644 index 0000000000000000000000000000000000000000..11e10acb8b267661dbcc013209b23cb6ebc05f25 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/m4a2wav.pl @@ -0,0 +1,64 @@ +#!/usr/bin/perl +# +# Copyright 2018 Ewald Enzinger +# 2022 Hongji Wang +# +# Usage: m4a2wav.pl /export/voxceleb2_m4a dev /export/voxceleb2_wav +# +# Note: This script requires ffmpeg to be installed and its location included in $PATH. + +if (@ARGV != 3) { + print STDERR "Usage: $0 \n"; + print STDERR "e.g. $0 /export/voxceleb2_m4a dev /export/voxceleb2_wav\n"; + exit(1); +} + +# Check that ffmpeg is installed. +if (`which ffmpeg` eq "") { + die "Error: this script requires that ffmpeg is installed."; +} + +($database_m4a, $dataset, $database_wav) = @ARGV; + +if ("$dataset" ne "dev" && "$dataset" ne "test") { + die "dataset parameter must be 'dev' or 'test'!"; +} + +opendir my $dh, "$database_m4a/$dataset/aac" or die "Cannot open directory: $!"; +my @spkr_dirs = grep {-d "$database_m4a/$dataset/aac/$_" && ! /^\.{1,2}$/} readdir($dh); +closedir $dh; + +if (system("mkdir -p $database_wav/$dataset") != 0) { + die "Error making directory $database_wav/$dataset"; +} + +open(fp, ">", "$database_wav/$dataset/m4a2wav_$dataset.sh") or die "Could not open the output file $database_wav/m4a2wav.sh"; + +foreach (@spkr_dirs) { + my $spkr_id = $_; + + opendir my $dh, "$database_m4a/$dataset/aac/$spkr_id/" or die "Cannot open directory: $!"; + my @rec_dirs = grep {-d "$database_m4a/$dataset/aac/$spkr_id/$_" && ! /^\.{1,2}$/} readdir($dh); + closedir $dh; + + foreach (@rec_dirs) { + my $rec_id = $_; + + opendir my $dh, "$database_m4a/$dataset/aac/$spkr_id/$rec_id/" or die "Cannot open directory: $!"; + my @files = map{s/\.[^.]+$//;$_}grep {/\.m4a$/} readdir($dh); + closedir $dh; + + foreach (@files) { + my $name = $_; + if ( not -e "$database_wav/$dataset/aac/$spkr_id/$rec_id"){ + system("mkdir -p $database_wav/$dataset/aac/$spkr_id/$rec_id"); + } + my $wav = "ffmpeg -v 8 -i $database_m4a/$dataset/aac/$spkr_id/$rec_id/$name.m4a -f wav -acodec pcm_s16le $database_wav/$dataset/aac/$spkr_id/$rec_id/$name.wav"; + print fp "$wav", "\n"; + } + } +} +close(fp) or die; + +# generate wav +#system("sh $database_wav/$dataset/m4a2wav_$dataset.sh"); diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/prepare_data.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/prepare_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..8f1528428608b43f6dd646bee9ceab494647eae4 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/prepare_data.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +stage=-1 +stop_stage=-1 +data=data + +. tools/parse_options.sh || exit 1 + +data=`realpath ${data}` +download_dir=${data}/download_data +rawdata_dir=${data}/raw_data + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Download musan.tar.gz, rirs_noises.zip, vox1_test_wav.zip, vox1_dev_wav.zip, and vox2_aac.zip." + echo "This may take a long time. Thus we recommand you to download all archives above in your own way first." + + ./local/download_data.sh --download_dir ${download_dir} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Decompress all archives ..." + echo "This could take some time ..." + + for archive in musan.tar.gz rirs_noises.zip vox1_test_wav.zip vox1_dev_wav.zip vox2_aac.zip; do + [ ! -f ${download_dir}/$archive ] && echo "Archive $archive not exists !!!" && exit 1 + done + [ ! -d ${rawdata_dir} ] && mkdir -p ${rawdata_dir} + + if [ ! -d ${rawdata_dir}/musan ]; then + tar -xzvf ${download_dir}/musan.tar.gz -C ${rawdata_dir} + fi + + if [ ! -d ${rawdata_dir}/RIRS_NOISES ]; then + unzip ${download_dir}/rirs_noises.zip -d ${rawdata_dir} + fi + + if [ ! -d ${rawdata_dir}/voxceleb1 ]; then + mkdir -p ${rawdata_dir}/voxceleb1/test ${rawdata_dir}/voxceleb1/dev + unzip ${download_dir}/vox1_test_wav.zip -d ${rawdata_dir}/voxceleb1/test + unzip ${download_dir}/vox1_dev_wav.zip -d ${rawdata_dir}/voxceleb1/dev + fi + + if [ ! -d ${rawdata_dir}/voxceleb2_m4a ]; then + mkdir -p ${rawdata_dir}/voxceleb2_m4a + unzip ${download_dir}/vox2_aac.zip -d ${rawdata_dir}/voxceleb2_m4a + fi + + echo "Decompress success !!!" +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Convert voxceleb2 wav format from m4a to wav using ffmpeg." + echo "This could also take some time ..." + + if [ ! -d ${rawdata_dir}/voxceleb2_wav ]; then + ./local/m4a2wav.pl ${rawdata_dir}/voxceleb2_m4a dev ${rawdata_dir}/voxceleb2_wav + # Here we use 8 parallel jobs + cat ${rawdata_dir}/voxceleb2_wav/dev/m4a2wav_dev.sh | xargs -P 8 -i sh -c "{}" + fi + + echo "Convert m4a2wav success !!!" +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Prepare wav.scp for each dataset ..." + export LC_ALL=C # kaldi config + + mkdir -p ${data}/musan ${data}/rirs ${data}/vox1 ${data}/vox2_dev + # musan + find ${rawdata_dir}/musan -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' >${data}/musan/wav.scp + # rirs + find ${rawdata_dir}/RIRS_NOISES/simulated_rirs -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' >${data}/rirs/wav.scp + # vox1 + find ${rawdata_dir}/voxceleb1 -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' | sort >${data}/vox1/wav.scp + awk '{print $1}' ${data}/vox1/wav.scp | awk -F "/" '{print $0,$1}' >${data}/vox1/utt2spk + ./tools/utt2spk_to_spk2utt.pl ${data}/vox1/utt2spk >${data}/vox1/spk2utt + if [ ! -d ${data}/vox1/trials ]; then + echo "Download trials for vox1 ..." + mkdir -p ${data}/vox1/trials + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test.txt -O ${data}/vox1/trials/vox1-O.txt + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_hard.txt -O ${data}/vox1/trials/vox1-H.txt + #wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_all.txt -O ${data}/vox1/trials/vox1-E.txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test2.txt -O ${data}/vox1/trials/vox1-O\(cleaned\).txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_hard2.txt -O ${data}/vox1/trials/vox1-H\(cleaned\).txt + wget --no-check-certificate https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/list_test_all2.txt -O ${data}/vox1/trials/vox1-E\(cleaned\).txt + # transform them into kaldi trial format + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1/trials/vox1-O\(cleaned\).txt >${data}/vox1/trials/vox1_O_cleaned.kaldi + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1/trials/vox1-H\(cleaned\).txt >${data}/vox1/trials/vox1_H_cleaned.kaldi + awk '{if($1==0)label="nontarget";else{label="target"}; print $2,$3,label}' ${data}/vox1/trials/vox1-E\(cleaned\).txt >${data}/vox1/trials/vox1_E_cleaned.kaldi + fi + # vox2 + find ${rawdata_dir}/voxceleb2_wav -name "*.wav" | awk -F"/" '{print $(NF-2)"/"$(NF-1)"/"$NF,$0}' | sort >${data}/vox2_dev/wav.scp + awk '{print $1}' ${data}/vox2_dev/wav.scp | awk -F "/" '{print $0,$1}' >${data}/vox2_dev/utt2spk + ./tools/utt2spk_to_spk2utt.pl ${data}/vox2_dev/utt2spk >${data}/vox2_dev/spk2utt + + echo "Success !!! Now data preparation is done !!!" +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/score.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/score.sh new file mode 100644 index 0000000000000000000000000000000000000000..72152175c0c0d8e74bbc2b630bfa6a811ea87dfd --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/score.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir= +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "apply cosine scoring ..." + mkdir -p ${exp_dir}/scores + trials_dir=data/vox1/trials + for x in $trials; do + echo $x + python wespeaker/bin/score.py \ + --exp_dir ${exp_dir} \ + --eval_scp_path ${exp_dir}/embeddings/vox1/xvector.scp \ + --cal_mean True \ + --cal_mean_dir ${exp_dir}/embeddings/vox2_dev \ + ${trials_dir}/${x} + done +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "compute metrics (EER/minDCF) ..." + scores_dir=${exp_dir}/scores + for x in $trials; do + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${x}.score \ + 2>&1 | tee -a ${scores_dir}/vox1_cos_result + + echo "compute DET curve ..." + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${x}.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/score_norm.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/score_norm.sh new file mode 100644 index 0000000000000000000000000000000000000000..9e3a07a80777bfa2d8c740431915aebcf553a23c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/local/score_norm.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +score_norm_method="asnorm" # asnorm/snorm +cohort_set=vox2_dev +top_n=100 +exp_dir= +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" + +stage=-1 +stop_stage=-1 + +. tools/parse_options.sh +. path.sh + + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + echo "compute mean xvector" + python tools/vector_mean.py \ + --spk2utt data/${cohort_set}/spk2utt \ + --xvector_scp $exp_dir/embeddings/${cohort_set}/xvector.scp \ + --spk_xvector_ark $exp_dir/embeddings/${cohort_set}/spk_xvector.ark +fi + +output_name=${cohort_set}_${score_norm_method} +[ "${score_norm_method}" == "asnorm" ] && output_name=${output_name}${top_n} +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + echo "compute norm score" + for x in $trials; do + python wespeaker/bin/score_norm.py \ + --score_norm_method $score_norm_method \ + --top_n $top_n \ + --trial_score_file $exp_dir/scores/${x}.score \ + --score_norm_file $exp_dir/scores/${output_name}_${x}.score \ + --cohort_emb_scp ${exp_dir}/embeddings/${cohort_set}/spk_xvector.scp \ + --eval_emb_scp ${exp_dir}/embeddings/vox1/xvector.scp \ + --mean_vec_path ${exp_dir}/embeddings/vox2_dev/mean_vec.npy + done +fi + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + echo "compute metrics" + for x in ${trials}; do + scores_dir=${exp_dir}/scores + python wespeaker/bin/compute_metrics.py \ + --p_target 0.01 \ + --c_fa 1 \ + --c_miss 1 \ + ${scores_dir}/${output_name}_${x}.score \ + 2>&1 | tee -a ${scores_dir}/vox1_${score_norm_method}${top_n}_result + + python wespeaker/bin/compute_det.py \ + ${scores_dir}/${output_name}_${x}.score + done +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/path.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/path.sh new file mode 100644 index 0000000000000000000000000000000000000000..b90a5154cf4a8f25b9a42851c59393f07dacb350 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/path.sh @@ -0,0 +1,5 @@ +export PATH=$PWD:$PATH + +# NOTE(kan-bayashi): Use UTF-8 in Python to avoid UnicodeDecodeError when LC_ALL=C +export PYTHONIOENCODING=UTF-8 +export PYTHONPATH=../../../:$PYTHONPATH diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..26342abf7556715a6ef22ce0596f0ce9ffbb89be --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v2_deprecated/run.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Copyright 2022 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) + +. ./path.sh || exit 1; + +stage=-1 +stop_stage=-1 + +HOST_NODE_ADDR="localhost:29400" +num_nodes=1 +job_id=2024 + +config=conf/resnet.yaml +exp_dir=exp/ResNet34-TSTP-emb256-fbank80-num_frms200-aug0.6-spTrue-saFalse-ArcMargin-SGD-epoch150 +gpus="[0,1]" +num_avg=10 +checkpoint= + +score_norm_method="asnorm" # asnorm/snorm +top_n=300 +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" + +. tools/parse_options.sh || exit 1 + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Prepare datasets ..." + ./local/prepare_data.sh --stage 2 --stop_stage 4 +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Start training ..." + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/bin/train_deprecated.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + ${checkpoint:+--checkpoint $checkpoint} +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Do model average ..." + avg_model=$exp_dir/models/avg_model.pt + python wespeaker/bin/average_model.py \ + --dst_model $avg_model \ + --src_path $exp_dir/models \ + --num ${num_avg} + + echo "Extract embeddings ..." + local/extract_vox.sh --exp_dir $exp_dir --model_path $avg_model --nj 4 --gpus $gpus +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Score ..." + local/score.sh \ + --stage 1 --stop-stage 2 \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "Score norm ..." + local/score_norm.sh \ + --stage 1 --stop-stage 3 \ + --score_norm_method $score_norm_method \ + --cohort_set vox2_dev \ + --top_n $top_n \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "Export the best model ..." + python wespeaker/bin/export_jit.py \ + --config $exp_dir/config.yaml \ + --checkpoint $exp_dir/models/avg_model.pt \ + --output_file $exp_dir/models/final.zip +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4eb765a64ee21ea5758620e10f92064a2b5bbc85 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/README.md @@ -0,0 +1,17 @@ +## Results + +* Setup: fbank80, num_frms200(short) 300(long), epoch150, CE, aug_prob1.0, no speed_perturb, no spec_aug +* Scoring: cosine (sub mean of vox2_dev) +* Metric: EER(%) + +| Model | Params | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:------|:------:|:------------:|:------------:|:------------:| +| ECAPA_TDNN_GLOB_c512-ASTP-emb192 | 6.19M | 3.016 | 3.093 | 5.538 | +| ECAPA_TDNN_GLOB_c1024-ASTP-emb192 | 14.65M | 2.627 | 2.665 | 4.644 | +| ResNet34-TSTP-emb256 | 6.63M | 3.170 | 3.324 | 5.821 | + + +* 🔥 UPDATE 2023.07: We support DINO based self-supervised speaker verification framework. Related papers: + * [A comprehensive study on self-supervised distillation for speaker representation learning](https://arxiv.org/pdf/2210.15936.pdf) + * [Emerging properties in self-supervised vision transformers](https://openaccess.thecvf.com/content/ICCV2021/papers/Caron_Emerging_Properties_in_Self-Supervised_Vision_Transformers_ICCV_2021_paper.pdf) + * [Self-supervised speaker verification using dynamic loss-gate and label correction](https://arxiv.org/pdf/2208.01928.pdf) \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/conf/ecapa_tdnn.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/conf/ecapa_tdnn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5e04d0c0a84be3ac174f4c43bf001519a3f71d21 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/conf/ecapa_tdnn.yaml @@ -0,0 +1,80 @@ +### train configuraton + +exp_dir: exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-fbank80-aug1.0-spFalse-saFalse-DINO-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 1 # save model every epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 64 + num_workers: 12 + pin_memory: False + prefetch_factor: 2 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + resample_rate: 16000 + speed_perturb: False + chunk_info_args: + global_chunk_len: 300 + global_chunk_num: 2 + local_chunk_len: 200 + local_chunk_num: 4 + aug_prob: 1.0 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ECAPA_TDNN_GLOB_c512 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 192 + pooling_func: "ASTP" + emb_bn: True + +dino_head_args: + out_dim: 65536 + use_bn: True + norm_last_layer: True + nlayers: 3 + normalize_input: False + +dino_loss_args: + warmup_teacher_temp: 0.04 + teacher_temp: 0.07 + warmup_teacher_temp_epochs_ratio: 0.2 + student_temp: 0.1 + center_momentum: 0.9 + +optim_type: "sgd" # choose from sgd, adamw, lars +clip_grad: 3.0 +freeze_last_layer: 1 + +scheduler_args: + lr: 0.2 + min_lr: 0.00005 + warmup_epochs: 20 + weight_decay: 0.00005 + weight_decay_end: 0.00005 + momentum_teacher: 0.996 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/conf/resnet34.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/conf/resnet34.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e54d450510257825ddc187b747bcd439f60313f4 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/conf/resnet34.yaml @@ -0,0 +1,80 @@ +### train configuraton + +exp_dir: exp/ResNet34-TSTP-emb256-fbank80-aug1.0-spFalse-saFalse-DINO-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 1 # save model every epoch +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 48 + num_workers: 12 + pin_memory: False + prefetch_factor: 2 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + resample_rate: 16000 + speed_perturb: False + chunk_info_args: + global_chunk_len: 300 + global_chunk_num: 2 + local_chunk_len: 200 + local_chunk_num: 4 + aug_prob: 1.0 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ResNet34 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 256 + pooling_func: "TSTP" # TSTP, ASTP, MQMHASTP + two_emb_layer: False + +dino_head_args: + out_dim: 65536 + use_bn: True + norm_last_layer: True + nlayers: 3 + normalize_input: False + +dino_loss_args: + warmup_teacher_temp: 0.04 + teacher_temp: 0.07 + warmup_teacher_temp_epochs_ratio: 0.2 + student_temp: 0.1 + center_momentum: 0.9 + +optim_type: "sgd" # choose from sgd, adamw, lars +clip_grad: 3.0 +freeze_last_layer: 1 + +scheduler_args: + lr: 0.2 + min_lr: 0.00005 + warmup_epochs: 20 + weight_decay: 0.00005 + weight_decay_end: 0.00005 + momentum_teacher: 0.996 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/path.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/path.sh new file mode 100644 index 0000000000000000000000000000000000000000..e7917ccb32b3a746a5eebbcfa445aa280bb39ed0 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/path.sh @@ -0,0 +1,5 @@ +export PATH=$PWD:$PATH + +# NOTE(kan-bayashi): Use UTF-8 in Python to avoid UnicodeDecodeError when LC_ALL=C +export PYTHONIOENCODING=UTF-8 +export PYTHONPATH=../../../:$PWD:$PYTHONPATH diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..2dd6b8bd1285f6d9ec448a1a5011ae7093ef1af4 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/dino/run.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +# Copyright 2022 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2023 Bing Han (hanbing97@sjtu.edu.cn) + +. ./path.sh || exit 1 + +stage=-1 +stop_stage=-1 + +HOST_NODE_ADDR="localhost:29400" +num_nodes=1 +job_id=2024 + +data=data +data_type="shard" # shard/raw + +config=conf/ecapa_tdnn.yaml +exp_dir=exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-fbank80-aug1.0-spFalse-saFalse-DINO-SGD-epoch150 +gpus="[0,1,2,3,4,5,6,7]" +num_avg=10 +checkpoint= + +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +score_norm_method="asnorm" # asnorm/snorm +top_n=300 + +. tools/parse_options.sh || exit 1 + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Prepare datasets ..." + ./local/prepare_data.sh --stage 2 --stop_stage 4 --data ${data} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Covert train and test data to ${data_type}..." + for dset in vox2_dev vox1; do + if [ $data_type == "shard" ]; then + python tools/make_shard_list.py --num_utts_per_shard 1000 \ + --num_threads 16 \ + --prefix shards \ + --shuffle \ + ${data}/$dset/wav.scp ${data}/$dset/utt2spk \ + ${data}/$dset/shards ${data}/$dset/shard.list + else + python tools/make_raw_list.py ${data}/$dset/wav.scp \ + ${data}/$dset/utt2spk ${data}/$dset/raw.list + fi + done + # Convert all musan data to LMDB + python tools/make_lmdb.py ${data}/musan/wav.scp ${data}/musan/lmdb + # Convert all rirs data to LMDB + python tools/make_lmdb.py ${data}/rirs/wav.scp ${data}/rirs/lmdb +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Start training ..." + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/ssl/bin/train_dino.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + --data_type "${data_type}" \ + --train_data ${data}/vox2_dev/${data_type}.list \ + --wav_scp ${data}/vox2_dev/wav.scp \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + ${checkpoint:+--checkpoint $checkpoint} +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Do model average ..." + avg_model=$exp_dir/models/avg_model.pt + python wespeaker/ssl/bin/average_dino_model.py \ + --dst_model $avg_model \ + --src_path $exp_dir/models \ + --num ${num_avg} + + echo "Extract embeddings ..." + local/extract_vox.sh \ + --exp_dir $exp_dir --model_path $avg_model \ + --nj 4 --gpus $gpus --data_type $data_type --data ${data} +fi + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "Score ..." + local/score.sh \ + --stage 1 --stop-stage 2 \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "Export the best model ..." + python wespeaker/bin/export_jit.py \ + --config $exp_dir/config.yaml \ + --checkpoint $exp_dir/models/avg_model.pt \ + --output_file $exp_dir/models/final.zip +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6d187f7edcb7674cfbf94cc0d1b15440aecf342f --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/README.md @@ -0,0 +1,13 @@ +## Results + +* Setup: fbank80, num_frms200, epoch150, InfoNCE, aug_prob1.0, speed_perturb, no spec_aug +* Scoring: cosine (sub mean of vox2_dev) +* Metric: EER(%) + +| Model | Params | Methods | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:------|:------:|:------------:|:------------:|:------------:|:------------:| +| ECAPA_TDNN_GLOB_c512-ASTP-emb192 | 6.19M | MoCo | 8.709 | 9.287 | 14.756 | + +* 🔥 UPDATE 2023.07: We support the MoCo based self-supervised speaker verification. Related papers: + * [Momentum Contrast for Unsupervised Visual Representation Learning](https://openaccess.thecvf.com/content_CVPR_2020/papers/He_Momentum_Contrast_for_Unsupervised_Visual_Representation_Learning_CVPR_2020_paper.pdf) + * [Self-supervised Text-independent Speaker Verification using Prototypical Momentum Contrastive Learning](https://arxiv.org/abs/2012.07178) \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/conf/ecapa_tdnn.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/conf/ecapa_tdnn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9e4d987753963216a711a9df42feabd506312ff9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/conf/ecapa_tdnn.yaml @@ -0,0 +1,75 @@ +### train configuraton + +exp_dir: exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-fbank80-num_frms200-aug1.0-spTrue-saFalse-MoCo-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 4 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + resample_rate: 16000 + speed_perturb: True + chunk_info_args: + global_chunk_len: 200 + global_chunk_num: 1 + local_chunk_len: 200 + local_chunk_num: 1 + aug_prob: 1.0 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ECAPA_TDNN_GLOB_c512 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 192 + pooling_func: "ASTP" +contrastive_type: "moco" # simclr, moco +simclr_args: + T: 0.07 + mlp: False +moco_args: + K: 65536 + m: 0.999 + T: 0.07 + mlp: False + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/path.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/path.sh new file mode 100644 index 0000000000000000000000000000000000000000..e7917ccb32b3a746a5eebbcfa445aa280bb39ed0 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/path.sh @@ -0,0 +1,5 @@ +export PATH=$PWD:$PATH + +# NOTE(kan-bayashi): Use UTF-8 in Python to avoid UnicodeDecodeError when LC_ALL=C +export PYTHONIOENCODING=UTF-8 +export PYTHONPATH=../../../:$PWD:$PYTHONPATH diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..f59b95c3efea51b2feab9635cdec66b48877192a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/moco/run.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +# Copyright 2022 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2023 Bing Han (hanbing97@sjtu.edu.cn) + +. ./path.sh || exit 1 + +stage=-1 +stop_stage=-1 + +HOST_NODE_ADDR="localhost:29400" +num_nodes=1 +job_id=2024 + +data=data +data_type="shard" # shard/raw + +config=conf/ecapa_tdnn.yaml +exp_dir=exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-fbank80-num_frms200-aug1.0-spTrue-saFalse-MoCo-SGD-epoch150 +gpus="[0,1,2,3]" +num_avg=10 +checkpoint= + +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +score_norm_method="asnorm" # asnorm/snorm +top_n=300 + +. tools/parse_options.sh || exit 1 + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Prepare datasets ..." + ./local/prepare_data.sh --stage 2 --stop_stage 4 --data ${data} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Covert train and test data to ${data_type}..." + for dset in vox2_dev vox1; do + if [ $data_type == "shard" ]; then + python tools/make_shard_list.py --num_utts_per_shard 1000 \ + --num_threads 16 \ + --prefix shards \ + --shuffle \ + ${data}/$dset/wav.scp ${data}/$dset/utt2spk \ + ${data}/$dset/shards ${data}/$dset/shard.list + else + python tools/make_raw_list.py ${data}/$dset/wav.scp \ + ${data}/$dset/utt2spk ${data}/$dset/raw.list + fi + done + # Convert all musan data to LMDB + python tools/make_lmdb.py ${data}/musan/wav.scp ${data}/musan/lmdb + # Convert all rirs data to LMDB + python tools/make_lmdb.py ${data}/rirs/wav.scp ${data}/rirs/lmdb +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Start training ..." + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/ssl/bin/train_contrastive.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + --data_type "${data_type}" \ + --train_data ${data}/vox2_dev/${data_type}.list \ + --wav_scp ${data}/vox2_dev/wav.scp \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + ${checkpoint:+--checkpoint $checkpoint} +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Do model average ..." + avg_model=$exp_dir/models/avg_model.pt + python wespeaker/ssl/bin/average_contrastive_model.py \ + --dst_model $avg_model \ + --src_path $exp_dir/models \ + --num ${num_avg} + + echo "Extract embeddings ..." + local/extract_vox.sh \ + --exp_dir $exp_dir --model_path $avg_model \ + --nj 4 --gpus $gpus --data_type $data_type --data ${data} +fi + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "Score ..." + local/score.sh \ + --stage 1 --stop-stage 2 \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "Export the best model ..." + python wespeaker/bin/export_jit.py \ + --config $exp_dir/config.yaml \ + --checkpoint $exp_dir/models/avg_model.pt \ + --output_file $exp_dir/models/final.zip +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3e723a4df0878b2298797f5a68364d97f2888722 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/README.md @@ -0,0 +1,13 @@ +## Results + +* Setup: fbank80, num_frms200, epoch150, InfoNCE, aug_prob1.0, speed_perturb, no spec_aug +* Scoring: cosine (sub mean of vox2_dev) +* Metric: EER(%) + +| Model | Params | Methods | vox1-O-clean | vox1-E-clean | vox1-H-clean | +|:------|:------:|:------------:|:------------:|:------------:|:------------:| +| ECAPA_TDNN_GLOB_c512-ASTP-emb192 | 6.19M | SimCLR | 8.523 | 9.417 | 14.907 | + +* 🔥 UPDATE 2023.07: We support the SimCLR based self-supervised speaker verification. Related Papers: + * [A Simple Framework for Contrastive Learning of Visual Representations](http://proceedings.mlr.press/v119/chen20j/chen20j.pdf) + * [Contrastive Self-Supervised Learning for Text-Independent Speaker Verification](https://ieeexplore.ieee.org/document/9413351) \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/conf/ecapa_tdnn.yaml b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/conf/ecapa_tdnn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9b9accd87524ace0275d30042ad6fc65a4a073c9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/conf/ecapa_tdnn.yaml @@ -0,0 +1,75 @@ +### train configuraton + +exp_dir: exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-fbank80-num_frms200-aug1.0-spTrue-saFalse-SimCLR-SGD-epoch150 +gpus: "[0,1]" +num_avg: 10 +enable_amp: False # whether enable automatic mixed precision training + +seed: 42 +num_epochs: 150 +save_epoch_interval: 5 # save model every 5 epochs +log_batch_interval: 100 # log every 100 batchs + +dataloader_args: + batch_size: 128 + num_workers: 16 + pin_memory: False + prefetch_factor: 4 + drop_last: True + +dataset_args: + # the sample number which will be traversed within one epoch, if the value equals to 0, + # the utterance number in the dataset will be used as the sample_num_per_epoch. + sample_num_per_epoch: 0 + shuffle: True + shuffle_args: + shuffle_size: 2500 + resample_rate: 16000 + speed_perturb: True + chunk_info_args: + global_chunk_len: 200 + global_chunk_num: 1 + local_chunk_len: 200 + local_chunk_num: 1 + aug_prob: 1.0 # prob to add reverb & noise aug per sample + fbank_args: + num_mel_bins: 80 + frame_shift: 10 + frame_length: 25 + dither: 1.0 + spec_aug: False + spec_aug_args: + num_t_mask: 1 + num_f_mask: 1 + max_t: 10 + max_f: 8 + prob: 0.6 + +model: ECAPA_TDNN_GLOB_c512 # ECAPA_TDNN_GLOB_c512, ECAPA_TDNN_GLOB_c1024 +model_init: null +model_args: + feat_dim: 80 + embed_dim: 192 + pooling_func: "ASTP" +contrastive_type: "simclr" # simclr, moco +simclr_args: + T: 0.07 + mlp: False +moco_args: + K: 65536 + m: 0.999 + T: 0.07 + mlp: False + +optimizer: SGD +optimizer_args: + momentum: 0.9 + nesterov: True + weight_decay: 0.0001 + +scheduler: ExponentialDecrease +scheduler_args: + initial_lr: 0.1 + final_lr: 0.00005 + warm_up_epoch: 6 + warm_from_zero: True diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/path.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/path.sh new file mode 100644 index 0000000000000000000000000000000000000000..e7917ccb32b3a746a5eebbcfa445aa280bb39ed0 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/path.sh @@ -0,0 +1,5 @@ +export PATH=$PWD:$PATH + +# NOTE(kan-bayashi): Use UTF-8 in Python to avoid UnicodeDecodeError when LC_ALL=C +export PYTHONIOENCODING=UTF-8 +export PYTHONPATH=../../../:$PWD:$PYTHONPATH diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..3c3b1c8a54d6da8239d54b0bb806f38311e33769 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxceleb/v3/simclr/run.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +# Copyright 2022 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2023 Bing Han (hanbing97@sjtu.edu.cn) + +. ./path.sh || exit 1 + +stage=-1 +stop_stage=-1 + +HOST_NODE_ADDR="localhost:29400" +num_nodes=1 +job_id=2024 + +data=data +data_type="shard" # shard/raw + +config=conf/ecapa_tdnn.yaml +exp_dir=exp/ECAPA_TDNN_GLOB_c512-ASTP-emb192-fbank80-num_frms200-aug1.0-spTrue-saFalse-SimCLR-SGD-epoch150 +gpus="[0,1,2,3]" +num_avg=10 +checkpoint= + +trials="vox1_O_cleaned.kaldi vox1_E_cleaned.kaldi vox1_H_cleaned.kaldi" +score_norm_method="asnorm" # asnorm/snorm +top_n=300 + +. tools/parse_options.sh || exit 1 + +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + echo "Prepare datasets ..." + ./local/prepare_data.sh --stage 2 --stop_stage 4 --data ${data} +fi + +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + echo "Covert train and test data to ${data_type}..." + for dset in vox2_dev vox1; do + if [ $data_type == "shard" ]; then + python tools/make_shard_list.py --num_utts_per_shard 1000 \ + --num_threads 16 \ + --prefix shards \ + --shuffle \ + ${data}/$dset/wav.scp ${data}/$dset/utt2spk \ + ${data}/$dset/shards ${data}/$dset/shard.list + else + python tools/make_raw_list.py ${data}/$dset/wav.scp \ + ${data}/$dset/utt2spk ${data}/$dset/raw.list + fi + done + # Convert all musan data to LMDB + python tools/make_lmdb.py ${data}/musan/wav.scp ${data}/musan/lmdb + # Convert all rirs data to LMDB + python tools/make_lmdb.py ${data}/rirs/wav.scp ${data}/rirs/lmdb +fi + +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + echo "Start training ..." + num_gpus=$(echo $gpus | awk -F ',' '{print NF}') + echo "$0: num_nodes is $num_nodes, proc_per_node is $num_gpus" + torchrun --nnodes=$num_nodes --nproc_per_node=$num_gpus \ + --rdzv_id=$job_id --rdzv_backend="c10d" --rdzv_endpoint=$HOST_NODE_ADDR \ + wespeaker/ssl/bin/train_contrastive.py --config $config \ + --exp_dir ${exp_dir} \ + --gpus $gpus \ + --num_avg ${num_avg} \ + --data_type "${data_type}" \ + --train_data ${data}/vox2_dev/${data_type}.list \ + --wav_scp ${data}/vox2_dev/wav.scp \ + --reverb_data ${data}/rirs/lmdb \ + --noise_data ${data}/musan/lmdb \ + ${checkpoint:+--checkpoint $checkpoint} +fi + +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + echo "Do model average ..." + avg_model=$exp_dir/models/avg_model.pt + python wespeaker/ssl/bin/average_contrastive_model.py \ + --dst_model $avg_model \ + --src_path $exp_dir/models \ + --num ${num_avg} + + echo "Extract embeddings ..." + local/extract_vox.sh \ + --exp_dir $exp_dir --model_path $avg_model \ + --nj 4 --gpus $gpus --data_type $data_type --data ${data} +fi + +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + echo "Score ..." + local/score.sh \ + --stage 1 --stop-stage 2 \ + --data ${data} \ + --exp_dir $exp_dir \ + --trials "$trials" +fi + + +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + echo "Export the best model ..." + python wespeaker/bin/export_jit.py \ + --config $exp_dir/config.yaml \ + --checkpoint $exp_dir/models/avg_model.pt \ + --output_file $exp_dir/models/final.zip +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/README.md new file mode 100644 index 0000000000000000000000000000000000000000..85af1c2dae6399e3937ccfdb702f99a0303d9af4 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/README.md @@ -0,0 +1,7 @@ +This is a **WeSpeaker** speaker diarization recipe on the Voxconverse 2020 dataset. It focused on a ``in the wild`` scenario, which was collected from YouTube videos with a semi-automatic pipeline and released for the diarization track in VoxSRC 2020 Challenge. See https://www.robots.ox.ac.uk/~vgg/data/voxconverse/ for more detailed information. + +Two recipes are provided, including **v1** and **v2**. Their only difference is that in **v2**, we split the Fbank extraction, embedding extraction and clustering modules to different stages. We recommend newcomers to follow the **v2** recipe and run it stage by stage. + +🔥 UPDATE 2024.08.20: +* silero-vad v5.1 is used in place of v3.1 +* umap dimensionality reduction + hdbscan clustering is also supported in v2 diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0c51b555e85ac956e17b3ee34e89ca9e94c2a784 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/README.md @@ -0,0 +1,36 @@ +## Overview + +* We suggest to run this recipe on a gpu-available machine, with onnxruntime-gpu supported. +* Dataset: Voxconverse2020 (dev: 216 utts) +* Speaker model: ResNet34 model pretrained by WeSpeaker + * Refer to [voxceleb sv recipe](https://github.com/wenet-e2e/wespeaker/tree/master/examples/voxceleb/v2) + * [pretrained model path](https://wespeaker-1256283475.cos.ap-shanghai.myqcloud.com/models/voxceleb/voxceleb_resnet34_LM.onnx) +* Speaker activity detection model: + * oracle SAD (from ground truth annotation) + * system SAD (VAD model pretrained by [silero-vad](https://github.com/snakers4/silero-vad), v3.1 is deprecated now) +* Clustering method: spectral clustering +* Metric: DER = MISS + FALSE ALARM + SPEAKER CONFUSION (%) + +## Results + +* Dev set + + | system | MISS | FA | SC | DER | + |:---|:---:|:---:|:---:|:---:| + | Ours (oracle SAD + spectral clustering) | 2.3 | 0.0 | 1.9 | 4.2 | + | Ours (silero-vad v3.1 + spectral clustering) | 3.7 | 0.8 | 2.0 | 6.5 | + | DIHARD 2019 baseline [^1] | 11.1 | 1.4 | 11.3 | 23.8 | + | DIHARD 2019 baseline w/ SE [^1] | 9.3 | 1.3 | 9.7 | 20.2 | + | (SyncNet ASD only) [^1] | 2.2 | 4.1 | 4.0 | 10.4 | + | (AVSE ASD only) [^1] | 2.0 | 5.9 | 4.6 | 12.4 | + | (proposed) [^1] | 2.4 | 2.3 | 3.0 | 7.7 | + + +[^1]: Spot the conversation: speaker diarisation in the wild, https://arxiv.org/pdf/2007.01216.pdf + +## Update 09/2022 : GPU Clustering +* You can use diar/clusterer\_gpu.py to run GPU Clustering +* We use [cupy](https://cupy.dev/) and [cuML](https://docs.rapids.ai/api/cuml/stable/api.html#k-means-clustering) to accelarate the clustering speed +* You have to install the above toolkits before inference +* Similar performances can be obtained from our experiments but with ~3X speech up +* Try the test function in diar/clusterer\_gpu.py to have more details diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/diar/clusterer.py b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/diar/clusterer.py new file mode 100644 index 0000000000000000000000000000000000000000..d31a5e4d00d0545bfd39aaeba27d7963c925288a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/diar/clusterer.py @@ -0,0 +1,303 @@ +# Copyright (c) 2022 Xu Xiang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["OPENBLAS_NUM_THREADS"] = "1" +os.environ["MKL_NUM_THREADS"] = "1" +os.environ["VECLIB_MAXIMUM_THREADS"] = "1" +os.environ["NUMEXPR_NUM_THREADS"] = "1" + +import argparse +from collections import OrderedDict +import concurrent.futures as cf + +import numpy as np +import scipy.linalg +from tqdm import tqdm + +from sklearn.cluster._kmeans import k_means + +import torch +import torchaudio +import torchaudio.compliance.kaldi as kaldi + +import onnxruntime as ort + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--scp', required=True, help='wav scp') + parser.add_argument('--segments', required=True, help='vad segments') + parser.add_argument('--output', required=True, help='output label file') + parser.add_argument('--source', required=True, help='onnx model') + parser.add_argument('--device', + default='cuda', + help='inference device type: cpu or cuda') + parser.add_argument('--batch-size', + type=int, + default=96, + help='batch size for embedding extraction') + args = parser.parse_args() + + return args + + +def compute_embeddings(scp, + segments, + source, + device, + batch_size, + sampling_rate=16000, + window_secs=1.50, + period_secs=0.75, + frame_shift=10): + + def read_segments(segments): + utt_to_segments = OrderedDict() + for line in open(segments, 'r'): + seg, utt, begin, end = line.strip().split() + begin, end = float(begin), float(end) + if utt not in utt_to_segments: + utt_to_segments[utt] = [(seg, begin, end)] + else: + utt_to_segments[utt].append((seg, begin, end)) + + return utt_to_segments + + def read_scp(scp): + utt_to_wav = OrderedDict() + for line in open(scp, 'r'): + utt, wav = line.strip().split() + utt_to_wav[utt] = wav + + return utt_to_wav + + def repeat_to_fill(x, window_fs): + assert len(x.size()) == 2 + assert x.size(1) == 1 + length = x.size(0) + num = (window_fs + length - 1) // length + + return x.repeat(num, 1)[:window_fs, :] + + def subsegment(wav, segments, window_fs, period_fs): + subsegs = [] + subseg_signals = [] + + signal, fs = torchaudio.load(wav, channels_first=False) + assert len(signal.size()) == 2 + assert signal.size(1) == 1 + assert sampling_rate == fs + + for (seg, begin, end) in segments: + seg_begin = int(begin * sampling_rate) + seg_end = int(end * sampling_rate) + seg_signal = signal[seg_begin:seg_end + 1, :] + seg_length = seg_end - seg_begin + + if seg_length <= window_fs: + subseg = seg + "-{:08d}-{:08d}".format( + 0, int(seg_length / sampling_rate * 1000 // frame_shift)) + subseg_signal = repeat_to_fill(seg_signal, window_fs) + + subsegs.append(subseg) + subseg_signals.append(subseg_signal) + else: + max_subseg_begin = seg_length - window_fs + period_fs + for subseg_begin in range(0, max_subseg_begin, period_fs): + subseg_end = min(subseg_begin + window_fs, seg_length) + subseg = seg + "-{:08d}-{:08d}".format( + int(subseg_begin / sampling_rate * 1000 / frame_shift), + int(subseg_end / sampling_rate * 1000 / frame_shift)) + subseg_signal = repeat_to_fill( + seg_signal[subseg_begin:subseg_end + 1, :], window_fs) + + subsegs.append(subseg) + subseg_signals.append(subseg_signal) + + return subsegs, subseg_signals + + def compute_fbank(wavs, + num_mel_bins=80, + frame_length=25, + frame_shift=10, + dither=0.0, + sample_frequency=16000): + + feats = [] + for wav in wavs: + wav = wav.unsqueeze(0) * (1 << 15) + feat = kaldi.fbank(wav, + num_mel_bins=num_mel_bins, + frame_length=frame_length, + frame_shift=frame_shift, + dither=dither, + sample_frequency=sample_frequency, + window_type='hamming', + use_energy=False) + feat = feat - torch.mean(feat, dim=0) # CMN + feats.append(feat) + feats = torch.stack(feats) + + return feats + + def init_session(source, device): + # Initialize ONNX session + if device == "cpu": + providers = ["CPUExecutionProvider"] + elif device == "cuda": + providers = ["CUDAExecutionProvider"] + else: + raise ValueError + + opts = ort.SessionOptions() + opts.inter_op_num_threads = 1 + opts.intra_op_num_threads = 1 + session = ort.InferenceSession(source, + sess_options=opts, + providers=providers) + return session + + def extract_embeddings(wavs, batch_size): + embeddings = [] + for i in range(0, wavs.size(0), batch_size): + batch_wavs = wavs[i:i + batch_size, :] + batch_feats = compute_fbank(batch_wavs) + batch_embs = session.run(input_feed={'feats': batch_feats.numpy()}, + output_names=['embs'])[0].squeeze() + + embeddings.append(batch_embs) + embeddings = np.vstack(embeddings) + + return embeddings + + window_fs = int(window_secs * sampling_rate) + period_fs = int(period_secs * sampling_rate) + + subsegs_list = [] + embeddings_list = [] + + utt_to_wav = read_scp(scp) + utt_to_segments = read_segments(segments) + + # encoder = init_encoder(source, device) + session = init_session(source, device) + + for utt in tqdm(utt_to_wav.keys()): + # Per utterance processing + wav = utt_to_wav[utt] + + if utt not in utt_to_segments: + continue + segments = utt_to_segments[utt] + + # Extract wav data using sliding window with overlap for each utterance + utt_subsegs, utt_subseg_signals = subsegment(wav, segments, window_fs, + period_fs) + # Convert a list of Tensor to a Tensor + utt_subseg_signals = torch.stack(utt_subseg_signals).squeeze(-1) + + # Extract embeddings for each subsegment-level wav data + utt_embeddings = extract_embeddings(utt_subseg_signals, batch_size) + + # Collect embeddings for each utterance + subsegs_list.append(utt_subsegs) + embeddings_list.append(utt_embeddings) + + return subsegs_list, embeddings_list + + +def cluster(embeddings, p=.01, num_spks=None, min_num_spks=1, max_num_spks=20): + # Define utility functions + def cosine_similarity(M): + M = M / np.linalg.norm(M, axis=1, keepdims=True) + return 0.5 * (1.0 + np.dot(M, M.T)) + + def prune(M, p): + m = M.shape[0] + if m < 1000: + n = max(m - 10, 2) + else: + n = int((1.0 - p) * m) + for i in range(m): + indexes = np.argsort(M[i, :]) + low_indexes, high_indexes = indexes[0:n], indexes[n:m] + M[i, low_indexes] = 0.0 + M[i, high_indexes] = 1.0 + return 0.5 * (M + M.T) + + def laplacian(M): + M[np.diag_indices(M.shape[0])] = 0.0 + D = np.diag(np.sum(np.abs(M), axis=1)) + return D - M + + def spectral(M, num_spks, min_num_spks, max_num_spks): + eig_values, eig_vectors = scipy.linalg.eigh(M) + num_spks = num_spks if num_spks is not None \ + else np.argmax(np.diff(eig_values[:max_num_spks + 1])) + 1 + num_spks = max(num_spks, min_num_spks) + return eig_vectors[:, :num_spks] + + def kmeans(data): + k = data.shape[1] + # centroids, labels = scipy.cluster.vq.kmeans2(data, k, minit='++') + _, labels, _ = k_means(data, k, random_state=None, n_init=10) + return labels + + # Fallback for trivial cases + if len(embeddings) <= 2: + return [0] * len(embeddings) + + # Compute similarity matrix + similarity_matrix = cosine_similarity(np.array(embeddings)) + # Prune matrix with p interval + pruned_similarity_matrix = prune(similarity_matrix, p) + # Compute Laplacian + laplacian_matrix = laplacian(pruned_similarity_matrix) + # Compute spectral embeddings + spectral_embeddings = spectral(laplacian_matrix, num_spks, min_num_spks, + max_num_spks) + # Assign class labels + labels = kmeans(spectral_embeddings) + + return labels + + +def main(): + args = get_args() + + print('Segmenting and extracting speaker embeddings') + subsegs_list, embeddings_list = compute_embeddings(args.scp, args.segments, + args.source, + args.device, + args.batch_size) + print('Embedding extraction finished') + print('Start Clustering') + + with cf.ProcessPoolExecutor() as executor, open(args.output, 'w') as f: + for (subsegs, labels) in zip(subsegs_list, + executor.map(cluster, embeddings_list)): + [ + print(subseg, label, file=f) + for (subseg, label) in zip(subsegs, labels) + ] + + +if __name__ == '__main__': + torch.set_num_threads(1) + # scipy.random.seed(1) + + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/diar/clusterer_gpu.py b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/diar/clusterer_gpu.py new file mode 100644 index 0000000000000000000000000000000000000000..34fc9ac46002d02c684ca235e5fd25ba1f932c66 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/diar/clusterer_gpu.py @@ -0,0 +1,143 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["OPENBLAS_NUM_THREADS"] = "1" +os.environ["MKL_NUM_THREADS"] = "1" +os.environ["VECLIB_MAXIMUM_THREADS"] = "1" +os.environ["NUMEXPR_NUM_THREADS"] = "1" + +import cupy as cp +from cuml.cluster import KMeans as cuKM +import numpy as np +from timeit import default_timer as timer +from clusterer import get_args, compute_embeddings +import torch + + +def cluster_gpu(embeddings, + p=.01, + num_spks=None, + min_num_spks=1, + max_num_spks=20): + # Define utility functions + def cosine_similarity(M): + M = M / cp.linalg.norm(M, axis=1, keepdims=True) + return 0.5 * (1.0 + cp.dot(M, M.T)) + + def prune(M, p): + m = M.shape[0] + if m < 1000: + n = max(m - 10, 2) + else: + n = int((1.0 - p) * m) + for i in range(m): + indexes = cp.argsort(M[i, :]) + low_indexes, high_indexes = indexes[0:n], indexes[n:m] + M[i, low_indexes] = 0.0 + M[i, high_indexes] = 1.0 + return 0.5 * (M + M.T) + + def laplacian(M): + M[cp.diag_indices(M.shape[0])] = 0.0 + D = cp.diag(cp.sum(cp.abs(M), axis=1)) + return D - M + + def spectral(M, num_spks, min_num_spks, max_num_spks): + eig_values, eig_vectors = cp.linalg.eigh(M) + num_spks = num_spks if num_spks is not None \ + else cp.argmax(cp.diff(eig_values[:max_num_spks + 1])) + 1 + num_spks = max(num_spks, min_num_spks) + return eig_vectors[:, :num_spks] + + def kmeans(data): + k = data.shape[1] + kmeans_float = cuKM(n_clusters=k, n_init=10) + kmeans_float.fit(cp.asarray(data)) + return kmeans_float.labels_ + + # Fallback for trivial cases + if len(embeddings) <= 2: + return [0] * len(embeddings) + + # How to specify the cuda device? + # with cp.cuda.Device(1): + # embeddings = cp.array(embeddings) + + # Compute similarity matrix + similarity_matrix = cosine_similarity(embeddings) + # Prune matrix with p interval + pruned_similarity_matrix = prune(similarity_matrix, p) + # Compute Laplacian + laplacian_matrix = laplacian(pruned_similarity_matrix) + # Compute spectral embeddings + spectral_embeddings = spectral(laplacian_matrix, num_spks, min_num_spks, + max_num_spks) + # Assign class labels + labels = kmeans(spectral_embeddings) + + return labels + + +def test_time(): + a = np.random.rand(1000, 256) + + def with_cuda(x, count): + for _ in range(count): + l = cluster_gpu(x) + return l + + for c in [1, 10, 100, 1000, 10000]: + print(c) + data = cp.asarray(a) + start = timer() + r = with_cuda(data, c) + cp.cuda.Device().synchronize() + elapsed_time = timer() - start + print("GPU Time: {}".format(elapsed_time)) + start = timer() + r = with_cpu(a, c) + elapsed_time = timer() - start + print("CPU Time: {}".format(elapsed_time)) + + +def main(): + args = get_args() + print('Segmenting and extracting speaker embeddings') + subsegs_list, embeddings_list = compute_embeddings(args.scp, args.segments, + args.source, + args.device, + args.batch_size) + print('Embedding extraction finished') + print('Start GPU Clustering') + + # Use the following part to do GPU Clustering + labels_list = [] + with open(args.output, 'w') as f: + for i in embeddings_list: + labels_list.append(cluster_gpu(cp.asarray(i))) + for (subsegs, labels) in zip(subsegs_list, labels_list): + [ + print(subseg, label, file=f) + for (subseg, label) in zip(subsegs, labels) + ] + + +if __name__ == '__main__': + # You can use the test_time() function + # to calculate the GPU vs CPU clustering speed + torch.set_num_threads(1) + + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..f60bd3ad463751141f92096c095653349e6cbc34 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v1/run.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# Copyright (c) 2022 Xu Xiang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +stage=-1 +stop_stage=-1 +sad_type="oracle" + +. tools/parse_options.sh + +# Prerequisite +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + mkdir -p external_tools + + # [1] Download evaluation toolkit + wget -c https://github.com/usnistgov/SCTK/archive/refs/tags/v2.4.12.zip -O external_tools/SCTK-v2.4.12.zip + unzip -o external_tools/SCTK-v2.4.12.zip -d external_tools + + # [2] Download voice activity detection model pretrained by Silero Team + #wget -c https://github.com/snakers4/silero-vad/archive/refs/tags/v3.1.zip -O external_tools/silero-vad-v3.1.zip + #unzip -o external_tools/silero-vad-v3.1.zip -d external_tools + + # [3] Download ResNet34 speaker model pretrained by WeSpeaker Team + mkdir -p pretrained_models + + wget -c https://wespeaker-1256283475.cos.ap-shanghai.myqcloud.com/models/voxceleb/voxceleb_resnet34_LM.onnx -O pretrained_models/voxceleb_resnet34_LM.onnx +fi + + +# Download VoxConverse dev audios and the corresponding annotations +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + mkdir -p data + + # Download annotations for dev and test sets + wget -c https://github.com/joonson/voxconverse/archive/refs/heads/master.zip -O data/voxconverse_master.zip + unzip -o data/voxconverse_master.zip -d data + + # Download dev audios + mkdir -p data/dev + wget -c https://mm.kaist.ac.kr/datasets/voxconverse/data/voxconverse_dev_wav.zip -O data/voxconverse_dev_wav.zip + unzip -o data/voxconverse_dev_wav.zip -d data/dev + + # Create wav.scp for dev audios + ls `pwd`/data/dev/audio/*.wav | awk -F/ '{print substr($NF, 1, length($NF)-4), $0}' > data/dev/wav.scp + + # Test audios + # mkdir -p data/test + # wget -c https://mm.kaist.ac.kr/datasets/voxconverse/data/voxconverse_test_wav.zip -O data/voxconverse_test_wav.zip + # unzip -o data/voxconverse_test_wav.zip -d data/test +fi + + +# Voice activity detection +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + # Set VAD min duration + min_duration=0.255 + + if [[ "x${sad_type}" == "xoracle" ]]; then + # Oracle SAD: handling overlapping or too short regions in ground truth RTTM + while read -r utt wav_path; do + python3 wespeaker/diar/make_oracle_sad.py \ + --rttm data/voxconverse-master/dev/${utt}.rttm \ + --min-duration $min_duration + done < data/dev/wav.scp > data/dev/oracle_sad + fi + + if [[ "x${sad_type}" == "xsystem" ]]; then + # System SAD: applying 'silero' VAD + python3 wespeaker/diar/make_system_sad.py \ + --scp data/dev/wav.scp \ + --min-duration $min_duration > data/dev/system_sad + fi +fi + + +# Applying spectral clustering algorithm (need a CUDA enabled GPU) +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + python3 diar/clusterer.py \ + --scp data/dev/wav.scp \ + --segments data/dev/${sad_type}_sad \ + --source pretrained_models/voxceleb_resnet34_LM.onnx \ + --device cuda \ + --output data/dev/${sad_type}_sad_labels +fi + + +# Convert labels to RTTMs +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + python3 wespeaker/diar/make_rttm.py \ + --labels data/dev/${sad_type}_sad_labels \ + --channel 1 > data/dev/${sad_type}_sad_rttm +fi + + +# Evaluate the result +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + perl external_tools/SCTK-2.4.12/src/md-eval/md-eval.pl \ + -c 0.25 \ + -r <(cat data/voxconverse-master/dev/*.rttm) \ + -s data/dev/${sad_type}_sad_rttm +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/README.md b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7a1d339a140eb0a066123e69185231eff2167af0 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/README.md @@ -0,0 +1,44 @@ +## Overview + +* We suggest to run this recipe on a gpu-available machine, with onnxruntime-gpu supported. +* Dataset: Voxconverse2020 (dev: 216 utts, test: 232 utts) +* Speaker model: ResNet34 model pretrained by WeSpeaker + * Refer to [voxceleb sv recipe](https://github.com/wenet-e2e/wespeaker/tree/master/examples/voxceleb/v2) + * [pretrained model path](https://wespeaker-1256283475.cos.ap-shanghai.myqcloud.com/models/voxceleb/voxceleb_resnet34_LM.onnx) +* Speaker activity detection model: + * oracle SAD (from ground truth annotation) + * system SAD (VAD model pretrained by [silero-vad](https://github.com/snakers4/silero-vad), v3.1 => v5.1) +* Clustering method: + * spectral clustering + * umap dimensionality reduction + hdbscan clustering +* Metric: DER = MISS + FALSE ALARM + SPEAKER CONFUSION (%) + +## Results + +* Dev set + + | system | MISS | FA | SC | DER | + |:---|:---:|:---:|:---:|:---:| + | Ours (oracle SAD + spectral clustering) | 2.3 | 0.0 | 2.1 | 4.4 | + | Ours (oracle SAD + umap clustering) | 2.3 | 0.0 | 1.3 | 3.6 | + | Ours (silero-vad v3.1 + spectral clustering) | 3.7 | 0.8 | 2.2 | 6.7 | + | Ours (silero-vad v5.1 + spectral clustering) | 3.4 | 0.6 | 2.3 | 6.3 | + | Ours (silero-vad v5.1 + umap clustering) | 3.4 | 0.6 | 1.4 | 5.4 | + | DIHARD 2019 baseline [^1] | 11.1 | 1.4 | 11.3 | 23.8 | + | DIHARD 2019 baseline w/ SE [^1] | 9.3 | 1.3 | 9.7 | 20.2 | + | (SyncNet ASD only) [^1] | 2.2 | 4.1 | 4.0 | 10.4 | + | (AVSE ASD only) [^1] | 2.0 | 5.9 | 4.6 | 12.4 | + | (proposed) [^1] | 2.4 | 2.3 | 3.0 | 7.7 | + +* Test set + + | system | MISS | FA | SC | DER | + |:---|:---:|:---:|:---:|:---:| + | Ours (oracle SAD + spectral clustering) | 1.6 | 0.0 | 3.3 | 4.9 | + | Ours (oracle SAD + umap clustering) | 1.6 | 0.0 | 1.9 | 3.5 | + | Ours (silero-vad v3.1 + spectral clustering) | 4.0 | 2.4 | 3.4 | 9.8 | + | Ours (silero-vad v5.1 + spectral clustering) | 3.8 | 1.7 | 3.3 | 8.8 | + | Ours (silero-vad v5.1 + umap clustering) | 3.8 | 1.7 | 1.8 | 7.3 | + + +[^1]: Spot the conversation: speaker diarisation in the wild, https://arxiv.org/pdf/2007.01216.pdf diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/local/extract_emb.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/local/extract_emb.sh new file mode 100644 index 0000000000000000000000000000000000000000..b12a1c0446cef91178de7af0716fec1249c50167 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/local/extract_emb.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Copyright (c) 2022 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +. ./path.sh || exit 1 + +scp='' +pretrained_model='' +device=cuda +store_dir='' +subseg_cmn=true +nj=1 + +batch_size=96 +frame_shift=10 +window_secs=1.5 +period_secs=0.75 + +. tools/parse_options.sh + +split_dir=$store_dir/split_scp +log_dir=$store_dir/log +mkdir -p $split_dir +mkdir -p $log_dir + +# split the scp file to sub_file, and we can use multi-process to extract embeddings +file_len=`wc -l $scp | awk '{print $1}'` +subfile_len=$[$file_len / $nj + 1] +prefix='split' +split -l $subfile_len -d -a 3 $scp ${split_dir}/${prefix}_scp_ + +for suffix in `seq 0 $[$nj-1]`;do + suffix=`printf '%03d' $suffix` + scp_subfile=${split_dir}/${prefix}_scp_${suffix} + write_ark=$store_dir/emb_${suffix}.ark + python3 wespeaker/diar/extract_emb.py \ + --scp ${scp_subfile} \ + --ark-path ${write_ark} \ + --source ${pretrained_model} \ + --device ${device} \ + --batch-size ${batch_size} \ + --frame-shift ${frame_shift} \ + --window-secs ${window_secs} \ + --period-secs ${period_secs} \ + --subseg-cmn ${subseg_cmn} \ + > ${log_dir}/${prefix}.${suffix}.log 2>&1 & +done + +wait + +cat $store_dir/emb_*.scp > $store_dir/emb.scp +echo "Finish extract embedding." diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/local/make_fbank.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/local/make_fbank.sh new file mode 100644 index 0000000000000000000000000000000000000000..3224c151c5a2a61d8b4286d8d95e326ef252d2df --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/local/make_fbank.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Copyright (c) 2022 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +. ./path.sh || exit 1 + +scp='' +segments='' +store_dir='' +subseg_cmn=true +nj=1 + +. tools/parse_options.sh + +split_dir=$store_dir/split_scp +log_dir=$store_dir/log +mkdir -p $split_dir +mkdir -p $log_dir + +# split the scp file to sub_file, and we can use multi-process to extract Fbank feature +file_len=`wc -l $scp | awk '{print $1}'` +subfile_len=$[$file_len / $nj + 1] +prefix='split' +split -l $subfile_len -d -a 3 $scp ${split_dir}/${prefix}_scp_ + +for suffix in `seq 0 $[$nj-1]`;do + suffix=`printf '%03d' $suffix` + scp_subfile=${split_dir}/${prefix}_scp_${suffix} + write_ark=$store_dir/fbank_${suffix}.ark + python3 wespeaker/diar/make_fbank.py \ + --scp ${scp_subfile} \ + --segments ${segments} \ + --ark-path ${write_ark} \ + --subseg-cmn ${subseg_cmn} \ + > ${log_dir}/${prefix}.${suffix}.log 2>&1 & +done + +wait + +cat $store_dir/fbank_*.scp > $store_dir/fbank.scp +echo "Finish make Fbank." diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/path.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/path.sh new file mode 100644 index 0000000000000000000000000000000000000000..b90a5154cf4a8f25b9a42851c59393f07dacb350 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/path.sh @@ -0,0 +1,5 @@ +export PATH=$PWD:$PATH + +# NOTE(kan-bayashi): Use UTF-8 in Python to avoid UnicodeDecodeError when LC_ALL=C +export PYTHONIOENCODING=UTF-8 +export PYTHONPATH=../../../:$PYTHONPATH diff --git a/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/run.sh b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..6c83171c8bb9f7dda968250292366fc6dc9de2b6 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/examples/voxconverse/v2/run.sh @@ -0,0 +1,188 @@ +#!/bin/bash +# Copyright (c) 2022-2023 Xu Xiang +# 2022 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2024 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +. ./path.sh || exit 1 + +stage=-1 +stop_stage=-1 +sad_type="oracle" # oracle/system +partition="dev" # dev/test +cluster_type="spectral" # spectral/umap + +# do cmn on the sub-segment or on the vad segment +subseg_cmn=true +# whether print the evaluation result for each file +get_each_file_res=1 + +. tools/parse_options.sh + +# Prerequisite +if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then + mkdir -p external_tools + + # [1] Download evaluation toolkit + wget -c https://github.com/usnistgov/SCTK/archive/refs/tags/v2.4.12.zip -O external_tools/SCTK-v2.4.12.zip + unzip -o external_tools/SCTK-v2.4.12.zip -d external_tools + + # [2] Download ResNet34 speaker model pretrained by WeSpeaker Team + mkdir -p pretrained_models + + wget -c https://wespeaker-1256283475.cos.ap-shanghai.myqcloud.com/models/voxceleb/voxceleb_resnet34_LM.onnx -O pretrained_models/voxceleb_resnet34_LM.onnx +fi + + +# Download VoxConverse dev/test audios and the corresponding annotations +if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then + mkdir -p data + + # Download annotations for dev and test sets (version 0.0.3) + wget -c https://github.com/joonson/voxconverse/archive/refs/heads/master.zip -O data/voxconverse_master.zip + unzip -o data/voxconverse_master.zip -d data + + # Download annotations from VoxSRC-23 validation toolkit (looks like version 0.0.2) + # cd data && git clone https://github.com/JaesungHuh/VoxSRC2023.git --recursive && cd - + + # Download dev audios + mkdir -p data/dev + + #wget --no-check-certificate -c https://mm.kaist.ac.kr/datasets/voxconverse/data/voxconverse_dev_wav.zip -O data/voxconverse_dev_wav.zip + # The above url may not be reachable, you can try the link below. + # This url is from https://github.com/joonson/voxconverse/blob/master/README.md + wget --no-check-certificate -c https://www.robots.ox.ac.uk/~vgg/data/voxconverse/data/voxconverse_dev_wav.zip -O data/voxconverse_dev_wav.zip + unzip -o data/voxconverse_dev_wav.zip -d data/dev + + # Create wav.scp for dev audios + ls `pwd`/data/dev/audio/*.wav | awk -F/ '{print substr($NF, 1, length($NF)-4), $0}' > data/dev/wav.scp + + # Test audios + mkdir -p data/test + + #wget --no-check-certificate -c https://mm.kaist.ac.kr/datasets/voxconverse/data/voxconverse_test_wav.zip -O data/voxconverse_test_wav.zip + # The above url may not be reachable, you can try the link below. + # This url is from https://github.com/joonson/voxconverse/blob/master/README.md + wget --no-check-certificate -c https://www.robots.ox.ac.uk/~vgg/data/voxconverse/data/voxconverse_test_wav.zip -O data/voxconverse_test_wav.zip + unzip -o data/voxconverse_test_wav.zip -d data/test + + # Create wav.scp for test audios + ls `pwd`/data/test/voxconverse_test_wav/*.wav | awk -F/ '{print substr($NF, 1, length($NF)-4), $0}' > data/test/wav.scp +fi + + +# Voice activity detection +if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then + # Set VAD min duration + min_duration=0.255 + + if [[ "x${sad_type}" == "xoracle" ]]; then + # Oracle SAD: handling overlapping or too short regions in ground truth RTTM + while read -r utt wav_path; do + python3 wespeaker/diar/make_oracle_sad.py \ + --rttm data/voxconverse-master/${partition}/${utt}.rttm \ + --min-duration $min_duration + done < data/${partition}/wav.scp > data/${partition}/oracle_sad + fi + + if [[ "x${sad_type}" == "xsystem" ]]; then + # System SAD: applying 'silero' VAD + python3 wespeaker/diar/make_system_sad.py \ + --scp data/${partition}/wav.scp \ + --min-duration $min_duration > data/${partition}/system_sad + fi +fi + + +# Extract fbank features +if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then + + [ -d "exp/${sad_type}_sad_fbank" ] && rm -r exp/${sad_type}_sad_fbank + + echo "Make Fbank features and store it under exp/${sad_type}_sad_fbank" + echo "..." + bash local/make_fbank.sh \ + --scp data/${partition}/wav.scp \ + --segments data/${partition}/${sad_type}_sad \ + --store_dir exp/${partition}_${sad_type}_sad_fbank \ + --subseg_cmn ${subseg_cmn} \ + --nj 24 +fi + +# Extract embeddings +if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then + + [ -d "exp/${sad_type}_sad_embedding" ] && rm -r exp/${sad_type}_sad_embedding + + echo "Extract embeddings and store it under exp/${sad_type}_sad_embedding" + echo "..." + bash local/extract_emb.sh \ + --scp exp/${partition}_${sad_type}_sad_fbank/fbank.scp \ + --pretrained_model pretrained_models/voxceleb_resnet34_LM.onnx \ + --device cuda \ + --store_dir exp/${partition}_${sad_type}_sad_embedding \ + --batch_size 96 \ + --frame_shift 10 \ + --window_secs 1.5 \ + --period_secs 0.75 \ + --subseg_cmn ${subseg_cmn} \ + --nj 1 +fi + + +# Applying spectral or ump+hdbscan clustering algorithm +if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then + + [ -f "exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_labels" ] && rm exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_labels + + echo "Doing ${cluster_type} clustering and store the result in exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_labels" + echo "..." + python3 wespeaker/diar/${cluster_type}_clusterer.py \ + --scp exp/${partition}_${sad_type}_sad_embedding/emb.scp \ + --output exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_labels +fi + + +# Convert labels to RTTMs +if [ ${stage} -le 7 ] && [ ${stop_stage} -ge 7 ]; then + python3 wespeaker/diar/make_rttm.py \ + --labels exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_labels \ + --channel 1 > exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_rttm +fi + + +# Evaluate the result +if [ ${stage} -le 8 ] && [ ${stop_stage} -ge 8 ]; then + ref_dir=data/voxconverse-master/ + #ref_dir=data/VoxSRC2023/voxconverse/ + echo -e "Get the DER results\n..." + perl external_tools/SCTK-2.4.12/src/md-eval/md-eval.pl \ + -c 0.25 \ + -r <(cat ${ref_dir}/${partition}/*.rttm) \ + -s exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_rttm 2>&1 | tee exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_res + + if [ ${get_each_file_res} -eq 1 ];then + single_file_res_dir=exp/${cluster_type}_cluster/${partition}_${sad_type}_single_file_res + mkdir -p $single_file_res_dir + echo -e "\nGet the DER results for each file and the results will be stored underd ${single_file_res_dir}\n..." + + awk '{print $2}' exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_rttm | sort -u | while read file_name; do + perl external_tools/SCTK-2.4.12/src/md-eval/md-eval.pl \ + -c 0.25 \ + -r <(cat ${ref_dir}/${partition}/${file_name}.rttm) \ + -s <(grep "${file_name}" exp/${cluster_type}_cluster/${partition}_${sad_type}_sad_rttm) > ${single_file_res_dir}/${partition}_${file_name}_res + done + echo "Done!" + fi +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/pyproject.toml b/MOSS_Speaker-RoPE/wespeaker/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..8b5760b582018ccfaf797820a5b9f204af18005a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=75.1.0,<=81", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/MOSS_Speaker-RoPE/wespeaker/requirements.txt b/MOSS_Speaker-RoPE/wespeaker/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..0587b966b560de6b181fb1731b512ea8a4d5f62d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/requirements.txt @@ -0,0 +1,32 @@ +setuptools<=81.0.0 +fire==0.4.0 +kaldiio==2.17.2 +numpy==1.22.4 +PyYAML==6.0 +scipy==1.10.0 +tableprint==0.9.1 +tqdm==4.66.3 +scikit-learn +matplotlib==3.5.1 +flake8==3.8.2 +flake8-bugbear +flake8-comprehensions +flake8-executable +flake8-pyi==20.5.0 +mccabe +h5py +pycodestyle==2.6.0 +pyflakes==2.2.0 +lmdb==1.3.0 +onnxruntime +soundfile +pypeln==0.4.9 +silero-vad +pre-commit==3.5.0 +s3prl +hdbscan>=0.8.40 +umap-learn==0.5.6 +importlib-metadata +openai-whisper +peft +accelerate diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/bin/CMakeLists.txt b/MOSS_Speaker-RoPE/wespeaker/runtime/core/bin/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..8d08b6d9187373dfff02e3e99a9fc6e864cccae3 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/bin/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(asv_main asv_main.cc) +target_link_libraries(asv_main PUBLIC speaker) + +add_executable(extract_emb_main extract_emb_main.cc) +target_link_libraries(extract_emb_main PUBLIC speaker) diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/bin/asv_main.cc b/MOSS_Speaker-RoPE/wespeaker/runtime/core/bin/asv_main.cc new file mode 100644 index 0000000000000000000000000000000000000000..f61a6ddb91caf7249ddeefeece16de8934c11476 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/bin/asv_main.cc @@ -0,0 +1,74 @@ +// Copyright (c) 2023 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "frontend/wav.h" +#include "gflags/gflags.h" +#include "speaker/speaker_engine.h" +#include "utils/timer.h" +#include "utils/utils.h" + +DEFINE_string(enroll_wav, "", "First wav as enroll wav."); +DEFINE_string(test_wav, "", "Second wav as test wav."); +DEFINE_double(threshold, 0.5, "Threshold"); + +DEFINE_string(speaker_model_path, "", "path of speaker model"); +DEFINE_int32(fbank_dim, 80, "fbank feature dimension"); +DEFINE_int32(sample_rate, 16000, "sample rate"); +DEFINE_int32(embedding_size, 256, "embedding size"); +DEFINE_int32(SamplesPerChunk, 32000, "samples of one chunk"); + +int main(int argc, char* argv[]) { + gflags::ParseCommandLineFlags(&argc, &argv, false); + google::InitGoogleLogging(argv[0]); + + // init model + LOG(INFO) << FLAGS_speaker_model_path; + LOG(INFO) << "Init model ..."; + auto speaker_engine = std::make_shared( + FLAGS_speaker_model_path, FLAGS_fbank_dim, FLAGS_sample_rate, + FLAGS_embedding_size, FLAGS_SamplesPerChunk); + int embedding_size = speaker_engine->EmbeddingSize(); + LOG(INFO) << "embedding size: " << embedding_size; + // read enroll wav/pcm data + auto data_reader = wenet::ReadAudioFile(FLAGS_enroll_wav); + int16_t* enroll_data = const_cast(data_reader->data()); + int enroll_samples = data_reader->num_sample(); + // NOTE(cdliang): memory allocation + std::vector enroll_embs(embedding_size, 0); + int enroll_wave_dur = static_cast(static_cast(enroll_samples) / + data_reader->sample_rate() * 1000); + LOG(INFO) << enroll_wave_dur; + speaker_engine->ExtractEmbedding(enroll_data, enroll_samples, &enroll_embs); + // test wav + auto test_data_reader = wenet::ReadAudioFile(FLAGS_test_wav); + int16_t* test_data = const_cast(test_data_reader->data()); + int test_samples = test_data_reader->num_sample(); + std::vector test_embs(embedding_size, 0); + int test_wave_dur = static_cast(static_cast(test_samples) / + test_data_reader->sample_rate() * 1000); + LOG(INFO) << test_wave_dur; + speaker_engine->ExtractEmbedding(test_data, test_samples, &test_embs); + float cosine_score; + LOG(INFO) << "compute score ..."; + cosine_score = speaker_engine->CosineSimilarity(enroll_embs, test_embs); + LOG(INFO) << "Cosine socre: " << cosine_score; + if (cosine_score >= FLAGS_threshold) { + LOG(INFO) << "It's the same speaker!"; + } else { + LOG(INFO) << "Warning! It's a different speaker."; + } + return 0; +} diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/bin/extract_emb_main.cc b/MOSS_Speaker-RoPE/wespeaker/runtime/core/bin/extract_emb_main.cc new file mode 100644 index 0000000000000000000000000000000000000000..62bc4a5e508f77d02d5d9a171f5d3b4e67cb8299 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/bin/extract_emb_main.cc @@ -0,0 +1,118 @@ +// Copyright (c) 2023 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include "frontend/wav.h" +#include "speaker/speaker_engine.h" +#include "utils/thread_pool.h" +#include "utils/timer.h" +#include "utils/utils.h" + +DEFINE_string(wav_scp, "", "input wav scp"); +DEFINE_string(wav_path, "", "input wav path"); +DEFINE_string(result, "", "output embedding file"); + +DEFINE_string(speaker_model_path, "", "path of speaker model"); +DEFINE_int32(fbank_dim, 80, "fbank feature dimension"); +DEFINE_int32(sample_rate, 16000, "sample rate"); +DEFINE_int32(embedding_size, 256, "embedding size"); +DEFINE_int32(samples_per_chunk, 32000, "samples of one chunk"); +DEFINE_int32(thread_num, 1, "num of extract_emb thread"); + +std::ofstream g_result; +std::mutex g_result_mutex; +int g_total_waves_dur = 0; +int g_total_extract_time = 0; + +void extract_emb(std::pair wav) { + // init model + auto speaker_engine = std::make_shared( + FLAGS_speaker_model_path, FLAGS_fbank_dim, FLAGS_sample_rate, + FLAGS_embedding_size, FLAGS_samples_per_chunk); + int embedding_size = speaker_engine->EmbeddingSize(); + LOG(INFO) << "embedding size: " << embedding_size; + // read wav.scp + wenet::WavReader wav_reader(wav.second); + CHECK_EQ(wav_reader.sample_rate(), 16000); + int16_t* data = const_cast(wav_reader.data()); + int samples = wav_reader.num_sample(); + // NOTE(cdliang): memory allocation + std::vector embs(FLAGS_embedding_size, 0); + + int wave_dur = static_cast(static_cast(samples) / + wav_reader.sample_rate() * 1000); + int extract_time = 0; + wenet::Timer timer; + speaker_engine->ExtractEmbedding(data, samples, &embs); + extract_time = timer.Elapsed(); + LOG(INFO) << "process: " << wav.first + << " RTF: " << static_cast(extract_time) / wave_dur; + g_result_mutex.lock(); + std::ostream& buffer = FLAGS_result.empty() ? std::cout : g_result; + buffer << wav.first; + for (size_t i = 0; i < embs.size(); i++) { + buffer << " " << embs[i]; + } + buffer << std::endl; + g_total_waves_dur += wave_dur; + g_total_extract_time += extract_time; + g_result_mutex.unlock(); +} + +int main(int argc, char* argv[]) { + gflags::ParseCommandLineFlags(&argc, &argv, false); + google::InitGoogleLogging(argv[0]); + + if (FLAGS_wav_scp.empty() && FLAGS_wav_path.empty()) { + LOG(FATAL) << "wav_scp and wav_path should not be empty at the same time"; + } + + std::vector> waves; + if (!FLAGS_wav_path.empty()) { + waves.emplace_back(make_pair("test", FLAGS_wav_path)); + } else { + std::ifstream wav_scp(FLAGS_wav_scp); + std::string line; + while (getline(wav_scp, line)) { + std::vector strs; + wespeaker::SplitString(line, &strs); + CHECK_EQ(strs.size(), 2); + waves.emplace_back(make_pair(strs[0], strs[1])); + } + if (waves.empty()) { + LOG(FATAL) << "Please provide non-empty wav scp."; + } + } + + if (!FLAGS_result.empty()) { + g_result.open(FLAGS_result, std::ios::out); + } + + { + ThreadPool pool(std::min(FLAGS_thread_num, static_cast(waves.size()))); + for (auto& wav : waves) { + pool.enqueue(extract_emb, wav); + } + } + LOG(INFO) << "Total: process " << g_total_waves_dur << "ms audio taken " + << g_total_extract_time << "ms."; + LOG(INFO) << "RTF: " << std::setprecision(4) + << static_cast(g_total_extract_time) / g_total_waves_dur; + return 0; +} diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/bpu.cmake b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/bpu.cmake new file mode 100644 index 0000000000000000000000000000000000000000..350d76c19d6f656fb130de09877d649cf49972a4 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/bpu.cmake @@ -0,0 +1,30 @@ +if(BPU) + if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") + set(EASY_DNN_URL "https://github.com/xingchensong/toolchain_pkg/releases/download/easy_dnn/easy_dnn.0.4.11.tar.gz") + set(URL_HASH "SHA256=a1a6f77d1baae7181d75ec5d37a2ee529ac4e1c4400babd6ceb1c007392a4904") + else() + message(FATAL_ERROR "Unsupported CMake System Processor '${CMAKE_SYSTEM_PROCESSOR}' (expected 'aarch64')") + endif() + else() + message(FATAL_ERROR "Unsupported CMake System Name '${CMAKE_SYSTEM_NAME}' (expected 'Linux')") + endif() + + FetchContent_Declare(easy_dnn + URL ${EASY_DNN_URL} + URL_HASH ${URL_HASH} + ) + FetchContent_MakeAvailable(easy_dnn) + include_directories(${easy_dnn_SOURCE_DIR}/easy_dnn/0.4.11_linux_aarch64-j3_hobot_gcc6.5.0/files/easy_dnn/include) + include_directories(${easy_dnn_SOURCE_DIR}/dnn/1.7.0_linux_aarch64-j3_hobot_gcc6.5.0/files/dnn/include) + include_directories(${easy_dnn_SOURCE_DIR}/hlog/0.4.7_linux_aarch64-j3_hobot_gcc6.5.0/files/hlog/include) + link_directories(${easy_dnn_SOURCE_DIR}/easy_dnn/0.4.11_linux_aarch64-j3_hobot_gcc6.5.0/files/easy_dnn/lib) + link_directories(${easy_dnn_SOURCE_DIR}/dnn/1.7.0_linux_aarch64-j3_hobot_gcc6.5.0/files/dnn/lib) + link_directories(${easy_dnn_SOURCE_DIR}/hlog/0.4.7_linux_aarch64-j3_hobot_gcc6.5.0/files/hlog/lib) + + add_definitions(-DUSE_BPU) + # NOTE(xcsong): Reasons for adding flag `-fuse-ld=gold`: + # https://stackoverflow.com/questions/59915966/unknown-gcc-linker-error-but-builds-sucessfully/59916438#59916438 + # https://github.com/tensorflow/tensorflow/issues/47849 + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fuse-ld=gold") +endif() diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/gflags.cmake b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/gflags.cmake new file mode 100644 index 0000000000000000000000000000000000000000..53ae5763b5a8c860b7e64d35b380eee5429f539d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/gflags.cmake @@ -0,0 +1,6 @@ +FetchContent_Declare(gflags + URL https://github.com/gflags/gflags/archive/v2.2.2.zip + URL_HASH SHA256=19713a36c9f32b33df59d1c79b4958434cb005b5b47dc5400a7a4b078111d9b5 +) +FetchContent_MakeAvailable(gflags) +include_directories(${gflags_BINARY_DIR}/include) \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/glog.cmake b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/glog.cmake new file mode 100644 index 0000000000000000000000000000000000000000..447ab4132f669ee2c3a52c37959dd684a39ff21b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/glog.cmake @@ -0,0 +1,6 @@ +FetchContent_Declare(glog + URL https://github.com/google/glog/archive/v0.4.0.zip + URL_HASH SHA256=9e1b54eb2782f53cd8af107ecf08d2ab64b8d0dc2b7f5594472f3bd63ca85cdc +) +FetchContent_MakeAvailable(glog) +include_directories(${glog_SOURCE_DIR}/src ${glog_BINARY_DIR}) \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/mnn.cmake b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/mnn.cmake new file mode 100644 index 0000000000000000000000000000000000000000..1d9fb5a8e593435628b906119b65a74581a5fad7 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/mnn.cmake @@ -0,0 +1,33 @@ +if(MNN) + set(MNN_URL "https://github.com/alibaba/MNN/archive/976d1d7c0f916ea8a7acc3d31352789590f00b18.zip") + set(URL_HASH "SHA256=7fcef0933992658e8725bdc1df2daff1410c8577c9c1ce838fd5d6c8c01d1ec1") + + FetchContent_Declare(mnn + URL ${MNN_URL} + URL_HASH ${URL_HASH} + ) + + set(MNN_BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) + set(MNN_BUILD_TOOLS OFF CACHE BOOL "" FORCE) + set(MNN_SUPPORT_DEPRECATED_OP OFF CACHE BOOL "" FORCE) + set(MNN_SEP_BUILD ON CACHE BOOL "" FORCE) + set(MNN_BUILD_MINI ${MINI_LIBS} CACHE BOOL "" FORCE) # mini version + set(MNN_JNI OFF CACHE BOOL "" FORCE) + set(MNN_USE_CPP11 ON CACHE BOOL "" FORCE) + set(MNN_SUPPORT_BF16 OFF CACHE BOOL "" FORCE) + set(MNN_BUILD_OPENCV OFF CACHE BOOL "" FORCE) + set(MNN_LOW_MEMORY OFF CACHE BOOL "" FORCE) + + FetchContent_GetProperties(mnn) + if(NOT mnn_POPULATED) + message(STATUS "Downloading mnn from ${MNN_URL}") + FetchContent_Populate(mnn) + endif() + + message(STATUS "mnn is downloaded to ${mnn_SOURCE_DIR}") + message(STATUS "mnn's binary dir is ${mnn_BINARY_DIR}") + add_subdirectory(${mnn_SOURCE_DIR} ${mnn_BINARY_DIR}) + include_directories(${mnn_SOURCE_DIR}/include) + link_directories(${mnn_BINARY_DIR}) + add_definitions(-DUSE_MNN) +endif() diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/onnx.cmake b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/onnx.cmake new file mode 100644 index 0000000000000000000000000000000000000000..ec2ebac72bfc6a4b2e04aa6a6afe8264ac367c5d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/cmake/onnx.cmake @@ -0,0 +1,43 @@ +if(ONNX) + set(ONNX_VERSION "1.12.0") + if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + set(ONNX_URL "https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-win-x64-${ONNX_VERSION}.zip") + set(URL_HASH "SHA256=8b5d61204989350b7904ac277f5fbccd3e6736ddbb6ec001e412723d71c9c176") + elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") + set(ONNX_URL "https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-linux-aarch64-${ONNX_VERSION}.tgz") + set(URL_HASH "SHA256=5820d9f343df73c63b6b2b174a1ff62575032e171c9564bcf92060f46827d0ac") + else() + if(GPU) + set(ONNX_URL "https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-linux-x64-gpu-${ONNX_VERSION}.tgz") + set(URL_HASH "SHA256=bc2e615314df0a871c560b7af6d4ce5896f351d23cad476562d2715208c9c7f7") + else() + set(ONNX_URL "https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-linux-x64-${ONNX_VERSION}.tgz") + set(URL_HASH "SHA256=5d503ce8540358b59be26c675e42081be14a3e833a5301926f555451046929c5") + endif() + endif() + elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") + set(ONNX_URL "https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-osx-arm64-${ONNX_VERSION}.tgz") + set(URL_HASH "SHA256=23117b6f5d7324d4a7c51184e5f808dd952aec411a6b99a1b6fd1011de06e300") + else() + set(ONNX_URL "https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-osx-x86_64-${ONNX_VERSION}.tgz") + set(URL_HASH "SHA256=09b17f712f8c6f19bb63da35d508815b443cbb473e16c6192abfaa297c02f600") + endif() + else() + message(FATAL_ERROR "Unsupported CMake System Name '${CMAKE_SYSTEM_NAME}' (expected 'Windows', 'Linux' or 'Darwin')") + endif() + + FetchContent_Declare(onnxruntime + URL ${ONNX_URL} + URL_HASH ${URL_HASH} + ) + FetchContent_MakeAvailable(onnxruntime) + include_directories(${onnxruntime_SOURCE_DIR}/include) + link_directories(${onnxruntime_SOURCE_DIR}/lib) + + add_definitions(-DUSE_ONNX) + if(GPU) + add_definitions(-DUSE_GPU) + endif() +endif() diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/CMakeLists.txt b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..bad571ad9c6a6fc4a46a06327c062e6fb31d9036 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/CMakeLists.txt @@ -0,0 +1,5 @@ +add_library(frontend STATIC + feature_pipeline.cc + fft.cc +) +target_link_libraries(frontend PUBLIC utils) diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/fbank.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/fbank.h new file mode 100644 index 0000000000000000000000000000000000000000..05ac5c6840fe4beb4fcbb37a8266553a072e2a54 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/fbank.h @@ -0,0 +1,222 @@ +// Copyright (c) 2017 Personal (Binbin Zhang) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FRONTEND_FBANK_H_ +#define FRONTEND_FBANK_H_ + +#include +#include +#include +#include +#include + +#include "frontend/fft.h" +#include "glog/logging.h" + +namespace wenet { + +// This code is based on kaldi Fbank implentation, please see +// https://github.com/kaldi-asr/kaldi/blob/master/src/feat/feature-fbank.cc +class Fbank { + public: + Fbank(int num_bins, int sample_rate, int frame_length, int frame_shift) + : num_bins_(num_bins), + sample_rate_(sample_rate), + frame_length_(frame_length), + frame_shift_(frame_shift), + use_log_(true), + remove_dc_offset_(true), + generator_(0), + distribution_(0, 1.0), + dither_(0.0) { + fft_points_ = UpperPowerOfTwo(frame_length_); + // generate bit reversal table and trigonometric function table + const int fft_points_4 = fft_points_ / 4; + bitrev_.resize(fft_points_); + sintbl_.resize(fft_points_ + fft_points_4); + make_sintbl(fft_points_, sintbl_.data()); + make_bitrev(fft_points_, bitrev_.data()); + + int num_fft_bins = fft_points_ / 2; + float fft_bin_width = static_cast(sample_rate_) / fft_points_; + int low_freq = 20, high_freq = sample_rate_ / 2; + float mel_low_freq = MelScale(low_freq); + float mel_high_freq = MelScale(high_freq); + float mel_freq_delta = (mel_high_freq - mel_low_freq) / (num_bins + 1); + bins_.resize(num_bins_); + center_freqs_.resize(num_bins_); + for (int bin = 0; bin < num_bins; ++bin) { + float left_mel = mel_low_freq + bin * mel_freq_delta, + center_mel = mel_low_freq + (bin + 1) * mel_freq_delta, + right_mel = mel_low_freq + (bin + 2) * mel_freq_delta; + center_freqs_[bin] = InverseMelScale(center_mel); + std::vector this_bin(num_fft_bins); + int first_index = -1, last_index = -1; + for (int i = 0; i < num_fft_bins; ++i) { + float freq = (fft_bin_width * i); // Center frequency of this fft + // bin. + float mel = MelScale(freq); + if (mel > left_mel && mel < right_mel) { + float weight; + if (mel <= center_mel) + weight = (mel - left_mel) / (center_mel - left_mel); + else + weight = (right_mel - mel) / (right_mel - center_mel); + this_bin[i] = weight; + if (first_index == -1) first_index = i; + last_index = i; + } + } + CHECK(first_index != -1 && last_index >= first_index); + bins_[bin].first = first_index; + int size = last_index + 1 - first_index; + bins_[bin].second.resize(size); + for (int i = 0; i < size; ++i) { + bins_[bin].second[i] = this_bin[first_index + i]; + } + } + + // NOTE(cdliang): add hamming window + hamming_window_.resize(frame_length_); + double a = M_2PI / (frame_length - 1); + for (int i = 0; i < frame_length; i++) { + double i_fl = static_cast(i); + hamming_window_[i] = 0.54 - 0.46 * cos(a * i_fl); + } + } + + void set_use_log(bool use_log) { use_log_ = use_log; } + + void set_remove_dc_offset(bool remove_dc_offset) { + remove_dc_offset_ = remove_dc_offset; + } + + void set_dither(float dither) { dither_ = dither; } + + int num_bins() const { return num_bins_; } + + static inline float InverseMelScale(float mel_freq) { + return 700.0f * (expf(mel_freq / 1127.0f) - 1.0f); + } + + static inline float MelScale(float freq) { + return 1127.0f * logf(1.0f + freq / 700.0f); + } + + static int UpperPowerOfTwo(int n) { + return static_cast(pow(2, ceil(log(n) / log(2)))); + } + + // preemphasis + void PreEmphasis(float coeff, std::vector* data) const { + if (coeff == 0.0) return; + for (int i = data->size() - 1; i > 0; i--) + (*data)[i] -= coeff * (*data)[i - 1]; + (*data)[0] -= coeff * (*data)[0]; + } + + // add hamming window + void Hamming(std::vector* data) const { + CHECK_GE(data->size(), hamming_window_.size()); + for (size_t i = 0; i < hamming_window_.size(); ++i) { + (*data)[i] *= hamming_window_[i]; + } + } + + // Compute fbank feat, return num frames + int Compute(const std::vector& wave, + std::vector>* feat) { + int num_samples = wave.size(); + if (num_samples < frame_length_) return 0; + int num_frames = 1 + ((num_samples - frame_length_) / frame_shift_); + feat->resize(num_frames); + std::vector fft_real(fft_points_, 0), fft_img(fft_points_, 0); + std::vector power(fft_points_ / 2); + for (int i = 0; i < num_frames; ++i) { + std::vector data(wave.data() + i * frame_shift_, + wave.data() + i * frame_shift_ + frame_length_); + // optional add noise + if (dither_ != 0.0) { + for (size_t j = 0; j < data.size(); ++j) + data[j] += dither_ * distribution_(generator_); + } + // optinal remove dc offset + if (remove_dc_offset_) { + float mean = 0.0; + for (size_t j = 0; j < data.size(); ++j) mean += data[j]; + mean /= data.size(); + for (size_t j = 0; j < data.size(); ++j) data[j] -= mean; + } + + PreEmphasis(0.97, &data); + // Povey(&data); + Hamming(&data); + // copy data to fft_real + memset(fft_img.data(), 0, sizeof(float) * fft_points_); + memset(fft_real.data() + frame_length_, 0, + sizeof(float) * (fft_points_ - frame_length_)); + memcpy(fft_real.data(), data.data(), sizeof(float) * frame_length_); + fft(bitrev_.data(), sintbl_.data(), fft_real.data(), fft_img.data(), + fft_points_); + // power + for (int j = 0; j < fft_points_ / 2; ++j) { + power[j] = fft_real[j] * fft_real[j] + fft_img[j] * fft_img[j]; + } + + (*feat)[i].resize(num_bins_); + // cepstral coefficients, triangle filter array + for (int j = 0; j < num_bins_; ++j) { + float mel_energy = 0.0; + int s = bins_[j].first; + for (size_t k = 0; k < bins_[j].second.size(); ++k) { + mel_energy += bins_[j].second[k] * power[s + k]; + } + // optional use log + if (use_log_) { + if (mel_energy < std::numeric_limits::epsilon()) + mel_energy = std::numeric_limits::epsilon(); + mel_energy = logf(mel_energy); + } + + (*feat)[i][j] = mel_energy; + // printf("%f ", mel_energy); + } + // printf("\n"); + } + return num_frames; + } + + private: + int num_bins_; + int sample_rate_; + int frame_length_, frame_shift_; + int fft_points_; + bool use_log_; + bool remove_dc_offset_; + std::vector center_freqs_; + std::vector>> bins_; + std::vector hamming_window_; + std::default_random_engine generator_; + std::normal_distribution distribution_; + float dither_; + + // bit reversal table + std::vector bitrev_; + // trigonometric function table + std::vector sintbl_; +}; + +} // namespace wenet + +#endif // FRONTEND_FBANK_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/feature_pipeline.cc b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/feature_pipeline.cc new file mode 100644 index 0000000000000000000000000000000000000000..c59d6f7351603b392de68b5d8e9ba2079735ee65 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/feature_pipeline.cc @@ -0,0 +1,113 @@ +// Copyright (c) 2017 Personal (Binbin Zhang) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "frontend/feature_pipeline.h" + +#include +#include + +namespace wenet { + +FeaturePipeline::FeaturePipeline(const FeaturePipelineConfig& config) + : config_(config), + feature_dim_(config.num_bins), + fbank_(config.num_bins, config.sample_rate, config.frame_length, + config.frame_shift), + num_frames_(0), + input_finished_(false) {} + +void FeaturePipeline::AcceptWaveform(const std::vector& wav) { + std::vector> feats; + std::vector waves; + waves.insert(waves.end(), remained_wav_.begin(), remained_wav_.end()); + waves.insert(waves.end(), wav.begin(), wav.end()); + int num_frames = fbank_.Compute(waves, &feats); + for (size_t i = 0; i < feats.size(); ++i) { + feature_queue_.Push(std::move(feats[i])); + } + num_frames_ += num_frames; + + int left_samples = waves.size() - config_.frame_shift * num_frames; + remained_wav_.resize(left_samples); + std::copy(waves.begin() + config_.frame_shift * num_frames, waves.end(), + remained_wav_.begin()); + // We are still adding wave, notify input is not finished + finish_condition_.notify_one(); +} + +void FeaturePipeline::AcceptWaveform(const std::vector& wav) { + std::vector float_wav(wav.size()); + for (size_t i = 0; i < wav.size(); i++) { + float_wav[i] = static_cast(wav[i]); + } + this->AcceptWaveform(float_wav); +} + +void FeaturePipeline::set_input_finished() { + CHECK(!input_finished_); + { + std::lock_guard lock(mutex_); + input_finished_ = true; + } + finish_condition_.notify_one(); +} + +bool FeaturePipeline::ReadOne(std::vector* feat) { + if (!feature_queue_.Empty()) { + *feat = std::move(feature_queue_.Pop()); + return true; + } else { + std::unique_lock lock(mutex_); + while (!input_finished_) { + // This will release the lock and wait for notify_one() + // from AcceptWaveform() or set_input_finished() + finish_condition_.wait(lock); + if (!feature_queue_.Empty()) { + *feat = std::move(feature_queue_.Pop()); + return true; + } + } + CHECK(input_finished_); + // Double check queue.empty, see issue#893 for detailed discussions. + if (!feature_queue_.Empty()) { + *feat = std::move(feature_queue_.Pop()); + return true; + } else { + return false; + } + } +} + +bool FeaturePipeline::Read(int num_frames, + std::vector>* feats) { + feats->clear(); + std::vector feat; + while (feats->size() < num_frames) { + if (ReadOne(&feat)) { + feats->push_back(std::move(feat)); + } else { + return false; + } + } + return true; +} + +void FeaturePipeline::Reset() { + input_finished_ = false; + num_frames_ = 0; + remained_wav_.clear(); + feature_queue_.Clear(); +} + +} // namespace wenet diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/feature_pipeline.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/feature_pipeline.h new file mode 100644 index 0000000000000000000000000000000000000000..a020c2afa92d667952bca63a53c0ae12564e6c40 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/feature_pipeline.h @@ -0,0 +1,118 @@ +// Copyright (c) 2017 Personal (Binbin Zhang) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FRONTEND_FEATURE_PIPELINE_H_ +#define FRONTEND_FEATURE_PIPELINE_H_ + +#include +#include +#include +#include + +#include "frontend/fbank.h" +#include "glog/logging.h" +#include "utils/blocking_queue.h" + +namespace wenet { + +struct FeaturePipelineConfig { + int num_bins; + int sample_rate; + int frame_length; + int frame_shift; + FeaturePipelineConfig(int num_bins, int sample_rate) + : num_bins(num_bins), // 80 dim fbank + sample_rate(sample_rate) { // 16k sample rate + frame_length = sample_rate / 1000 * 25; // frame length 25ms + frame_shift = sample_rate / 1000 * 10; // frame shift 10ms + } + + void Info() const { + LOG(INFO) << "feature pipeline config" + << " num_bins " << num_bins << " frame_length " << frame_length + << " frame_shift " << frame_shift; + } +}; + +// Typically, FeaturePipeline is used in two threads: one thread A calls +// AcceptWaveform() to add raw wav data and set_input_finished() to notice +// the end of input wav, another thread B (decoder thread) calls Read() to +// consume features.So a BlockingQueue is used to make this class thread safe. + +// The Read() is designed as a blocking method when there is no feature +// in feature_queue_ and the input is not finished. + +class FeaturePipeline { + public: + explicit FeaturePipeline(const FeaturePipelineConfig& config); + + // The feature extraction is done in AcceptWaveform(). + void AcceptWaveform(const std::vector& wav); + void AcceptWaveform(const std::vector& wav); + + // Current extracted frames number. + int num_frames() const { return num_frames_; } + int feature_dim() const { return feature_dim_; } + const FeaturePipelineConfig& config() const { return config_; } + + // The caller should call this method when speech input is end. + // Never call AcceptWaveform() after calling set_input_finished() ! + void set_input_finished(); + bool input_finished() const { return input_finished_; } + + // Return False if input is finished and no feature could be read. + // Return True if a feature is read. + // This function is a blocking method. It will block the thread when + // there is no feature in feature_queue_ and the input is not finished. + bool ReadOne(std::vector* feat); + + // Read #num_frames frame features. + // Return False if less then #num_frames features are read and the + // input is finished. + // Return True if #num_frames features are read. + // This function is a blocking method when there is no feature + // in feature_queue_ and the input is not finished. + bool Read(int num_frames, std::vector>* feats); + + void Reset(); + bool IsLastFrame(int frame) const { + return input_finished_ && (frame == num_frames_ - 1); + } + + int NumQueuedFrames() const { return feature_queue_.Size(); } + + private: + const FeaturePipelineConfig& config_; + int feature_dim_; + Fbank fbank_; + + BlockingQueue> feature_queue_; + int num_frames_; + bool input_finished_; + + // The feature extraction is done in AcceptWaveform(). + // This wavefrom sample points are consumed by frame size. + // The residual wavefrom sample points after framing are + // kept to be used in next AcceptWaveform() calling. + std::vector remained_wav_; + + // Used to block the Read when there is no feature in feature_queue_ + // and the input is not finished. + mutable std::mutex mutex_; + std::condition_variable finish_condition_; +}; + +} // namespace wenet + +#endif // FRONTEND_FEATURE_PIPELINE_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/fft.cc b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/fft.cc new file mode 100644 index 0000000000000000000000000000000000000000..d293203ed2b470f6733d32debb81dfef0b9bdc6f --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/fft.cc @@ -0,0 +1,121 @@ +// Copyright (c) 2016 HR + +#include +#include +#include + +#include "frontend/fft.h" + +namespace wenet { + +void make_sintbl(int n, float* sintbl) { + int i, n2, n4, n8; + float c, s, dc, ds, t; + + n2 = n / 2; + n4 = n / 4; + n8 = n / 8; + t = sin(M_PI / n); + dc = 2 * t * t; + ds = sqrt(dc * (2 - dc)); + t = 2 * dc; + c = sintbl[n4] = 1; + s = sintbl[0] = 0; + for (i = 1; i < n8; ++i) { + c -= dc; + dc += t * c; + s += ds; + ds -= t * s; + sintbl[i] = s; + sintbl[n4 - i] = c; + } + if (n8 != 0) sintbl[n8] = sqrt(0.5); + for (i = 0; i < n4; ++i) sintbl[n2 - i] = sintbl[i]; + for (i = 0; i < n2 + n4; ++i) sintbl[i + n2] = -sintbl[i]; +} + +void make_bitrev(int n, int* bitrev) { + int i, j, k, n2; + + n2 = n / 2; + i = j = 0; + for (;;) { + bitrev[i] = j; + if (++i >= n) break; + k = n2; + while (k <= j) { + j -= k; + k /= 2; + } + j += k; + } +} + +// bitrev: bit reversal table +// sintbl: trigonometric function table +// x:real part +// y:image part +// n: fft length +int fft(const int* bitrev, const float* sintbl, float* x, float* y, int n) { + int i, j, k, ik, h, d, k2, n4, inverse; + float t, s, c, dx, dy; + + /* preparation */ + if (n < 0) { + n = -n; + inverse = 1; /* inverse transform */ + } else { + inverse = 0; + } + n4 = n / 4; + if (n == 0) { + return 0; + } + + /* bit reversal */ + for (i = 0; i < n; ++i) { + j = bitrev[i]; + if (i < j) { + t = x[i]; + x[i] = x[j]; + x[j] = t; + t = y[i]; + y[i] = y[j]; + y[j] = t; + } + } + + /* transformation */ + for (k = 1; k < n; k = k2) { + h = 0; + k2 = k + k; + d = n / k2; + for (j = 0; j < k; ++j) { + c = sintbl[h + n4]; + if (inverse) + s = -sintbl[h]; + else + s = sintbl[h]; + for (i = j; i < n; i += k2) { + ik = i + k; + dx = s * y[ik] + c * x[ik]; + dy = c * y[ik] - s * x[ik]; + x[ik] = x[i] - dx; + x[i] += dx; + y[ik] = y[i] - dy; + y[i] += dy; + } + h += d; + } + } + if (inverse) { + /* divide by n in case of the inverse transformation */ + for (i = 0; i < n; ++i) { + x[i] /= n; + y[i] /= n; + } + } + return 0; /* finished successfully */ +} + +} // namespace wenet diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/fft.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/fft.h new file mode 100644 index 0000000000000000000000000000000000000000..501531115d492f4c0436bbc451a9e382fb45679a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/fft.h @@ -0,0 +1,25 @@ +// Copyright (c) 2016 HR + +#ifndef FRONTEND_FFT_H_ +#define FRONTEND_FFT_H_ + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif +#ifndef M_2PI +#define M_2PI 6.283185307179586476925286766559005 +#endif + +namespace wenet { + +// Fast Fourier Transform + +void make_sintbl(int n, float* sintbl); + +void make_bitrev(int n, int* bitrev); + +int fft(const int* bitrev, const float* sintbl, float* x, float* y, int n); + +} // namespace wenet + +#endif // FRONTEND_FFT_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/wav.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/wav.h new file mode 100644 index 0000000000000000000000000000000000000000..ac0fa607bfd4fbf28c5df423eeec84c6deaa4ebe --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/frontend/wav.h @@ -0,0 +1,250 @@ +// Copyright (c) 2016 Personal (Binbin Zhang) +// Created on 2016-08-15 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FRONTEND_WAV_H_ +#define FRONTEND_WAV_H_ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "gflags/gflags.h" +#include "glog/logging.h" + +DEFINE_int32(pcm_sample_rate, 16000, "pcm data sample rate"); + +namespace wenet { + +class AudioReader { + public: + AudioReader() {} + explicit AudioReader(const std::string& filename) {} + virtual ~AudioReader() {} + + virtual int num_channel() const = 0; + virtual int sample_rate() const = 0; + virtual int bits_per_sample() const = 0; + virtual int num_sample() const = 0; + virtual const int16_t* data() const = 0; +}; + +struct WavHeader { + char riff[4]; // "riff" + unsigned int size; + char wav[4]; // "WAVE" + char fmt[4]; // "fmt " + unsigned int fmt_size; + uint16_t format; + uint16_t channels; + unsigned int sample_rate; + unsigned int bytes_per_second; + uint16_t block_size; + uint16_t bit; + char data[4]; // "data" + unsigned int data_size; +}; + +class WavReader : public AudioReader { + public: + WavReader() {} + explicit WavReader(const std::string& filename) { Open(filename); } + + bool Open(const std::string& filename) { + FILE* fp = fopen(filename.c_str(), "rb"); + if (NULL == fp) { + LOG(WARNING) << "Error in read " << filename; + return false; + } + + WavHeader header; + fread(&header, 1, sizeof(header), fp); + if (header.fmt_size < 16) { + fprintf(stderr, + "WaveData: expect PCM format data " + "to have fmt chunk of at least size 16.\n"); + return false; + } else if (header.fmt_size > 16) { + int offset = 44 - 8 + header.fmt_size - 16; + fseek(fp, offset, SEEK_SET); + fread(header.data, 8, sizeof(char), fp); + } + // check "riff" "WAVE" "fmt " "data" + + // Skip any subchunks between "fmt" and "data". Usually there will + // be a single "fact" subchunk, but on Windows there can also be a + // "list" subchunk. + while (0 != strncmp(header.data, "data", 4)) { + // We will just ignore the data in these chunks. + fseek(fp, header.data_size, SEEK_CUR); + // read next subchunk + fread(header.data, 8, sizeof(char), fp); + } + + num_channel_ = header.channels; + sample_rate_ = header.sample_rate; + bits_per_sample_ = header.bit; + int num_data = header.data_size / (bits_per_sample_ / 8); + data_.resize(num_data); + int num_read = fread(&data_[0], 1, header.data_size, fp); + if (num_read < header.data_size) { + // If the header size is wrong, adjust + header.data_size = num_read; + num_data = header.data_size / (bits_per_sample_ / 8); + data_.resize(num_data); + } + num_sample_ = num_data / num_channel_; + fclose(fp); + return true; + } + + int num_channel() const { return num_channel_; } + int sample_rate() const { return sample_rate_; } + int bits_per_sample() const { return bits_per_sample_; } + int num_sample() const { return num_sample_; } + const int16_t* data() const { return data_.data(); } + + private: + int num_channel_; + int sample_rate_; + int bits_per_sample_; + int num_sample_; // sample points per channel + std::vector data_; +}; + +class WavWriter { + public: + WavWriter(const float* data, int num_sample, int num_channel, int sample_rate, + int bits_per_sample) + : data_(data), + num_sample_(num_sample), + num_channel_(num_channel), + sample_rate_(sample_rate), + bits_per_sample_(bits_per_sample) {} + + void Write(const std::string& filename) { + FILE* fp = fopen(filename.c_str(), "w"); + // init char 'riff' 'WAVE' 'fmt ' 'data' + WavHeader header; + char wav_header[44] = {0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, + 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00}; + memcpy(&header, wav_header, sizeof(header)); + header.channels = num_channel_; + header.bit = bits_per_sample_; + header.sample_rate = sample_rate_; + header.data_size = num_sample_ * num_channel_ * (bits_per_sample_ / 8); + header.size = sizeof(header) - 8 + header.data_size; + header.bytes_per_second = + sample_rate_ * num_channel_ * (bits_per_sample_ / 8); + header.block_size = num_channel_ * (bits_per_sample_ / 8); + + fwrite(&header, 1, sizeof(header), fp); + + for (int i = 0; i < num_sample_; ++i) { + for (int j = 0; j < num_channel_; ++j) { + switch (bits_per_sample_) { + case 8: { + char sample = static_cast(data_[i * num_channel_ + j]); + fwrite(&sample, 1, sizeof(sample), fp); + break; + } + case 16: { + int16_t sample = static_cast(data_[i * num_channel_ + j]); + fwrite(&sample, 1, sizeof(sample), fp); + break; + } + case 32: { + int sample = static_cast(data_[i * num_channel_ + j]); + fwrite(&sample, 1, sizeof(sample), fp); + break; + } + } + } + } + fclose(fp); + } + + private: + const float* data_; + int num_sample_; // total float points in data_ + int num_channel_; + int sample_rate_; + int bits_per_sample_; +}; + +class PcmReader : public AudioReader { + public: + PcmReader() {} + explicit PcmReader(const std::string& filename) { Open(filename); } + + bool Open(const std::string& filename) { + FILE* fp = fopen(filename.c_str(), "rb"); + if (NULL == fp) { + LOG(WARNING) << "Error in read " << filename; + return false; + } + + num_channel_ = 1; + sample_rate_ = FLAGS_pcm_sample_rate; + bits_per_sample_ = 16; + fseek(fp, 0, SEEK_END); + int data_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + // If data_size is odd, apply for (data_size + 1) bytes of space. + // If data_size is even, apply for data_size bytes of space. + num_sample_ = ceil(1.0 * data_size / sizeof(int16_t)); + data_.resize(num_sample_); + fread(&data_[0], data_size, 1, fp); + fclose(fp); + return true; + } + + int num_channel() const { return num_channel_; } + int sample_rate() const { return sample_rate_; } + int bits_per_sample() const { return bits_per_sample_; } + int num_sample() const { return num_sample_; } + + const int16_t* data() const { return data_.data(); } + + private: + int num_channel_; + int sample_rate_; + int bits_per_sample_; + int num_sample_; // sample points per channel + std::vector data_; +}; + +std::shared_ptr ReadAudioFile(const std::string& filename) { + size_t pos = filename.rfind('.'); + std::string suffix = filename.substr(pos); + if (suffix == ".wav" || suffix == ".WAV") { + return std::make_shared(filename); + } else { + return std::make_shared(filename); + } +} + +} // namespace wenet + +#endif // FRONTEND_WAV_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/CMakeLists.txt b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..576902fb3455813902309df64728b26b24ad2545 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/CMakeLists.txt @@ -0,0 +1,23 @@ +set(speaker_srcs + speaker_engine.cc) + +if(NOT ONNX AND NOT MNN) + message(FATAL_ERROR "Please build with ONNX or MNN!") +endif() +if(ONNX) + list(APPEND speaker_srcs onnx_speaker_model.cc) +endif() +if(MNN) + list(APPEND speaker_srcs mnn_speaker_model.cc) +endif() + +add_library(speaker STATIC ${speaker_srcs}) +target_link_libraries(speaker PUBLIC frontend) + +if(ONNX) + target_link_libraries(speaker PUBLIC onnxruntime) +endif() +if(MNN) + target_link_libraries(speaker PUBLIC MNN) +endif() + diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/mnn_speaker_model.cc b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/mnn_speaker_model.cc new file mode 100644 index 0000000000000000000000000000000000000000..1cd33bab7ad3d6281a12add31a4576654da1f137 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/mnn_speaker_model.cc @@ -0,0 +1,102 @@ +// Copyright (c) 2024 Chengdong Liang (liangchengdongd@qq.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifdef USE_MNN + +#include + +#include "glog/logging.h" +#include "speaker/mnn_speaker_model.h" +#include "utils/utils.h" + +namespace wespeaker { + +MnnSpeakerModel::MnnSpeakerModel(const std::string& model_path, + int num_threads) { + // 1. Load sessions + speaker_interpreter_ = std::shared_ptr( + MNN::Interpreter::createFromFile(model_path.c_str())); + + MNN::ScheduleConfig config; + config.type = MNN_FORWARD_CPU; + config.numThread = num_threads; + MNN::BackendConfig backend_config; + backend_config.precision = MNN::BackendConfig::Precision_Low; + backend_config.power = MNN::BackendConfig::Power_High; + config.backendConfig = &backend_config; + + speaker_session_ = speaker_interpreter_->createSession(config); + if (!speaker_session_) { + LOG(ERROR) << "[MNN] Create session failed!"; + return; + } +} + +MnnSpeakerModel::~MnnSpeakerModel() { + if (speaker_session_) { + speaker_interpreter_->releaseModel(); + speaker_interpreter_->releaseSession(speaker_session_); + } +} + +void MnnSpeakerModel::ExtractEmbedding( + const std::vector>& feats, std::vector* embed) { + unsigned int num_frames = feats.size(); + unsigned int feat_dim = feats[0].size(); + + // 1. input tensor + auto input_tensor = + speaker_interpreter_->getSessionInput(speaker_session_, nullptr); + + auto shape = input_tensor->shape(); + CHECK_EQ(shape.size(), 3); + if (shape[0] == -1 || shape[1] == -1 || shape[2] == -1) { + VLOG(2) << "dynamic shape."; + std::vector input_dims = {1, static_cast(num_frames), + static_cast(feat_dim)}; + speaker_interpreter_->resizeTensor(input_tensor, input_dims); + speaker_interpreter_->resizeSession(speaker_session_); + } else { + if (shape[0] != 1 || shape[1] != num_frames || shape[2] != feat_dim) { + LOG(ERROR) << "shape error!"; + return; + } + } + + std::shared_ptr nchw_tensor( + new MNN::Tensor(input_tensor, MNN::Tensor::CAFFE)); // NCHW + for (size_t i = 0; i < num_frames; ++i) { + for (size_t j = 0; j < feat_dim; ++j) { + nchw_tensor->host()[i * feat_dim + j] = feats[i][j]; + } + } + input_tensor->copyFromHostTensor(nchw_tensor.get()); + + // 2. run session + speaker_interpreter_->runSession(speaker_session_); + + // 3. output + auto output = speaker_interpreter_->getSessionOutput(speaker_session_, NULL); + std::shared_ptr output_tensor( + new MNN::Tensor(output, MNN::Tensor::CAFFE)); + output->copyToHostTensor(output_tensor.get()); + embed->reserve(output_tensor->elementSize()); + for (int i = 0; i < output_tensor->elementSize(); ++i) { + embed->push_back(output->host()[i]); + } +} + +} // namespace wespeaker + +#endif // USE_MNN diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/mnn_speaker_model.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/mnn_speaker_model.h new file mode 100644 index 0000000000000000000000000000000000000000..d64d94b0acdf11ed9fd6a560a0c67ffd4079d720 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/mnn_speaker_model.h @@ -0,0 +1,49 @@ +// Copyright (c) 2024 Chengdong Liang (liangchengdongd@qq.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SPEAKER_MNN_SPEAKER_MODEL_H_ +#define SPEAKER_MNN_SPEAKER_MODEL_H_ + +#ifdef USE_MNN + +#include +#include +#include +#include + +#include "MNN/Interpreter.hpp" +#include "MNN/MNNDefine.h" +#include "MNN/Tensor.hpp" +#include "speaker/speaker_model.h" + +namespace wespeaker { + +class MnnSpeakerModel : public SpeakerModel { + public: + explicit MnnSpeakerModel(const std::string& model_path, int num_threads); + + void ExtractEmbedding(const std::vector>& feats, + std::vector* embed) override; + ~MnnSpeakerModel(); + + private: + // session + std::shared_ptr speaker_interpreter_; + MNN::Session* speaker_session_ = nullptr; +}; + +} // namespace wespeaker + +#endif // USE_MNN +#endif // SPEAKER_MNN_SPEAKER_MODEL_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/onnx_speaker_model.cc b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/onnx_speaker_model.cc new file mode 100644 index 0000000000000000000000000000000000000000..3e7c6eb0e1871bbe670990ebe8797a3b17e5e369 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/onnx_speaker_model.cc @@ -0,0 +1,105 @@ +// Copyright (c) 2023 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifdef USE_ONNX + +#include + +#include "glog/logging.h" +#include "speaker/onnx_speaker_model.h" +#include "utils/utils.h" + +namespace wespeaker { + +Ort::Env OnnxSpeakerModel::env_ = + Ort::Env(ORT_LOGGING_LEVEL_WARNING, "OnnxModel"); +Ort::SessionOptions OnnxSpeakerModel::session_options_ = Ort::SessionOptions(); + +void OnnxSpeakerModel::InitEngineThreads(int num_threads) { + session_options_.SetIntraOpNumThreads(num_threads); +} + +#ifdef USE_GPU +void OnnxSpeakerModel::SetGpuDeviceId(int gpu_id) { + Ort::ThrowOnError( + OrtSessionOptionsAppendExecutionProvider_CUDA(session_options_, gpu_id)); +} +#endif + +OnnxSpeakerModel::OnnxSpeakerModel(const std::string& model_path) { + session_options_.SetGraphOptimizationLevel( + GraphOptimizationLevel::ORT_ENABLE_EXTENDED); +// 1. Load sessions +#ifdef _MSC_VER + speaker_session_ = std::make_shared( + env_, ToWString(model_path).c_str(), session_options_); +#else + speaker_session_ = std::make_shared(env_, model_path.c_str(), + session_options_); +#endif + // 2. Model info + Ort::AllocatorWithDefaultOptions allocator; + // 2.1. input info + int num_nodes = speaker_session_->GetInputCount(); + // NOTE(cdliang): for speaker model, num_nodes is 1. + CHECK_EQ(num_nodes, 1); + input_names_.resize(num_nodes); + char* name = speaker_session_->GetInputName(0, allocator); + input_names_[0] = name; + LOG(INFO) << "Ouput name: " << name; + + // 2.2. output info + num_nodes = speaker_session_->GetOutputCount(); + CHECK_EQ(num_nodes, 1); + output_names_.resize(num_nodes); + name = speaker_session_->GetOutputName(0, allocator); + output_names_[0] = name; + LOG(INFO) << "Output name: " << name; +} + +void OnnxSpeakerModel::ExtractEmbedding( + const std::vector>& feats, std::vector* embed) { + Ort::MemoryInfo memory_info = + Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); + // prepare onnx required data + unsigned int num_frames = feats.size(); + unsigned int feat_dim = feats[0].size(); + std::vector feats_onnx(num_frames * feat_dim, 0.0); + for (size_t i = 0; i < num_frames; ++i) { + for (size_t j = 0; j < feat_dim; ++j) { + feats_onnx[i * feat_dim + j] = feats[i][j]; + } + } + // NOTE(cdliang): batchsize = 1 + const int64_t feats_shape[3] = {1, num_frames, feat_dim}; + Ort::Value feats_ort = Ort::Value::CreateTensor( + memory_info, feats_onnx.data(), feats_onnx.size(), feats_shape, 3); + std::vector inputs; + inputs.emplace_back(std::move(feats_ort)); + std::vector ort_outputs = speaker_session_->Run( + Ort::RunOptions{nullptr}, input_names_.data(), inputs.data(), + inputs.size(), output_names_.data(), output_names_.size()); + // output + float* outputs = ort_outputs[0].GetTensorMutableData(); + auto type_info = ort_outputs[0].GetTensorTypeAndShapeInfo(); + + embed->reserve(type_info.GetElementCount()); + for (size_t i = 0; i < type_info.GetElementCount(); ++i) { + embed->emplace_back(outputs[i]); + } +} + +} // namespace wespeaker + +#endif // USE_ONNX diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/onnx_speaker_model.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/onnx_speaker_model.h new file mode 100644 index 0000000000000000000000000000000000000000..7ba9565b5109276ef461536aacf0a9059841fd95 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/onnx_speaker_model.h @@ -0,0 +1,56 @@ +// Copyright (c) 2023 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SPEAKER_ONNX_SPEAKER_MODEL_H_ +#define SPEAKER_ONNX_SPEAKER_MODEL_H_ + +#ifdef USE_ONNX + +#include +#include +#include +#include + +#include "onnxruntime_cxx_api.h" // NOLINT +#include "speaker/speaker_model.h" + +namespace wespeaker { + +class OnnxSpeakerModel : public SpeakerModel { + public: + static void InitEngineThreads(int num_threads = 1); +#ifdef USE_GPU + static void SetGpuDeviceId(int gpu_id = 0); +#endif + public: + explicit OnnxSpeakerModel(const std::string& model_path); + + void ExtractEmbedding(const std::vector>& feats, + std::vector* embed) override; + + private: + // session + static Ort::Env env_; + static Ort::SessionOptions session_options_; + std::shared_ptr speaker_session_ = nullptr; + // node names + std::vector input_names_; + std::vector output_names_; + int embedding_size_ = 0; +}; + +} // namespace wespeaker + +#endif // USE_ONNX +#endif // SPEAKER_ONNX_SPEAKER_MODEL_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/speaker_engine.cc b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/speaker_engine.cc new file mode 100644 index 0000000000000000000000000000000000000000..cd669f149a39fe4302f6938f3f4eeb34d46c50db --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/speaker_engine.cc @@ -0,0 +1,175 @@ +// Copyright (c) 2023 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "speaker/speaker_engine.h" +#include +#include +#include +#include + +#ifdef USE_ONNX +#include "speaker/onnx_speaker_model.h" +#endif +#ifdef USE_MNN +#include "speaker/mnn_speaker_model.h" +#endif + +namespace wespeaker { + +SpeakerEngine::SpeakerEngine(const std::string& model_path, const int feat_dim, + const int sample_rate, const int embedding_size, + const int SamplesPerChunk) { + // NOTE(cdliang): default num_threads = 1 + const int kNumGemmThreads = 1; + LOG(INFO) << "Reading model " << model_path; + embedding_size_ = embedding_size; + LOG(INFO) << "Embedding size: " << embedding_size_; + per_chunk_samples_ = SamplesPerChunk; + LOG(INFO) << "per_chunk_samples: " << per_chunk_samples_; + sample_rate_ = sample_rate; + LOG(INFO) << "Sample rate: " << sample_rate_; + feature_config_ = + std::make_shared(feat_dim, sample_rate); + feature_pipeline_ = + std::make_shared(*feature_config_); + feature_pipeline_->Reset(); +#ifdef USE_ONNX + OnnxSpeakerModel::InitEngineThreads(kNumGemmThreads); +#ifdef USE_GPU + // NOTE(cdliang): default gpu_id = 0 + OnnxSpeakerModel::SetGpuDeviceId(0); +#endif + model_ = std::make_shared(model_path); +#elif USE_MNN + model_ = std::make_shared(model_path, kNumGemmThreads); +#elif USE_BPU + model_ = std::make_shared(model_path); +#endif +} + +int SpeakerEngine::EmbeddingSize() { return embedding_size_; } + +void SpeakerEngine::ApplyMean(std::vector>* feat, + unsigned int feat_dim) { + std::vector mean(feat_dim, 0); + for (auto& i : *feat) { + std::transform(i.begin(), i.end(), mean.begin(), mean.begin(), + std::plus<>{}); + } + std::transform(mean.begin(), mean.end(), mean.begin(), + [&](const float d) { return d / feat->size(); }); + for (auto& i : *feat) { + std::transform(i.begin(), i.end(), mean.begin(), i.begin(), std::minus<>{}); + } +} + +// 1. full mode +// When per_chunk_samples_ <= 0, extract the features of the full audio. +// 2. chunk by chunk +// Extract audio features chunk by chunk, with 198 frames for each chunk. +// If the last chunk is less than 198 frames, +// concatenate the head frame to the tail. +void SpeakerEngine::ExtractFeature( + const int16_t* data, int data_size, + std::vector>>* chunks_feat) { + if (data != nullptr) { + std::vector> chunk_feat; + feature_pipeline_->AcceptWaveform( + std::vector(data, data + data_size)); + if (per_chunk_samples_ <= 0) { + // full mode + feature_pipeline_->Read(feature_pipeline_->num_frames(), &chunk_feat); + feature_pipeline_->Reset(); + chunks_feat->emplace_back(chunk_feat); + chunk_feat.clear(); + } else { + // NOTE(cdliang): extract feature with chunk by chunk + int num_chunk_frames_ = + 1 + ((per_chunk_samples_ - sample_rate_ / 1000 * 25) / + (sample_rate_ / 1000 * 10)); + int chunk_num = + std::ceil(feature_pipeline_->num_frames() / num_chunk_frames_); + chunks_feat->reserve(chunk_num); + chunk_feat.reserve(num_chunk_frames_); + while (feature_pipeline_->NumQueuedFrames() >= num_chunk_frames_) { + feature_pipeline_->Read(num_chunk_frames_, &chunk_feat); + chunks_feat->emplace_back(chunk_feat); + chunk_feat.clear(); + } + // last_chunk + int last_frames = feature_pipeline_->NumQueuedFrames(); + if (last_frames > 0) { + feature_pipeline_->Read(last_frames, &chunk_feat); + if (chunks_feat->empty()) { + // wav_len < chunk_len + int num_pad = static_cast(num_chunk_frames_ / last_frames); + for (int i = 1; i < num_pad; i++) { + chunk_feat.insert(chunk_feat.end(), chunk_feat.begin(), + chunk_feat.begin() + last_frames); + } + chunk_feat.insert( + chunk_feat.end(), chunk_feat.begin(), + chunk_feat.begin() + (num_chunk_frames_ - chunk_feat.size())); + } else { + chunk_feat.insert(chunk_feat.end(), (*chunks_feat)[0].begin(), + (*chunks_feat)[0].begin() + (num_chunk_frames_ - + chunk_feat.size())); + } + CHECK_EQ(chunk_feat.size(), num_chunk_frames_); + chunks_feat->emplace_back(chunk_feat); + chunk_feat.clear(); + } + feature_pipeline_->Reset(); + } + } else { + LOG(ERROR) << "Input is nullptr!"; + } +} + +void SpeakerEngine::ExtractEmbedding(const int16_t* data, int data_size, + std::vector* avg_emb) { + // chunks_feat: [nchunk, T, D] + std::vector>> chunks_feat; + this->ExtractFeature(data, data_size, &chunks_feat); + int chunk_num = chunks_feat.size(); + avg_emb->resize(embedding_size_, 0); + for (int i = 0; i < chunk_num; i++) { + std::vector tmp_emb; + this->ApplyMean(&chunks_feat[i], chunks_feat[i][0].size()); + model_->ExtractEmbedding(chunks_feat[i], &tmp_emb); + for (int j = 0; j < tmp_emb.size(); j++) { + (*avg_emb)[j] += tmp_emb[j]; + } + } + // avg_emb: [embedding_size_] + for (size_t i = 0; i < avg_emb->size(); i++) { + (*avg_emb)[i] /= chunk_num; + } +} + +float SpeakerEngine::CosineSimilarity(const std::vector& emb1, + const std::vector& emb2) { + CHECK_EQ(emb1.size(), emb2.size()); + float dot = std::inner_product(emb1.begin(), emb1.end(), emb2.begin(), 0.0); + float emb1_sum = + std::inner_product(emb1.begin(), emb1.end(), emb1.begin(), 0.0); + float emb2_sum = + std::inner_product(emb2.begin(), emb2.end(), emb2.begin(), 0.0); + dot /= std::max(std::sqrt(emb1_sum) * std::sqrt(emb2_sum), + std::numeric_limits::epsilon()); + dot = (dot + 1.0) / 2.0; // normalize: [-1, 1] => [0, 1] + return dot; +} + +} // namespace wespeaker diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/speaker_engine.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/speaker_engine.h new file mode 100644 index 0000000000000000000000000000000000000000..219bbb3fd58d192921fcd74f4199e841817228ae --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/speaker_engine.h @@ -0,0 +1,57 @@ +// Copyright (c) 2023 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SPEAKER_SPEAKER_ENGINE_H_ +#define SPEAKER_SPEAKER_ENGINE_H_ + +#include +#include +#include + +#include "frontend/feature_pipeline.h" +#include "speaker/speaker_model.h" + +namespace wespeaker { + +class SpeakerEngine { + public: + explicit SpeakerEngine(const std::string& model_path, const int feat_dim, + const int sample_rate, const int embedding_size, + const int SamplesPerChunk); + // return embedding_size + int EmbeddingSize(); + // extract fbank + void ExtractFeature( + const int16_t* data, int data_size, + std::vector>>* chunks_feat); + // extract embedding + void ExtractEmbedding(const int16_t* data, int data_size, + std::vector* avg_emb); + + float CosineSimilarity(const std::vector& emb1, + const std::vector& emb2); + + private: + void ApplyMean(std::vector>* feats, unsigned int feat_dim); + std::shared_ptr model_ = nullptr; + std::shared_ptr feature_config_ = nullptr; + std::shared_ptr feature_pipeline_ = nullptr; + int embedding_size_ = 0; + int per_chunk_samples_ = 32000; + int sample_rate_ = 16000; +}; + +} // namespace wespeaker + +#endif // SPEAKER_SPEAKER_ENGINE_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/speaker_model.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/speaker_model.h new file mode 100644 index 0000000000000000000000000000000000000000..08e7f8150c1b91d378899bbff7628814e3c14fa1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/speaker/speaker_model.h @@ -0,0 +1,36 @@ +// Copyright (c) 2023 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SPEAKER_SPEAKER_MODEL_H_ +#define SPEAKER_SPEAKER_MODEL_H_ + +#include +#include + +#include "utils/utils.h" + +namespace wespeaker { + +class SpeakerModel { + public: + virtual ~SpeakerModel() = default; + // extract embedding + // NOTE: https://www.cnblogs.com/zhmlzhml/p/12973618.html + virtual void ExtractEmbedding(const std::vector>& feats, + std::vector* embed) {} +}; + +} // namespace wespeaker + +#endif // SPEAKER_SPEAKER_MODEL_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/toolchains/aarch64-linux-gnu.toolchain.cmake b/MOSS_Speaker-RoPE/wespeaker/runtime/core/toolchains/aarch64-linux-gnu.toolchain.cmake new file mode 100644 index 0000000000000000000000000000000000000000..9ad37cba9eb6fa58aa194ece96cf9a5da472a76d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/toolchains/aarch64-linux-gnu.toolchain.cmake @@ -0,0 +1,5 @@ +set(CMAKE_SYSTEM_NAME Linux) +SET (CMAKE_SYSTEM_PROCESSOR aarch64) + +set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) +set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/CMakeLists.txt b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e18684316b73a7668f3f3fa7dc162c709fbe968e --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(utils STATIC + utils.cc +) +target_link_libraries(utils PUBLIC glog gflags) diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/blocking_queue.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/blocking_queue.h new file mode 100644 index 0000000000000000000000000000000000000000..b1748f6113509d900fd88cf521155d231fb20d79 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/blocking_queue.h @@ -0,0 +1,98 @@ +// Copyright (c) 2020 Mobvoi Inc (Binbin Zhang) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef UTILS_BLOCKING_QUEUE_H_ +#define UTILS_BLOCKING_QUEUE_H_ + +#include +#include +#include +#include +#include + +namespace wenet { + +#define WENET_DISALLOW_COPY_AND_ASSIGN(Type) \ + Type(const Type&) = delete; \ + Type& operator=(const Type&) = delete; + +template +class BlockingQueue { + public: + explicit BlockingQueue(size_t capacity = std::numeric_limits::max()) + : capacity_(capacity) {} + + void Push(const T& value) { + { + std::unique_lock lock(mutex_); + while (queue_.size() >= capacity_) { + not_full_condition_.wait(lock); + } + queue_.push(value); + } + not_empty_condition_.notify_one(); + } + + void Push(T&& value) { + { + std::unique_lock lock(mutex_); + while (queue_.size() >= capacity_) { + not_full_condition_.wait(lock); + } + queue_.push(std::move(value)); + } + not_empty_condition_.notify_one(); + } + + T Pop() { + std::unique_lock lock(mutex_); + while (queue_.empty()) { + not_empty_condition_.wait(lock); + } + T t(std::move(queue_.front())); + queue_.pop(); + not_full_condition_.notify_one(); + return t; + } + + bool Empty() const { + std::lock_guard lock(mutex_); + return queue_.empty(); + } + + size_t Size() const { + std::lock_guard lock(mutex_); + return queue_.size(); + } + + void Clear() { + while (!Empty()) { + Pop(); + } + } + + private: + size_t capacity_; + mutable std::mutex mutex_; + std::condition_variable not_full_condition_; + std::condition_variable not_empty_condition_; + std::queue queue_; + + public: + WENET_DISALLOW_COPY_AND_ASSIGN(BlockingQueue); +}; + +} // namespace wenet + +#endif // UTILS_BLOCKING_QUEUE_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/thread_pool.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/thread_pool.h new file mode 100644 index 0000000000000000000000000000000000000000..a78162995d90bf079ad091cf14cb9f2cd4476d05 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/thread_pool.h @@ -0,0 +1,113 @@ +// Copyright (c) 2012 Jakob Progsch, Václav Zeman + +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. + +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: + +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. + +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. + +// 3. This notice may not be removed or altered from any source +// distribution. + +#ifndef UTILS_THREAD_POOL_H_ +#define UTILS_THREAD_POOL_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ThreadPool { + public: + explicit ThreadPool(size_t); + template + auto enqueue(F&& f, Args&&... args) + -> std::future::type>; + ~ThreadPool(); + + private: + // need to keep track of threads so we can join them + std::vector workers; + // the task queue + std::queue > tasks; + + // synchronization + std::mutex queue_mutex; + std::condition_variable condition; + bool stop; +}; + +// the constructor just launches some amount of workers +inline ThreadPool::ThreadPool(size_t threads) : stop(false) { + for (size_t i = 0; i < threads; ++i) + workers.emplace_back([this] { + for (;;) { + std::function task; + + { + std::unique_lock lock(this->queue_mutex); + this->condition.wait( + lock, [this] { return this->stop || !this->tasks.empty(); }); + if (this->stop && this->tasks.empty()) return; + task = std::move(this->tasks.front()); + this->tasks.pop(); + } + + task(); + } + }); +} + +// add new work item to the pool +template +auto ThreadPool::enqueue(F&& f, Args&&... args) + -> std::future::type> { + using return_type = typename std::result_of::type; + + auto task = std::make_shared >( + std::bind(std::forward(f), std::forward(args)...)); + + std::future res = task->get_future(); + { + std::unique_lock lock(queue_mutex); + + // don't allow enqueueing after stopping the pool + if (stop) { + throw std::runtime_error("enqueue on stopped ThreadPool"); + } + + tasks.emplace([task]() { (*task)(); }); + } + condition.notify_one(); + return res; +} + +// the destructor joins all threads +inline ThreadPool::~ThreadPool() { + { + std::unique_lock lock(queue_mutex); + stop = true; + } + condition.notify_all(); + for (std::thread& worker : workers) { + worker.join(); + } +} + +#endif // UTILS_THREAD_POOL_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/timer.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/timer.h new file mode 100644 index 0000000000000000000000000000000000000000..068519f98d140ba0eef68babe2ad2fdcb798c074 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/timer.h @@ -0,0 +1,39 @@ +// Copyright (c) 2021 Mobvoi Inc (Binbin Zhang) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef UTILS_TIMER_H_ +#define UTILS_TIMER_H_ + +#include + +namespace wenet { + +class Timer { + public: + Timer() : time_start_(std::chrono::steady_clock::now()) {} + void Reset() { time_start_ = std::chrono::steady_clock::now(); } + // return int in milliseconds + int Elapsed() const { + auto time_now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast(time_now - + time_start_) + .count(); + } + + private: + std::chrono::time_point time_start_; +}; +} // namespace wenet + +#endif // UTILS_TIMER_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/utils.cc b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/utils.cc new file mode 100644 index 0000000000000000000000000000000000000000..cd4c870e4fae354de8b22a2848a208c808ad2752 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/utils.cc @@ -0,0 +1,100 @@ +// Copyright (c) 2023 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include + +#include "glog/logging.h" +#include "utils/utils.h" + +namespace wespeaker { + +void WriteToFile(const std::string& file_path, + const std::vector>& embs) { + // embs [num_enroll, emb_dim] + std::ofstream fout; + fout.open(file_path, std::ios::out); + for (size_t i = 0; i < embs.size(); i++) { + for (size_t j = 0; j < embs[0].size(); j++) { + fout << embs[i][j] << " "; + } + fout << std::endl; + } + fout.close(); +} + +void ReadToFile(const std::string& file_path, + std::vector>* embs) { + // embs [num_enroll, emb_dim] + std::ifstream fin(file_path); + std::string line; + while (getline(fin, line)) { + std::vector tmp; + std::stringstream word(line); + float num; + while (word >> num) { + tmp.push_back(num); + } + embs->push_back(tmp); + } +} + +std::string Ltrim(const std::string& str) { + size_t start = str.find_first_not_of(WHITESPACE); + return (start == std::string::npos) ? "" : str.substr(start); +} + +std::string Rtrim(const std::string& str) { + size_t end = str.find_last_not_of(WHITESPACE); + return (end == std::string::npos) ? "" : str.substr(0, end + 1); +} + +std::string Trim(const std::string& str) { return Rtrim(Ltrim(str)); } + +void SplitString(const std::string& str, std::vector* strs) { + SplitStringToVector(Trim(str), " \t", true, strs); +} + +void SplitStringToVector(const std::string& full, const char* delim, + bool omit_empty_strings, + std::vector* out) { + size_t start = 0, found = 0, end = full.size(); + out->clear(); + while (found != std::string::npos) { + found = full.find_first_of(delim, start); + // start != end condition is for when the delimiter is at the end + if (!omit_empty_strings || (found != start && start != end)) + out->push_back(full.substr(start, found - start)); + start = found + 1; + } +} + +#ifdef _MSC_VER +std::wstring ToWString(const std::string& str) { + unsigned len = str.size() * 2; + setlocale(LC_CTYPE, ""); + wchar_t* p = new wchar_t[len]; + mbstowcs(p, str.c_str(), len); + std::wstring wstr(p); + delete[] p; + return wstr; +} +#endif + +} // namespace wespeaker diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/utils.h b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/utils.h new file mode 100644 index 0000000000000000000000000000000000000000..9f52815ec9a4028d949ca4bd22b1bf356ba2100b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/core/utils/utils.h @@ -0,0 +1,44 @@ +// Copyright (c) 2023 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef UTILS_UTILS_H_ +#define UTILS_UTILS_H_ + +#include +#include +#include + +namespace wespeaker { + +const char WHITESPACE[] = " \n\r\t\f\v"; + +void WriteToFile(const std::string& file_path, + const std::vector>& embs); +void ReadToFile(const std::string& file_path, + std::vector>* embs); + +// Split the string with space or tab. +void SplitString(const std::string& str, std::vector* strs); + +void SplitStringToVector(const std::string& full, const char* delim, + bool omit_empty_strings, + std::vector* out); + +#ifdef _MSC_VER +std::wstring ToWString(const std::string& str); +#endif + +} // namespace wespeaker + +#endif // UTILS_UTILS_H_ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/mnn/.gitignore b/MOSS_Speaker-RoPE/wespeaker/runtime/mnn/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..32e4a3dda4c6acb9d0554e646a4325823c0b05ea --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/mnn/.gitignore @@ -0,0 +1,2 @@ +fc_base/ +build* diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/mnn/CMakeLists.txt b/MOSS_Speaker-RoPE/wespeaker/runtime/mnn/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..b1e74bc446935da13dcb2a0537016bb38244a510 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/mnn/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.14) +project(wespeaker VERSION 0.1) + +set(MNN ON CACHE BOOL "whether to build with MNN") +option(MINI_LIBS "whether to build minimum libraies with MNN" OFF) + +set(CMAKE_VERBOSE_MAKEFILE OFF) + +include(FetchContent) +set(FETCHCONTENT_QUIET OFF) +get_filename_component(fc_base "fc_base" REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +set(FETCHCONTENT_BASE_DIR ${fc_base}) + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -pthread -fPIC") + +# Include all dependency +if(MNN) + include(mnn) +endif() +include(glog) +include(gflags) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + +# build all libraries +add_subdirectory(utils) +add_subdirectory(frontend) +add_subdirectory(speaker) +add_subdirectory(bin) diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/mnn/README.md b/MOSS_Speaker-RoPE/wespeaker/runtime/mnn/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ef6fd635901a852954feb50f52a1fce60227c73e --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/mnn/README.md @@ -0,0 +1,92 @@ +# MNN backend on WeSpeaker + +* Step 1. Export your experiment model to MNN + +First, export your experiment model to ONNX by [export_onnx.py](../../wespeaker/bin/export_onnx.py). + +``` sh +# 1. dynamic shape +python wespeaker/bin/export_onnx.py \ + --config config.yaml \ + --checkpoint model.pt \ + --output_model model.onnx + # When it finishes, you can find `model.onnx`. +# 2. static shape +# python wespeaker/bin/export_onnx.py \ +# --config config.yaml \ +# --checkpoint model.pt \ +# --output_model model.onnx \ +# --num_frames 198 +``` + +Second, export ONNX to MNN by [export_mnn.py](../../wespeaker/bin/export_mnn.py). + +``` sh +# 1. dynamic shape +python wespeaker/bin/export_mnn.py \ + --onnx_model model.onnx \ + --output_model model.mnn +# When it finishes, you can find `model.mnn`. +# 2. static shape +# python wespeaker/bin/export_mnn.py \ +# --onnx_model model.onnx \ +# --output_model model.mnn \ +# --num_frames 198 +``` + +* Step 2. Build. The build requires cmake 3.14 or above, and gcc/g++ 5.4 or above. + +``` sh +mkdir build && cd build +# 1. normal +cmake .. +# 2. minimum libs +# cmake -DMINI_LIBS=ON .. +cmake --build . +``` + +* Step 3. Testing. + +1. the RTF(real time factor) is shown in the console, and embedding will be written to the txt file. +``` sh +export GLOG_logtostderr=1 +export GLOG_v=2 +./build/bin/extract_emb_main \ + --wav_scp wav.scp \ + --result embedding.txt \ + --speaker_model_path model.mnn \ + --embedding_size 256 \ + --samples_per_chunk 80000 # 5s +``` + +> NOTE: samples_per_chunk: samples of one chunk. samples_per_chunk = sample_rate * duration +> +> If samples_per_chunk = -1, compute the embedding of whole sentence; +> else compute embedding with chunk by chunk, and then average embeddings of chunk. + +2. Calculate the similarity of two speech. +```sh +export GLOG_logtostderr=1 +export GLOG_v=2 +./build/bin/asv_main \ + --enroll_wav wav1_path \ + --test_wav wav2_path \ + --threshold 0.5 \ + --speaker_model_path model.mnn \ + --embedding_size 256 +``` + +## Benchmark + +1. RTF +> num_threads = 1 +> +> samples_per_chunk = 32000 +> +> Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz + +| Model | Params | FLOPs | engine | RTF | +| :------------------ | :------ | :------- | :------------ | :------- | +| ResNet-34 | 6.63 M | 4.55 G | onnxruntime | 0.1377 | +| ResNet-34 | 6.63 M | 4.55 G | mnn | 0.1333 | +| ResNet-34 | 6.63 M | 4.55 G | mnn mini_libs | 0.05262 | diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/onnxruntime/.gitignore b/MOSS_Speaker-RoPE/wespeaker/runtime/onnxruntime/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c472add81c6d7bd193a76d6bfe12a2e812b630c1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/onnxruntime/.gitignore @@ -0,0 +1,2 @@ +fc_base/ +build/ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/onnxruntime/CMakeLists.txt b/MOSS_Speaker-RoPE/wespeaker/runtime/onnxruntime/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e0cc4b3a3448ad54e9e4e3abd70e19aa9a9c7258 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/onnxruntime/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.14) +project(wespeaker VERSION 0.1) + +option(ONNX "whether to build with ONNX" ON) +option(GPU "whether to build with GPU" OFF) + +set(CMAKE_VERBOSE_MAKEFILE OFF) + +include(FetchContent) +set(FETCHCONTENT_QUIET OFF) +get_filename_component(fc_base "fc_base" REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +set(FETCHCONTENT_BASE_DIR ${fc_base}) + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -pthread -fPIC") + +# Include all dependency +if(ONNX) + include(onnx) +endif() +include(glog) +include(gflags) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + +# build all libraries +add_subdirectory(utils) +add_subdirectory(frontend) +add_subdirectory(speaker) +add_subdirectory(bin) diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/onnxruntime/README.md b/MOSS_Speaker-RoPE/wespeaker/runtime/onnxruntime/README.md new file mode 100644 index 0000000000000000000000000000000000000000..70edd9d79f2a58fc478e3843f0527458ae5f79e1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/onnxruntime/README.md @@ -0,0 +1,110 @@ +# ONNX backend on WeSpeaker + +* Step 1. Export your experiment model to ONNX by https://github.com/wenet-e2e/wespeaker/blob/master/wespeaker/bin/export_onnx.py + +``` sh +exp=exp # Change it to your experiment dir +onnx_dir=onnx +python wespeaker/bin/export_onnx.py \ + --config $exp/config.yaml \ + --checkpoint $exp/avg_model.pt \ + --output_model $onnx_dir/final.onnx + +# When it finishes, you can find `final.onnx`. +``` + +* Step 2. Build. The build requires cmake 3.14 or above, and gcc/g++ 5.4 or above. + +``` sh +mkdir build && cd build +# 1. no gpu +cmake -DONNX=ON .. +# 2. gpu +# cmake -DONNX=ON -DGPU=ON .. +cmake --build . +``` + +* Step 3. Testing. + +> NOTE: If using GPU, you need to specify the cuda path. +> ```bash +> export PATH=/usr/local/cuda-11.1/bin${PATH:+:${PATH}} +> export LD_LIBRARY_PATH=/usr/local/cuda-11.1/lib64:${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} +> ``` + +1. the RTF(real time factor) is shown in the console, and embedding will be written to the txt file. +``` sh +export GLOG_logtostderr=1 +export GLOG_v=2 +wav_scp=your_test_wav_scp +onnx_dir=your_model_dir +embed_out=your_embedding_txt +./build/bin/extract_emb_main \ + --wav_scp $wav_scp \ + --result $embed_out \ + --speaker_model_path $onnx_dir/final.onnx \ + --embedding_size 256 \ + --samples_per_chunk 80000 # 5s + +``` + +> NOTE: samples_per_chunk: samples of one chunk. samples_per_chunk = sample_rate * duration +> +> If samples_per_chunk = -1, compute the embedding of whole sentence; +> else compute embedding with chunk by chunk, and then average embeddings of chunk. + +2. Calculate the similarity of two speech. +```sh +export GLOG_logtostderr=1 +export GLOG_v=2 +onnx_dir=your_model_dir +./build/bin/asv_main \ + --enroll_wav wav1_path \ + --test_wav wav2_path \ + --threshold 0.5 \ + --speaker_model_path $onnx_dir/final.onnx \ + --embedding_size 256 +``` + +## Benchmark +1. RTF +> num_threads = 1 +> +> samples_per_chunk = 80000 +> +> CPU: Intel(R) Xeon(R) Platinum 8160 CPU @ 2.10GHz + +| Model | Params | FLOPs | RTF | +| :------------------ | :------ | :------- | :------- | +| ECAPA-TDNN (C=512) | 6.19 M | 1.04 G | 0.018351 | +| ECAPA-TDNN (C=1024) | 14.65 M | 2.65 G | 0.041724 | +| RepVGG-TINY-A0 | 6.26 M | 4.65 G | 0.055117 | +| ResNet-34 | 6.63 M | 4.55 G | 0.060735 | +| ResNet-50 | 11.13 M | 5.17 G | 0.073231 | +| ResNet-101 | 15.89 M | 9.96 G | 0.124613 | +| ResNet-152 | 19.81 M | 14.76 G | 0.179379 | +| ResNet-221 | 23.79 M | 21.29 G | 0.267511 | +| ResNet-293 | 28.62 M | 28.10 G | 0.364011 | +| CAM++ | 7.18 M | 1.15 G | 0.022978 | + +> num_threads = 1 +> +> samples_per_chunk = 80000 +> +> CPU: Intel(R) Xeon(R) Platinum 8160 CPU @ 2.10GHz +> +> GPU: NVIDIA 3090 + +| Model | Params | FLOPs | RTF | +| :------------------ | :------ | :------- | :--------- | +| ResNet-34 | 6.63 M | 4.55 G | 0.00857436 | + +2. EER (%) +> onnxruntime: samples_per_chunk=-1. +> +> don't use mean normalization for evaluation embeddings. + +| Model | vox-O | vox-E | vox-H | +| :------------- | ----- | ----- | ----- | +| ResNet-34-pt | 0.814 | 0.933 | 1.679 | +| ResNet-34-onnx | 0.814 | 0.933 | 1.679 | diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/Dockerfile/dockerfile.client b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/Dockerfile/dockerfile.client new file mode 100644 index 0000000000000000000000000000000000000000..a7f8219d1d39f6b83ce32e50920f625de2b22071 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/Dockerfile/dockerfile.client @@ -0,0 +1,33 @@ +################################################################################################### +# +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials +# provided with the distribution. +# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used +# to endorse or promote products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +################################################################################################### + +FROM nvcr.io/nvidia/tritonserver:22.07-py3-sdk +LABEL maintainer="NVIDIA" +LABEL repository="tritonserver" + +RUN apt-get update && apt-get install -y libsndfile1 +RUN pip3 install soundfile kaldiio +WORKDIR /workspace diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/Dockerfile/dockerfile.server b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/Dockerfile/dockerfile.server new file mode 100644 index 0000000000000000000000000000000000000000..ca5621586b0e671690b7ec9cf461dc1bbc6786d8 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/Dockerfile/dockerfile.server @@ -0,0 +1,39 @@ +################################################################################################### +# +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials +# provided with the distribution. +# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used +# to endorse or promote products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +################################################################################################### +FROM nvcr.io/nvidia/tritonserver:22.07-py3 +LABEL maintainer="NVIDIA" +LABEL repository="tritonserver" + +RUN apt-get update && apt-get -y install swig && apt-get -y install python3-dev && apt-get install -y cmake +RUN pip3 install torch==1.10.0+cu113 torchvision==0.11.1+cu113 torchaudio==0.10.0+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html +RUN pip3 install -v kaldifeat +RUN python3 -m pip install cupy +RUN python3 -m pip install soundfile +RUN pip3 install --upgrade pip +RUN pip install --extra-index-url https://pypi.nvidia.com cudf_cu11 +RUN pip install --extra-index-url https://pypi.nvidia.com cuml_cu11 +RUN pip install --extra-index-url https://pypi.nvidia.com cugraph_cu11 +WORKDIR /workspace diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/README.md b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8722da3219d5a07ab39a6a01ea87fc2e28da1ae2 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/README.md @@ -0,0 +1,184 @@ +# Best Practice for Deploying a WeSpeaker diarization service using Triton + +In this best practice, we'll go through how to deploy a WeSpeaker diarization pipeline in GPU by using NVIDIA [Triton Inference Server](https://github.com/triton-inference-server/server), which contains several modules including SAD, Speaker Embedding Extraction, Clustering and etc. + +We will use [Triton Business Logic Scripting](https://github.com/triton-inference-server/python_backend#business-logic-scripting) (BLS) to implement this pipeline. + +## Table of Contents + +- [Preparation](#preparation) + - [Prepare Environment](#prepare-environment) + - [Prepare Models](#prepare-models) + - [Preapare Test Data](#prepare-test-data) +- [Triton Inference Server](#triton-inference-server) + - [Quick Start](#quick-start) + - [Business Logic Scripting](#bls) +- [Inference Client](#inference-client) + - [Quick Start](#quick-start-1) + - [Compute Metrics](#compute-metrics) +- [Benchmark](#benchmark) + + +## Preparation + +Let's prepare enrivonments, models and data first. + +### Prepare Environment + +Clone the repository: + +```bash +# Clond WeSpeaker repo +git clone https://github.com/wenet-e2e/wespeaker.git +export WeSpeaker=$PWD/wespeaker/ +cd runtime/server/diarization_gpu +export PROJECT_DIR=$PWD + +``` + +### Prepare Models + +To depoloy this pipeline, first we should obtain SAD and Speaker models. + +#### Speaker Models + +You can refer to [voxceleb sv recipe](https://github.com/wenet-e2e/wespeaker/tree/master/examples/voxceleb/v2) to train a WeSpeaker model or use a pre-trained model: + +```bash +export SPK_MODEL_DIR=/workspace/pretrained_models +mkdir -p ${SPK_MODEL_DIR} +wget -c https://wespeaker-1256283475.cos.ap-shanghai.myqcloud.com/models/voxceleb/voxceleb_resnet34_LM.onnx -O ${SPK_MODEL_DIR}/voxceleb_resnet34_LM.onnx +``` + +Then you can follow the best practice of [GPU deployment](https://github.com/wenet-e2e/wespeaker/tree/master/runtime/server/x86_gpu) to deploy the WeSpeaker model in Triton. +After that, speaker models will be avaliable in `wespeaker/runtime/server/x86_gpu/model_repo/` directory. + +```bash +export SPK_MODEL_REPO="wespeaker/runtime/server/x86_gpu/model_repo/" +``` + +#### SAD Models + +Speaker activity detection model: system SAD (VAD model pretrained by [silero](https://github.com/snakers4/silero-vad)). + +```bash +export SAD_DIR=/workspace/SAD +wget -c https://github.com/snakers4/silero-vad/archive/refs/tags/v3.1.zip -O external_tools/silero-vad-v3.1.zip +unzip -o external_tools/silero-vad-v3.1.zip -d external_tools +cp external_tools/silero-vad-3.1/files/silero_vad.jit $SAD_DIR/ +``` + +### Prepare Test Data + +You can use the following command to access the evluation datas from voxconverse: + +```bash +bash $WeSpeaker/examples/voxconverse/v1/run.sh --stage 2 --stop_stage 2 +``` + +If you are using your own data, you can evaluate the audio one by one. Or you should preapre a `wav.scp`, which contains a list of audios. For example, + +``` +abjxc abjxc.wav +afjiv afjiv.wav +``` + +## Triton Inference Server + +[Triton Inference Server](https://github.com/triton-inference-server/server) can help with the most of serving work for us and handles requests/results sending and receiving, request scheduling, load balance, and inference execution. In this section, we will use Triton to depoy the diarization pipeline. + +![Pipeline](./bls.png) + +Build the server docker image: +``` +docker build . -f Dockerfile/dockerfile.server -t wespeaker_server:latest --network host +``` + +You can also directly use the pre-built image: +``` +docker pull wd929/wespeaker_server:v1 +``` + +Build the client docker image: +``` +docker build . -f Dockerfile/dockerfile.client -t wespeaker_client:latest --network host +``` + +Run the following commands to put the pretrained SAD and Speaker models into current `model_repo` directory. + +```bash +cd ${PROJECT_DIR} +mkdir -p model_repo/run/1 +cp -r $SPK_MODEL_REPO/* model_repo/ + +``` + +### Quick Start + +Now start server: + +```bash +# Start the docker container +docker run --gpus all -v $PWD/model_repo:/workspace/model_repo -v $SAD_DIR:/workspace/triton/ --shm-size=1g --ulimit memlock=-1 -p 8000:8000 -p 8001:8001 -p 8002:8002 --ulimit stack=67108864 -it wespeaker_server:latest + +# Inside the docker container +tritonserver --model-repository=/workspace/model_repo + +``` + +### Business Logic Scripting + +Business Logic Scripting (BLS) can execute inference requests on other models being served by Triton as a part of executing one Python model. + + +## Inference Client + +In this section, we will show how to send requests to our deployed SD service, and receive the RTTM results. + + +### Quick Start + +Run, + +```bash +AUDIO_DATA= +docker run -ti --net host -v $PWD/client:/ws/client -v $AUDIO_DATA:/ws/test_data wespeaker_client:latest +cd /ws/client +``` + +In the docker container, run the client script to do the whole pipeline inference. + +```bash +# Test one audio +export output_directory="output" +mkdir -p $output_directory +python client.py --url=localhost:8001 --audio_file=/ws/test_data/abjxc.wav --output_directory=$output_directory +cat $output_directory/rttm* > $output_directory/rttm +``` + +The above command sends a single audio `abjxc.wav` to the server and get the result. `--url` option specifies the IP and port of the server, in our example, we set the server and client on the same machine, therefore IP is `localhost`, and we use port `8001` since it is the default port for gRPC in Triton. But if your client is not on the same machine as the server, you should change this option. + +You can also test specify the path of `wav.scp` with `--wavscp` option, then the client will test the audio files in the `wav.scp`. + +```bash +# Test a bunch of audios +export wav_scp_dir=/ws/test_data +python client.py --url=localhost:8001 --wavscp=$wav_scp_dir/wav.scp --output_directory="outp" +cat $output_directory/rttm* > $output_directory/rttm +``` + +Finally, you can get the RTTM information in `$output_directory/rttm`. + +### Compute Metrics + +If you want to test the performances of our SD pipeline, you can run: + +```bash +perl external_tools/SCTK-2.4.12/src/md-eval/md-eval.pl \ + -c 0.25 \ + -r <(cat data/voxconverse-master/dev/*.rttm) \ + -s $output_directory/rttm +``` + +## Benchmark (TODO) + diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/bls.png b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/bls.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd15d493acd599c6f2c3ccbed7d0645c92a9238 Binary files /dev/null and b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/bls.png differ diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/client/client.py b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/client/client.py new file mode 100644 index 0000000000000000000000000000000000000000..a2d034eb53cd5b1bfbf2998f52a9d20fb80cbb45 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/client/client.py @@ -0,0 +1,157 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import multiprocessing +from multiprocessing import Pool + +import tritonclient.grpc as grpcclient +from tritonclient.utils import np_to_triton_dtype +import numpy as np +import soundfile +import argparse +import os + + +class SpeakerClient(object): + + def __init__(self, triton_client, model_name, protocol_client): + self.triton_client = triton_client + self.protocol_client = protocol_client + self.model_name = model_name + + def recognize(self, wav_path, client_index): + # We send batchsize=1 data to server + # BatchSize > 1 is also ok but you need to take care of + # padding. + waveform, sample_rate = soundfile.read(wav_path) + cur_length = len(waveform) + input = np.zeros((1, cur_length), dtype=np.float32) + input[0][0:cur_length] = waveform[0:cur_length] + inputs = [ + self.protocol_client.InferInput("input", input.shape, + np_to_triton_dtype(input.dtype)) + ] + inputs[0].set_data_from_numpy(input) + outputs = [grpcclient.InferRequestedOutput("LABELS")] + response = self.triton_client.infer(self.model_name, + inputs, + request_id=str(client_index), + outputs=outputs) + result = response.as_numpy("LABELS")[0] + return [result] + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-v', + '--verbose', + action="store_true", + required=False, + default=False, + help='Enable verbose output') + parser.add_argument('-u', + '--url', + type=str, + required=False, + default='localhost:8001', + help='Inference server URL. Default is ' + 'localhost:8001.') + parser.add_argument('--model_name', + required=False, + default='run', + help='the model to send request to') + parser.add_argument('--wavscp', + type=str, + required=False, + default=None, + help='audio_id \t absolute_wav_path') + parser.add_argument('--output_directory', + type=str, + required=False, + default=None, + help='the path to save the segment files') + parser.add_argument('--data_dir', + type=str, + required=False, + default=None, + help='data dir will be append to audio file if given') + parser.add_argument('--audio_file', + type=str, + required=False, + default=None, + help='single wav file') + FLAGS = parser.parse_args() + + # load data + audio_wavpath = [] + if FLAGS.audio_file is not None: + path = FLAGS.audio_file + if FLAGS.data_dir: + path = os.path.join(FLAGS.data_dir, path) + if os.path.exists(path): + audio_wavpath = [(FLAGS.audio_file, path)] + elif FLAGS.wavscp is not None: + with open(FLAGS.wavscp, "r", encoding="utf-8") as f: + for line in f: + aid, path = line.strip().split(' ') + audio_wavpath.append((aid, path)) + + num_workers = multiprocessing.cpu_count() // 2 + + def single_job(li): + idx, audio_files = li + dir_name = os.path.dirname(FLAGS.output_directory) # get the path + if not os.path.exists(dir_name) and (dir_name != ''): + os.makedirs(dir_name) + seg_writer = open(os.path.join(FLAGS.output_directory, + 'rttm' + str(idx)), + 'w', + encoding="utf-8") + + with grpcclient.InferenceServerClient( + url=FLAGS.url, verbose=FLAGS.verbose) as triton_client: + protocol_client = grpcclient + speech_client = SpeakerClient(triton_client, FLAGS.model_name, + protocol_client) + + predictions = {} + + for li in audio_files: + utt, wavpath = li + rttms = speech_client.recognize(wavpath, idx)[0] + spec = "SPEAKER {} {} {:.3f} {:.3f} {} " + for i in range(0, rttms.shape[0]): + begin = rttms[i][0] + end = rttms[i][1] + label = int(rttms[i][2]) + channel = 1 + seg_writer.write( + spec.format(utt, channel, begin, end - begin, label) + + '\n') + seg_writer.flush() + return predictions + + # start to do inference + # Group requests in batches + predictions = [] + tasks = [] + splits = np.array_split(audio_wavpath, num_workers) + + for idx, per_split in enumerate(splits): + cur_files = per_split.tolist() + tasks.append((idx, cur_files)) + + with Pool(processes=num_workers) as pool: + prediction = pool.map(single_job, tasks) diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/clusterer/1/model.py b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/clusterer/1/model.py new file mode 100644 index 0000000000000000000000000000000000000000..ad3b6816772318a4eb097662326b1c6b03a1b429 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/clusterer/1/model.py @@ -0,0 +1,167 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import triton_python_backend_utils as pb_utils +from torch.utils.dlpack import from_dlpack +import json +import cupy as cp +import numpy as np +from cuml.cluster import KMeans as cuKM + + +class TritonPythonModel: + """Your Python model must use the same class name. Every Python model + that is created must have "TritonPythonModel" as the class name. + """ + + def initialize(self, args): + """`initialize` is called only once when the model is being loaded. + Implementing `initialize` function is optional. This function allows + the model to initialize any state associated with this model. + + Parameters + ---------- + args : dict + Both keys and values are strings. The dictionary keys and values are: + * model_config: A JSON string containing the model configuration + * model_instance_kind: A string containing model instance kind + * model_instance_device_id: A string containing model instance + * device ID + * model_repository: Model repository path + * model_version: Model version + * model_name: Model name + """ + self.model_config = model_config = json.loads(args['model_config']) + self.max_batch_size = max(model_config["max_batch_size"], 1) + + if "GPU" in model_config["instance_group"][0]["kind"]: + self.device = "cuda" + else: + self.device = "cpu" + + # Get OUTPUT0 configuration + output0_config = pb_utils.get_output_config_by_name( + model_config, "LABELS") + # Convert Triton types to numpy types + self.output0_dtype = pb_utils.triton_string_to_numpy( + output0_config['data_type']) + + def cluster_gpu(self, + embeddings, + p=.01, + num_spks=None, + min_num_spks=1, + max_num_spks=20): + # Define utility functions + def cosine_similarity(M): + M = M / cp.linalg.norm(M, axis=1, keepdims=True) + return 0.5 * (1.0 + cp.dot(M, M.T)) + + def prune(M, p): + m = M.shape[0] + if m < 1000: + n = max(m - 10, 2) + else: + n = int((1.0 - p) * m) + for i in range(m): + indexes = cp.argsort(M[i, :]) + low_indexes, high_indexes = indexes[0:n], indexes[n:m] + M[i, low_indexes] = 0.0 + M[i, high_indexes] = 1.0 + return 0.5 * (M + M.T) + + def laplacian(M): + M[cp.diag_indices(M.shape[0])] = 0.0 + D = cp.diag(cp.sum(cp.abs(M), axis=1)) + return D - M + + def spectral(M, num_spks, min_num_spks, max_num_spks): + eig_values, eig_vectors = cp.linalg.eigh(M) + num_spks = num_spks if num_spks is not None \ + else cp.argmax(cp.diff(eig_values[:max_num_spks + 1])) + 1 + num_spks = max(num_spks, min_num_spks) + return eig_vectors[:, :num_spks] + + def kmeans(data): + k = data.shape[1] + kmeans_float = cuKM(n_clusters=k, n_init=10, random_state=10) + kmeans_float.fit(cp.asarray(data)) + return kmeans_float.labels_ + + # Fallback for trivial cases + if len(embeddings) <= 2: + return [0] * len(embeddings) + + # Compute similarity matrix + similarity_matrix = cosine_similarity(embeddings) + # Prune matrix with p interval + pruned_similarity_matrix = prune(similarity_matrix, p) + # Compute Laplacian + laplacian_matrix = laplacian(pruned_similarity_matrix) + # Compute spectral embeddings + spectral_embeddings = spectral(laplacian_matrix, num_spks, + min_num_spks, max_num_spks) + # Assign class labels + labels = kmeans(spectral_embeddings) + + return labels + + def execute(self, requests): + """`execute` must be implemented in every Python model. `execute` + function receives a list of pb_utils.InferenceRequest as the only + argument. This function is called when an inference is requested + for this model. + + Parameters + ---------- + requests : list + A list of pb_utils.InferenceRequest + + Returns + ------- + list + A list of pb_utils.InferenceResponse. + The length of this list must be the same as `requests` + """ + batch_count = [] + total_embd = [] + + responses = [] + for request in requests: + # the requests will all have the same shape + # different shape request will be + # separated by triton inference server + input0 = pb_utils.get_input_tensor_by_name(request, "EMBEDDINGS") + cur_b_embd = from_dlpack(input0.to_dlpack()) + cur_batch = cur_b_embd.shape[0] + batch_count.append(cur_batch) + + for embds in cur_b_embd: + total_embd.append(embds.to(self.device)) + + labels_list = [] + for embds in total_embd: + res = self.cluster_gpu(cp.asarray(embds)) + labels_list.append(cp.asnumpy(res)) + + idx = 0 + for b in batch_count: + batch_labels = np.array(labels_list[idx:idx + b]) + idx += b + out0 = pb_utils.Tensor("LABELS", + batch_labels.astype(self.output0_dtype)) + inference_response = pb_utils.InferenceResponse( + output_tensors=[out0]) + responses.append(inference_response) + return responses diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/clusterer/config.pbtxt b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/clusterer/config.pbtxt new file mode 100644 index 0000000000000000000000000000000000000000..87f310cddb9bf9a254164148e22339d042305a29 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/clusterer/config.pbtxt @@ -0,0 +1,43 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "clusterer" +backend: "python" +max_batch_size: 256 + +input [ + { + name: "EMBEDDINGS" + data_type: TYPE_FP32 + dims: [ -1, 256 ] # embedding dim + } +] + +output [ + { + name: "LABELS" + data_type: TYPE_INT32 + dims: [ -1 ] + } +] + +dynamic_batching { + preferred_batch_size: [ 16, 32 ] + } +instance_group [ + { + count: 2 + kind: KIND_GPU + } +] diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/run/1/model.py b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/run/1/model.py new file mode 100644 index 0000000000000000000000000000000000000000..ef73734d0cb4de92e80c2e125127e5ddf9815d31 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/run/1/model.py @@ -0,0 +1,387 @@ +import triton_python_backend_utils as pb_utils +from torch.utils.dlpack import to_dlpack, from_dlpack +import torch +import numpy as np +import json +import asyncio + + +class TritonPythonModel: + """Your Python model must use the same class name. Every Python model + that is created must have "TritonPythonModel" as the class name. + """ + + def initialize(self, args): + """`initialize` is called only once when the model is being loaded. + Implementing `initialize` function is optional. This function allows + the model to initialize any state associated with this model. + Parameters + ---------- + args : dict + Both keys and values are strings. The dictionary keys and values are: + * model_config: A JSON string containing the model configuration + * model_instance_kind: A string containing model instance kind + * model_instance_device_id: A string containing model instance + device ID + * model_repository: Model repository path + * model_version: Model version + * model_name: Model name + """ + self.model_config = model_config = json.loads(args['model_config']) + self.max_batch_size = max(model_config["max_batch_size"], 1) + + if "GPU" in model_config["instance_group"][0]["kind"]: + self.device = "cuda" + else: + self.device = "cpu" + + # Get OUTPUT0 configuration + output0_config = pb_utils.get_output_config_by_name( + model_config, "LABELS") + # Convert Triton types to numpy types + self.output0_dtype = pb_utils.triton_string_to_numpy( + output0_config['data_type']) + + self.init_jit_model("/workspace/triton/silero_vad.jit") + + def init_jit_model(self, model_path): + torch.set_grad_enabled(False) + self.sad_model = torch.jit.load(model_path, map_location=self.device) + self.sad_model.eval() + + def prepare_chunks(self, + wav, + audio_length_samples, + sr: int = 16000, + window_size_samples: int = 1536): + chunks = [] + self.sad_model.reset_states() + + for current_start_sample in range(0, audio_length_samples, + window_size_samples): + chunk = wav[current_start_sample:current_start_sample + + window_size_samples] + if len(chunk) < window_size_samples: + chunk = torch.nn.functional.pad( + chunk, (0, int(window_size_samples - len(chunk)))) + speech_prob = self.sad_model(chunk, 16000) + chunks.append(speech_prob) + return chunks + + def get_timestamps(self, + speech_probs, + audio_length_samples, + sr: int = 16000, + threshold: float = 0.5, + min_duration: float = 0.255, + min_speech_duration_ms: int = 250, + min_silence_duration_ms: int = 100, + window_size_samples: int = 1536, + speech_pad_ms: int = 30): + triggered = False + speeches = [] + current_speech = {} + neg_threshold = threshold - 0.15 + temp_end = 0 + + min_speech_samples = sr * min_speech_duration_ms / 1000 + min_silence_samples = sr * min_silence_duration_ms / 1000 + speech_pad_samples = sr * speech_pad_ms / 1000 + + for i, speech_prob in enumerate(speech_probs): + if (speech_prob >= threshold) and temp_end: + temp_end = 0 + + if (speech_prob >= threshold) and not triggered: + triggered = True + current_speech['start'] = window_size_samples * i + continue + + if (speech_prob < neg_threshold) and triggered: + if not temp_end: + temp_end = window_size_samples * i + if (window_size_samples * i) - temp_end < min_silence_samples: + continue + else: + current_speech['end'] = temp_end + if (current_speech['end'] - + current_speech['start']) > min_speech_samples: + speeches.append(current_speech) + temp_end = 0 + current_speech = {} + triggered = False + continue + if current_speech: + current_speech['end'] = audio_length_samples + speeches.append(current_speech) + + for i, speech in enumerate(speeches): + if i == 0: + speech['start'] = int( + max(0, speech['start'] - speech_pad_samples)) + if i != len(speeches) - 1: + silence_duration = speeches[i + 1]['start'] - speech['end'] + if silence_duration < 2 * speech_pad_samples: + speech['end'] += int(silence_duration // 2) + speeches[i + 1]['start'] = int( + max(0, + speeches[i + 1]['start'] - silence_duration // 2)) + else: + speech['end'] += int(speech_pad_samples) + else: + speech['end'] = int( + min(audio_length_samples, + speech['end'] + speech_pad_samples)) + vad_result = [] + for item in speeches: + begin = item['start'] / sr + end = item['end'] / sr + if end - begin >= min_duration: + item['start'] = begin + item['end'] = end + vad_result.append(item) + return vad_result + + def subsegment(self, + wav, + segments, + wav_idx, + window_fs: float = 1.50, + period_fs: float = 0.75, + sr: int = 16000, + frame_shift: int = 10): + + def repeat_to_fill(x, window_fs): + length = x.size(0) + num = (window_fs + length - 1) // length + + x = x.repeat(1, num)[0][:window_fs] + input = torch.zeros((1, window_fs), device=self.device) + input[0] = x + return input + + subsegs = [] + subseg_signals = [] + + seg_idx = 0 + + window_fs = int(window_fs * sr) + period_fs = int(period_fs * sr) + for segment in segments: + seg_begin = int(segment['start'] * sr) + seg_end = int(segment['end'] * sr) + seg_signal = wav[seg_begin:seg_end + 1] + seg_length = seg_end - seg_begin + + if seg_length <= window_fs: + subseg = [ + wav_idx, seg_idx, segment['start'], segment['end'], 0, + int(seg_length / sr * 1000 // frame_shift) + ] + subseg_signal = repeat_to_fill(seg_signal, window_fs) + + subsegs.append(subseg) + subseg_signals.append(subseg_signal) + seg_idx += 1 + else: + max_subseg_begin = seg_length - window_fs + period_fs + for subseg_begin in range(0, max_subseg_begin, period_fs): + subseg_end = min(subseg_begin + window_fs, seg_length) + subseg = [ + wav_idx, seg_idx, segment['start'], segment['end'], + int(subseg_begin / sr * 1000 / frame_shift), + int(subseg_end / sr * 1000 / frame_shift) + ] + subseg_signal = repeat_to_fill( + seg_signal[subseg_begin:subseg_end + 1], window_fs) + + subsegs.append(subseg) + subseg_signals.append(subseg_signal) + seg_idx += 1 + + return subsegs, subseg_signals + + def read_labels(self, subseg_ids, label, frame_shift=10): + utt_to_subseg_labels = [] + new_sort = {} + for i, subseg in enumerate(subseg_ids): + (utt, seg_idx, begin_ms, end_ms, begin_frames, end_frames) = subseg + begin = (int(begin_ms * 1000) + + int(begin_frames) * frame_shift) / 1000.0 + end = (int(begin_ms * 1000) + + int(end_frames) * frame_shift) / 1000.0 + new_sort[seg_idx] = (begin, end, label[i]) + utt_to_subseg_labels = list(dict(sorted(new_sort.items())).values()) + return utt_to_subseg_labels + + def merge_segments(self, subseg_to_labels): + merged_segment_to_labels = [] + + if len(subseg_to_labels) == 0: + return merged_segment_to_labels + + (begin, end, label) = subseg_to_labels[0] + for (b, e, la) in subseg_to_labels[1:]: + if b <= end and la == label: + end = e + elif b > end: + merged_segment_to_labels.append((begin, end, label)) + begin, end, label = b, e, la + elif b <= end and la != label: + pivot = (b + end) / 2.0 + merged_segment_to_labels.append((begin, pivot, label)) + begin, end, label = pivot, e, la + else: + raise ValueError + merged_segment_to_labels.append((begin, e, label)) + + return merged_segment_to_labels + + async def execute(self, requests): + """`execute` must be implemented in every Python model. `execute` + function receives a list of pb_utils.InferenceRequest as the only + argument. This function is called when an inference is requested + for this model. + Parameters + ---------- + requests : list + A list of pb_utils.InferenceRequest + Returns + ------- + list + A list of pb_utils.InferenceResponse. The length of this list must + be the same as `requests` + """ + + batch_count = [] + batch_len = [] + + total_wavs = [] + total_lens = [] + responses = [] + + for request in requests: + input0 = pb_utils.get_input_tensor_by_name(request, "input") + + cur_b_wav = from_dlpack(input0.to_dlpack()) + cur_batch = cur_b_wav.shape[0] + cur_len = cur_b_wav.shape[1] + batch_count.append(cur_batch) + batch_len.append(cur_len) + + for wav in cur_b_wav: + total_lens.append(len(wav)) + total_wavs.append(wav.to(self.device)) + + speech_shapes = [] + all_probs = [] + + for wav, lens in zip(total_wavs, total_lens): + chunks = self.prepare_chunks(wav, lens) + speech_shapes.append(len(chunks)) + all_probs.append(chunks) + reshape_probs = [] + idx = 0 + for i in range(0, len(speech_shapes)): + cur_speech = [] + for j in range(0, speech_shapes[i]): + cur_speech.append(all_probs[i][j]) + idx += 1 + reshape_probs.append(cur_speech) + + out_segs = [] + for speech_prob, speech_len in zip(reshape_probs, total_lens): + segments = self.get_timestamps(speech_prob, + speech_len, + threshold=0.36) + out_segs.append(segments) + + total_subsegments = [] + total_subsegment_ids = [] + total_embds = [] + + wav_idx = 0 + for waveform, segments in zip(total_wavs, out_segs): + subsegs, subseg_signals = self.subsegment(waveform, segments, + wav_idx) + total_subsegments.extend(subseg_signals) + total_subsegment_ids.extend(subsegs) + wav_idx += 1 + + inference_response_awaits = [] + for wavs in total_subsegments: + input_tensor_spk0 = pb_utils.Tensor.from_dlpack( + "WAV", to_dlpack(wavs)) + + input_tensors_spk = [input_tensor_spk0] + inference_request = pb_utils.InferenceRequest( + model_name='speaker', + requested_output_names=['EMBEDDINGS'], + inputs=input_tensors_spk) + inference_response_awaits.append(inference_request.async_exec()) + + inference_responses = await asyncio.gather(*inference_response_awaits) + + for inference_response in inference_responses: + if inference_response.has_error(): + raise pb_utils.TritonModelException( + inference_response.error().message()) + else: + batched_result = pb_utils.get_output_tensor_by_name( + inference_response, 'EMBEDDINGS') + total_embds.extend(from_dlpack(batched_result.to_dlpack())) + + out_embds = list() + out_time_info = list() + for i in range(0, len(total_wavs)): + out_embds.append(list()) + out_time_info.append(list()) + + for subseg_idx, embds in zip(total_subsegment_ids, total_embds): + wav_idx = subseg_idx[0] + out_embds[wav_idx].append(embds) + out_time_info[wav_idx].append(subseg_idx) + + # Begin clustering + inference_response_awaits = [] + for i, embd in enumerate(out_embds): + embd = torch.stack(embd) + input_tensor_embds0 = pb_utils.Tensor.from_dlpack( + "EMBEDDINGS", to_dlpack(torch.unsqueeze(embd, 0))) + + input_tensors_spk = [input_tensor_embds0] + inference_request = pb_utils.InferenceRequest( + model_name='clusterer', + requested_output_names=['LABELS'], + request_id=str(i), + inputs=input_tensors_spk) + inference_response_awaits.append(inference_request.async_exec()) + + inference_responses = await asyncio.gather(*inference_response_awaits) + + i = 0 + results = [] + for inference_response in inference_responses: + if inference_response.has_error(): + raise pb_utils.TritonModelException( + inference_response.error().message()) + else: + result = pb_utils.get_output_tensor_by_name( + inference_response, 'LABELS').as_numpy()[0] + utt_to_subseg_labels = self.read_labels( + out_time_info[i], result) + i += 1 + rttm = self.merge_segments(utt_to_subseg_labels) + if len(rttm) > 0: + results.append(rttm) + + # Return the batched resoponse + st = 0 + for b in batch_count: + sents = np.array(results[st:st + b]) + out0 = pb_utils.Tensor("LABELS", sents.astype(self.output0_dtype)) + inference_response = pb_utils.InferenceResponse( + output_tensors=[out0]) + responses.append(inference_response) + st += b + return responses diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/run/config.pbtxt b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/run/config.pbtxt new file mode 100644 index 0000000000000000000000000000000000000000..5a51c6edfd767e33fa6ea66e86ab300e0e5a5d1d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/diarization_gpu/model_repo/run/config.pbtxt @@ -0,0 +1,43 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "run" +backend: "python" +max_batch_size: 128 + +input [ + { + name: "input" + data_type: TYPE_FP32 + dims: [ -1 ] + } +] + +output [ + { + name: "LABELS" + data_type: TYPE_FP32 + dims: [ -1, 3 ] + } +] + +dynamic_batching { + preferred_batch_size: [ 16, 32 ] + } +instance_group [ + { + count: 2 + kind: KIND_GPU + } +] diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/Dockerfile/dockerfile.client b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/Dockerfile/dockerfile.client new file mode 100644 index 0000000000000000000000000000000000000000..80ffe413240ea7e4bebe0e8335e0f9da9f0a9c16 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/Dockerfile/dockerfile.client @@ -0,0 +1,33 @@ +################################################################################################### +# +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials +# provided with the distribution. +# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used +# to endorse or promote products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +################################################################################################### + +FROM nvcr.io/nvidia/tritonserver:22.03-py3-sdk +LABEL maintainer="NVIDIA" +LABEL repository="tritonserver" + +RUN apt-get update && apt-get install -y libsndfile1 +RUN pip3 install soundfile kaldiio +WORKDIR /workspace \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/Dockerfile/dockerfile.server b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/Dockerfile/dockerfile.server new file mode 100644 index 0000000000000000000000000000000000000000..0beabb836dcb038bb71736db284da99344f32c55 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/Dockerfile/dockerfile.server @@ -0,0 +1,33 @@ +################################################################################################### +# +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials +# provided with the distribution. +# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used +# to endorse or promote products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +################################################################################################### +FROM nvcr.io/nvidia/tritonserver:22.03-py3 +LABEL maintainer="NVIDIA" +LABEL repository="tritonserver" + +RUN apt-get update && apt-get -y install swig && apt-get -y install python3-dev && apt-get install -y cmake +RUN pip3 install torch==1.10.0+cu113 torchvision==0.11.1+cu113 torchaudio==0.10.0+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html +RUN pip3 install -v kaldifeat +WORKDIR /workspace \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/README.md b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/README.md new file mode 100644 index 0000000000000000000000000000000000000000..098aa5e22598e3c52924c17e8041d8ebea6d355c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/README.md @@ -0,0 +1,161 @@ +# Introduction +In this project, we use models trained in [wespeaker](https://github.com/wenet-e2e/wespeaker) as an example to show how to convert speaker model to tensorrt and deploy them on [Triton Inference Server](https://github.com/triton-inference-server/server.git). If you only have CPUs, instead of using GPUs to deploy Tensorrt model, you may deploy the exported onnx model on Triton Inference Server as well. + +## Step 0. Train a model +Please follow wespeaker examples to train a model. After training, you should get several checkpoints under your `exp/xxx/models/` folder. We take [voxceleb](https://github.com/wenet-e2e/wespeaker/tree/master/examples/voxceleb/v2) as an example. + +## Step 1. Export model +We'll first export our model to onnx and then convert our onnx model to tensorrt. +``` +# go to your example +cd wespeaker/examples/voxceleb/v2 +. ./path.sh +exp_dir=exp/resnet +python3 wespeaker/bin/export_onnx.py --config=${exp_dir}/config.yaml --checkpoint=${exp_dir}/models/avg_model.pt --output_model=${exp_dir}/models/avg_model.onnx + +# If you want to minus the mean vector in the onnx model, you may simply add the --mean_vec to the .npy mean vector file. +python3 wespeaker/bin/export_onnx.py --config=${exp_dir}/config.yaml --checkpoint=exp/resnet/models/avg_model.pt --output_model=exp/resnet/models/avg_model.onnx --mean_vec=${exp_dir}/embeddings/vox2_dev/mean_vec.npy +``` + +If you only want to deploy the onnx model on CPU or GPU, you may skip the Tensorrt part and go to [the section](#construct-model-repo) to construct your model repository. + +### Export to Tensorrt Engine +Now let's convert our onnx model to tensorrt engine. We will deploy our model on Triton 22.03 therefore we here will use tensorrt 22.03 docker as an example to show how to convert the model. Please move your onnx model to the target platform/GPU you will deploy. + +``` +docker run --gpus '"device=0"' -it -v :/models nvcr.io/nvidia/tensorrt:22.03-py3 +cd /models/ +# shape=BxTxF batchsize, sequence_length, feature_size +trtexec --saveEngine=b1_b128_s3000_fp16.trt --onnx=/models/avg_model.onnx --minShapes=feats:1x200x80 --optShapes=feats:64x200x80 --maxShapes=feats:128x3000x80 --fp16 +``` +Here we get an engine which has maximum sequence length of 3000 and minimum length of 200. Since the frame stride is 10ms, 200 and 3000 corresponds to 2.02 seconds and 30.02 seconds respectively(kaldi feature extractor). Notice these numbers will differ and depend on your feature extractor parameters. Notice we've added `--fp16` and in pratice, we found this option will not affect the final accuracy and improve the perf at the same time. + +You may set these numbers by your production requirements. If you only know the seconds of audio you will use and have no idea of how many frames it will generate, you may try the below script: +```python +import torchaudio.compliance.kaldi as kaldi +import torch +audio_dur_in_seconds = 2 +feat_dim = 80 # please check config.yaml if you dont know +sample_rate = 16000 + +waveform = torch.ones(sample_rate * audio_dur_in_seconds).unsqueeze(0) +feat_tensor = kaldi.fbank(waveform, + num_mel_bins=feat_dim, + frame_shift=10, + frame_length=25, + energy_floor=0.0, + window_type='hamming', + htk_compat=True, + use_energy=False, + dither=1) +print(feat_tensor.shape) # (198, 80) +``` +Then you may find `198` is the actual number of frames for audio of 2 seconds long. + +That's it!We build an engine that can accept 2.02 to 30.02 seconds long audio. If your application can accept fixed audio segments, we suggest you to set the `minShapes`, `optShapes` and `maxShapes` to the same shape. + +### Construct Model Repo + +Now edit the config file under `model_repo/speaker_model/config.pbtxt` and replace `default_model_filename:xxx` with the name of your engine (e.g., `b1_b128_s3000_fp16.trt`) or onnx model (e.g., `avg_model.onnx`) and put the engine or model under `model_repo/speaker_model/1/`. + +And if you use other model settings or different model from ours (resnet34), for example, ecapa model, the embedding dim of which is 192, therefore, you should edit the `model_repo/speaker_model/config.pbtxt` and `model_repo/speaker/config.pbtxt` and set embedding dim to 192. + +If your model is onnx model, you should also edit `backend: "tensorrt"` to `backend: "onnxruntime"` in `model_repo/speaker_model/config.pbtxt`. + +If you want to deploy model on CPUs, you should edit `config.pbtxt` under `speaker` and `speaker_model` and replace `kind: KIND_GPU` to `kind: KIND_CPU`. + +Notice Tensorrt engine can only run on GPUs. + +## Step 2. Build server and start server + +Notice we use triton 22.03 in dockerfile. Be sure to use the triton that has the same version as your tensorrt. + +Build server: +``` +# server +docker build . -f Dockerfile/dockerfile.server -t wespeaker:latest --network host +``` + +``` +docker run --gpus '"device=0"' -v $PWD/model_repo:/ws/model_repo --shm-size=1g --ulimit memlock=-1 -p 8000:8000 -p 8001:8001 -p 8002:8002 --ulimit stack=67108864 -ti wespeaker:latest +tritonserver --model-repository=/ws/model_repo +``` +The port `8000` is for http request and `8001` for grpc request. + +## Step 3. Build client and start client + +Build client: + +``` +# client +docker build . -f Dockerfile/dockerfile.client -t wespeaker_client:latest --network host +``` + +``` +docker run -it -v $PWD:/ws -v : --network=host wespeaker_client + +# example command +cd /ws/client/ +python3 client.py --url=:8001 --wavscp=/raid/dgxsa/slyne/wespeaker/examples/voxceleb/v2/data/vox1/wav.scp --output_directory= + +# The output direcotry will be something like: +# xvector_000.ark xvextor_000.scp xvector_001.scp ..... + +``` + +## Step 4. Test score +After you extract the embeddings, you can now use the same way as wespeaker to test these embeddings. For example, you may test the extracted embeddings in wespeaker by: +``` +cat embeddings/xvector_*.scp > embeddings/xvector.scp + +config=conf/resnet.yaml +exp_dir=exp/resnet + +mkdir -p embeddings/scores +trials_dir=data/vox1/trials +python -u wespeaker/bin/score.py \ + --exp_dir ${exp_dir} \ + --eval_scp_path /raid/dgxsa/slyne/wespeaker/runtime/server/x86_gpu/embeddings/xvector.scp \ # embeddings generated from our server + --cal_mean True \ + --cal_mean_dir ${exp_dir}/embeddings/vox2_dev \ + --p_target 0.01 \ + --c_miss 1 \ + --c_fa 1 \ + ${trials_dir}/vox1_O_cleaned.kaldi ${trials_dir}/vox1_E_cleaned.kaldi ${trials_dir}/vox1_H_cleaned.kaldi \ + 2>&1 | tee /raid/dgxsa/slyne/wespeaker/runtime/server/x86_gpu/embeddings/scores/vox1_cos_result +``` + +# Perf + +We build our engines for 2.02 seconds long audio only by: +``` +trtexec --saveEngine=resnet_b1_b128_s200_fp16.trt --onnx=resnet/resnet_avg_model.onnx --minShapes=feats:1x200x80 --optShapes=feats:64x200x80 --maxShapes=feats:128x200x80 --fp16 + +trtexec --saveEngine=ecapa_b1_b128_s200_fp16.trt --onnx=ecapa/ecapa_avg_model.onnx --minShapes=feats:1x200x80 --optShapes=feats:64x200x80 --maxShapes=feats:128x200x80 --fp16 +``` + +* GPU: T4 +* resnet: resnet34. + +|Engine |Throughput (bz=64)| utter/s| +|------------------------------------|------------------|--------| +|resnet_b1_b128_s200_fp16.trt |39.7842 |2546 | +|ecapa_b1_b128_s200_fp16.trt |52.958 |3389 | + +### Pipeline Perf + +In client docker, we may test the whole pipeline performance. +``` +cd client/ +# generate test input +python3 generate_input.py --audio_file=test.wav --seconds=2.02 + +perf_analyzer -m speaker -b 1 --concurrency-range 200:1000:200 --input-data=input.json -u localhost:8000 +``` + +|Engine | Conccurency | Throughput | Avg Latency(ms) | P99 Latency(ms) | +|----------------------------------|-------------|------------|-----------------|-----------------| +|resnet_b1_b128_s200_fp16.trt | 200 | 2033 | 98 | 111 | +| | 400 | 2010 | 198 | 208 | +|ecapa_b1_b128_s200_fp16.trt | 200 | 2647 | 75 | 111 | +| | 400 | 2726 | 147 | 172 | \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/client/client.py b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/client/client.py new file mode 100644 index 0000000000000000000000000000000000000000..11a13c583422f5301ee72eb19d7909251cd0e9e6 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/client/client.py @@ -0,0 +1,157 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import multiprocessing +from multiprocessing import Pool + +import tritonclient.grpc as grpcclient +from tritonclient.utils import * +import numpy as np +import soundfile +import argparse +import os +import kaldiio + + +class SpeakerClient(object): + + def __init__(self, triton_client, model_name, protocol_client): + self.triton_client = triton_client + self.protocol_client = protocol_client + self.model_name = model_name + + def recognize(self, wav_path, client_index): + # We send batchsize=1 data to server + # BatchSize > 1 is also ok but you need to take care of + # padding. + waveform, sample_rate = soundfile.read(wav_path) + # trim audio that is longer than 30 seconds + # there are many audio samples in vox1 longer than 30s + # we found the EER will not grow a lot when trimming the audio to 30s + max_length = 30 * 16000 # 30 * 16000 + cur_length = min(max_length, len(waveform)) + if len(waveform) > cur_length: + print(len(waveform) // 16000) + input = np.zeros((1, cur_length), dtype=np.float32) + input[0][0:cur_length] = waveform[0:cur_length] + inputs = [ + self.protocol_client.InferInput("WAV", input.shape, + np_to_triton_dtype(input.dtype)) + ] + inputs[0].set_data_from_numpy(input) + outputs = [grpcclient.InferRequestedOutput("EMBEDDINGS")] + response = self.triton_client.infer(self.model_name, + inputs, + request_id=str(client_index), + outputs=outputs) + return response.as_numpy("EMBEDDINGS")[0] + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-v', + '--verbose', + action="store_true", + required=False, + default=False, + help='Enable verbose output') + parser.add_argument('-u', + '--url', + type=str, + required=False, + default='localhost:8001', + help='Inference server URL. Default is ' + 'localhost:8001.') + parser.add_argument('--model_name', + required=False, + default='speaker', + help='the model to send request to') + parser.add_argument('--wavscp', + type=str, + required=False, + default=None, + help='audio_id \t absolute_wav_path') + parser.add_argument('--output_directory', + type=str, + required=False, + default=None, + help='audio_id \t text') + parser.add_argument('--data_dir', + type=str, + required=False, + default=None, + help='data dir will be append to audio file if given') + parser.add_argument('--audio_file', + type=str, + required=False, + default=None, + help='single wav file') + + FLAGS = parser.parse_args() + + # load data + audio_wavpath = [] + if FLAGS.audio_file is not None: + path = FLAGS.audio_file + if FLAGS.data_dir: + path = os.path.join(FLAGS.data_dir, path) + if os.path.exists(path): + audio_wavpath = [(FLAGS.audio_file, path)] + elif FLAGS.wavscp is not None: + with open(FLAGS.wavscp, "r", encoding="utf-8") as f: + for line in f: + aid, path = line.strip().split() + if FLAGS.data_dir: + path = os.path.join(FLAGS.data_dir, path) + audio_wavpath.append((aid, path)) + + num_workers = multiprocessing.cpu_count() // 2 + + def single_job(li): + idx, audio_files = li + dir_name = os.path.dirname(FLAGS.output_directory) # get the path + if not os.path.exists(dir_name) and (dir_name != ''): + os.makedirs(dir_name) + + embed_ark = os.path.abspath(dir_name) + "/xvector_{:0>3d}.ark".format( + idx) + embed_scp = embed_ark[:-3] + "scp" + + with grpcclient.InferenceServerClient( + url=FLAGS.url, verbose=FLAGS.verbose) as triton_client: + protocol_client = grpcclient + speech_client = SpeakerClient(triton_client, FLAGS.model_name, + protocol_client) + + with kaldiio.WriteHelper('ark,scp:' + embed_ark + "," + + embed_scp) as writer: + for li in audio_files: + utt, wavpath = li + embed = speech_client.recognize(wavpath, idx) + writer(utt, embed) + + return predictions + + # start to do inference + # Group requests in batches + predictions = [] + tasks = [] + splits = np.array_split(audio_wavpath, num_workers) + + for idx, per_split in enumerate(splits): + cur_files = per_split.tolist() + tasks.append((idx, cur_files)) + + with Pool(processes=num_workers) as pool: + predictions = pool.map(single_job, tasks) diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/client/generate_input.py b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/client/generate_input.py new file mode 100644 index 0000000000000000000000000000000000000000..07dee2083603678af2081d512be6b092d7e06647 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/client/generate_input.py @@ -0,0 +1,42 @@ +import numpy as np +import soundfile +import argparse +import json + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--audio_file', + type=str, + default=None, + help='single wav file') + parser.add_argument( + '--seconds', + type=float, + required=False, + default=None, + help='how long of the audio will be used as test sample') + + FLAGS = parser.parse_args() + wav_file = FLAGS.audio_file + waveform, sample_rate = soundfile.read(wav_file) + true_length = len(waveform) // sample_rate + if FLAGS.seconds: + num_samples = int(FLAGS.second * sample_rate) + seconds = FLAGS.seconds + if seconds < true_length: + waveform = waveform[0:num_samples] + else: + temp = np.zeros(num_samples, dtype=np.float32) + temp[0:len(waveform)] = waveform[:] + waveform = temp + + data = { + "data": [{ + "WAV": { + "shape": [len(waveform)], + "content": waveform.tolist() + } + }] + } + + json.dump(data, open("input.json", "w")) diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/feature_extractor/1/model.py b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/feature_extractor/1/model.py new file mode 100644 index 0000000000000000000000000000000000000000..4f9e74a1a1869bcd0ea4a7776e9e56cf8dffe3ba --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/feature_extractor/1/model.py @@ -0,0 +1,134 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import triton_python_backend_utils as pb_utils +from torch.utils.dlpack import to_dlpack, from_dlpack +import torch +import kaldifeat +from typing import List +import json + + +class Fbank(torch.nn.Module): + + def __init__(self, opts): + super(Fbank, self).__init__() + self.fbank = kaldifeat.Fbank(opts) + + def forward(self, waves: List[torch.Tensor]): + feats = self.fbank(waves) + B, T, F = len(feats), feats[0].size(0), feats[0].size(1) + feats = torch.cat(feats, axis=0) + feats = torch.reshape(feats, (B, T, F)) + feats = feats - torch.mean(feats, dim=1, keepdim=True) + return feats + + +class TritonPythonModel: + """Your Python model must use the same class name. Every Python model + that is created must have "TritonPythonModel" as the class name. + """ + + def initialize(self, args): + """`initialize` is called only once when the model is being loaded. + Implementing `initialize` function is optional. This function allows + the model to initialize any state associated with this model. + + Parameters + ---------- + args : dict + Both keys and values are strings. The dictionary keys and values are: + * model_config: A JSON string containing the model configuration + * model_instance_kind: A string containing model instance kind + * model_instance_device_id: A string containing model instance + * device ID + * model_repository: Model repository path + * model_version: Model version + * model_name: Model name + """ + self.model_config = model_config = json.loads(args['model_config']) + self.max_batch_size = max(model_config["max_batch_size"], 1) + + if "GPU" in model_config["instance_group"][0]["kind"]: + self.device = "cuda" + else: + self.device = "cpu" + + params = self.model_config['parameters'] + opts = kaldifeat.FbankOptions() + opts.frame_opts.window_type = 'hamming' + opts.frame_opts.dither = 1 # 0 -> 1 + opts.htk_compat = True + + for li in params.items(): + key, value = li + value = value["string_value"] + if key == "num_mel_bins": + opts.mel_opts.num_bins = int(value) + elif key == "frame_shift_in_ms": + opts.frame_opts.frame_shift_ms = float(value) + elif key == "frame_length_in_ms": + opts.frame_opts.frame_length_ms = float(value) + elif key == "sample_rate": + opts.frame_opts.samp_freq = int(value) + opts.device = torch.device(self.device) + self.opts = opts + self.feature_extractor = Fbank(self.opts) + self.feature_size = opts.mel_opts.num_bins + + def execute(self, requests): + """`execute` must be implemented in every Python model. `execute` + function receives a list of pb_utils.InferenceRequest as the only + argument. This function is called when an inference is requested + for this model. + + Parameters + ---------- + requests : list + A list of pb_utils.InferenceRequest + + Returns + ------- + list + A list of pb_utils.InferenceResponse. + The length of this list must be the same as `requests` + """ + batch_count = [] + total_waves = [] + + responses = [] + for request in requests: + # the requests will all have the same shape + # different shape request will be + # separated by triton inference server + input0 = pb_utils.get_input_tensor_by_name(request, "wav") + cur_b_wav = from_dlpack(input0.to_dlpack()) + cur_b_wav = cur_b_wav * (1 << 15) # b x -1 + cur_batch = cur_b_wav.shape[0] + batch_count.append(cur_batch) + + for wav in cur_b_wav: + total_waves.append(wav.to(self.device)) + + features = self.feature_extractor(total_waves).cpu() + idx = 0 + for b in batch_count: + batch_speech = features[idx:idx + b] + idx += b + out0 = pb_utils.Tensor.from_dlpack("speech", + to_dlpack(batch_speech)) + inference_response = pb_utils.InferenceResponse( + output_tensors=[out0]) + responses.append(inference_response) + return responses diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/feature_extractor/config.pbtxt b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/feature_extractor/config.pbtxt new file mode 100644 index 0000000000000000000000000000000000000000..32bff85705f703d1e8ea06310dc4c658da4e87fa --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/feature_extractor/config.pbtxt @@ -0,0 +1,63 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "feature_extractor" +backend: "python" +max_batch_size: 128 + +parameters [ + { + key: "num_mel_bins", + value: { string_value: "80"} + }, + { + key: "frame_shift_in_ms" + value: { string_value: "10"} + }, + { + key: "frame_length_in_ms" + value: { string_value: "25"} + }, + { + key: "sample_rate" + value: { string_value: "16000"} + } + +] + +input [ + { + name: "wav" + data_type: TYPE_FP32 + dims: [ -1 ] + } +] + +output [ + { + name: "speech" + data_type: TYPE_FP32 + dims: [ -1, 80 ] # 80 + } +] + +dynamic_batching { + preferred_batch_size: [ 16, 32 ] + } +instance_group [ + { + count: 2 + kind: KIND_GPU + } +] \ No newline at end of file diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker/1/no_model_here b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker/1/no_model_here new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker/config.pbtxt b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker/config.pbtxt new file mode 100644 index 0000000000000000000000000000000000000000..571c52dca5c604d9c945c498baccb4ee944d08f3 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker/config.pbtxt @@ -0,0 +1,62 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "speaker" +platform: "ensemble" +max_batch_size: 128 #MAX_BATCH + +input [ + { + name: "WAV" + data_type: TYPE_FP32 + dims: [-1] + } +] + +output [ + { + name: "EMBEDDINGS" + data_type: TYPE_FP32 + dims: [256] # embedding dim + } +] + +ensemble_scheduling { + step [ + { + model_name: "feature_extractor" + model_version: -1 + input_map { + key: "wav" + value: "WAV" + } + output_map { + key: "speech" + value: "SPEECH" + } + }, + { + model_name: "speaker_model" + model_version: -1 + input_map { + key: "feats" + value: "SPEECH" + } + output_map { + key: "embs" + value: "EMBEDDINGS" + } + } + ] +} diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker_model/1/add_model_here b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker_model/1/add_model_here new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker_model/config.pbtxt b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker_model/config.pbtxt new file mode 100644 index 0000000000000000000000000000000000000000..ec27d499683c9baff5cbea5dd2a7826f405f8ff4 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/runtime/server/x86_gpu/model_repo/speaker_model/config.pbtxt @@ -0,0 +1,43 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "speaker_model" +backend: "tensorrt" +default_model_filename: "b1_b128_s3000_fp16.trt" + +max_batch_size: 128 +input [ + { + name: "feats" + data_type: TYPE_FP32 + dims: [ -1, 80 ] # num_mel_bins + } +] + +output [ + { + name: "embs" + data_type: TYPE_FP32 + dims: [ 256 ] # [embedding_size] + } +] +dynamic_batching { + preferred_batch_size: [ 16, 32 ] + } +instance_group [ + { + count: 1 + kind: KIND_GPU + } +] diff --git a/MOSS_Speaker-RoPE/wespeaker/setup.py b/MOSS_Speaker-RoPE/wespeaker/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..9d554cc18b90bfa307e0533998eae4387c1d5acc --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/setup.py @@ -0,0 +1,62 @@ +from pathlib import Path + +from setuptools import find_packages, setup + +setup( + name="wespeaker", + version="0.0.1", + description=("WeSpeaker: A Research and Production Oriented " + "Speaker Embedding Learning Toolkit"), + long_description=Path("README.md").read_text(encoding="utf-8"), + long_description_content_type="text/markdown", + license="Apache-2.0", + python_requires=">=3.8", + author="WeSpeaker Contributors", + keywords=["speaker", "embedding", "verification", "diarization", "speech"], + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Multimedia :: Sound/Audio :: Speech", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + ], + install_requires=[ + "tqdm", + "kaldiio", + "hdbscan>=0.8.40", + "umap-learn==0.5.6", + "torch>=2.0.0", + "torchaudio>=2.0.0", + "silero-vad", + "s3prl", + "openai-whisper", + "peft", + "accelerate", + ], + extras_require={ + "dev": [ + "flake8", + "pre-commit", + ], + }, + project_urls={ + "Homepage": "https://github.com/wenet-e2e/wespeaker", + "Documentation": "http://wenet.org.cn/wespeaker", + "Repository": "https://github.com/wenet-e2e/wespeaker", + "Issues": "https://github.com/wenet-e2e/wespeaker/issues", + }, + entry_points={ + "console_scripts": [ + "wespeaker=wespeaker.cli.speaker:main", + ], + }, + packages=find_packages(include=["wespeaker*"]), + include_package_data=True, +) diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/combine_data.sh b/MOSS_Speaker-RoPE/wespeaker/tools/combine_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..12e2e3cd7b489ca9d2969781e28a911c5846c7b8 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/combine_data.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# Copyright 2012 Johns Hopkins University (Author: Daniel Povey). Apache 2.0. +# 2014 David Snyder + +# This script combines the data from multiple source directories into +# a single destination directory. + +# See http://kaldi-asr.org/doc/data_prep.html#data_prep_data for information +# about what these directories contain. + +# Begin configuration section. +extra_files= # specify additional files in 'src-data-dir' to merge, ex. "file1 file2 ..." +skip_fix=false # skip the fix_data_dir.sh in the end +# End configuration section. + +echo "$0 $@" # Print the command line for logging + +if [ -f path.sh ]; then . ./path.sh; fi +. tools/parse_options.sh || exit 1; + +if [ $# -lt 2 ]; then + echo "Usage: combine_data.sh [--extra-files 'file1 file2'] ..." + echo "Note, files that don't appear in all source dirs will not be combined," + echo "with the exception of utt2uniq and segments, which are created where necessary." + exit 1 +fi + +dest=$1; +shift; + +first_src=$1; + +rm -r $dest 2>/dev/null || true +mkdir -p $dest; + +export LC_ALL=C + +for dir in $*; do + if [ ! -f $dir/utt2spk ]; then + echo "$0: no such file $dir/utt2spk" + exit 1; + fi +done + +# Check that frame_shift are compatible, where present together with features. +dir_with_frame_shift= +for dir in $*; do + if [[ -f $dir/feats.scp && -f $dir/frame_shift ]]; then + if [[ $dir_with_frame_shift ]] && + ! cmp -s $dir_with_frame_shift/frame_shift $dir/frame_shift; then + echo "$0:error: different frame_shift in directories $dir and " \ + "$dir_with_frame_shift. Cannot combine features." + exit 1; + fi + dir_with_frame_shift=$dir + fi +done + +# W.r.t. utt2uniq file the script has different behavior compared to other files +# it is not compulsary for it to exist in src directories, but if it exists in +# even one it should exist in all. We will create the files where necessary +has_utt2uniq=false +for in_dir in $*; do + if [ -f $in_dir/utt2uniq ]; then + has_utt2uniq=true + break + fi +done + +if $has_utt2uniq; then + # we are going to create an utt2uniq file in the destdir + for in_dir in $*; do + if [ ! -f $in_dir/utt2uniq ]; then + # we assume that utt2uniq is a one to one mapping + cat $in_dir/utt2spk | awk '{printf("%s %s\n", $1, $1);}' + else + cat $in_dir/utt2uniq + fi + done | sort -k1 > $dest/utt2uniq + echo "$0: combined utt2uniq" +else + echo "$0 [info]: not combining utt2uniq as it does not exist" +fi +# some of the old scripts might provide utt2uniq as an extrafile, so just remove it +extra_files=$(echo "$extra_files"|sed -e "s/utt2uniq//g") + +# segments are treated similarly to utt2uniq. If it exists in some, but not all +# src directories, then we generate segments where necessary. +has_segments=false +for in_dir in $*; do + if [ -f $in_dir/segments ]; then + has_segments=true + break + fi +done + +if $has_segments; then + for in_dir in $*; do + if [ ! -f $in_dir/segments ]; then + echo "$0 [info]: will generate missing segments for $in_dir" 1>&2 + tools/data/get_segments_for_data.sh $in_dir + else + cat $in_dir/segments + fi + done | sort -k1 > $dest/segments + echo "$0: combined segments" +else + echo "$0 [info]: not combining segments as it does not exist" +fi + +for file in utt2spk utt2lang utt2dur utt2num_frames reco2dur feats.scp text cmvn.scp vad.scp reco2file_and_channel wav.scp spk2gender $extra_files; do + exists_somewhere=false + absent_somewhere=false + for d in $*; do + if [ -f $d/$file ]; then + exists_somewhere=true + else + absent_somewhere=true + fi + done + + if ! $absent_somewhere; then + set -o pipefail + ( for f in $*; do cat $f/$file; done ) | sort -k1 > $dest/$file || exit 1; + set +o pipefail + echo "$0: combined $file" + else + if ! $exists_somewhere; then + echo "$0 [info]: not combining $file as it does not exist" + else + echo "$0 [info]: **not combining $file as it does not exist everywhere**" + fi + fi +done + +tools/utt2spk_to_spk2utt.pl <$dest/utt2spk >$dest/spk2utt + +if [[ $dir_with_frame_shift ]]; then + cp $dir_with_frame_shift/frame_shift $dest +fi + +if ! $skip_fix ; then + tools/fix_data_dir.sh $dest || exit 1; +fi + +exit 0 diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/copy_data_dir.sh b/MOSS_Speaker-RoPE/wespeaker/tools/copy_data_dir.sh new file mode 100644 index 0000000000000000000000000000000000000000..c4cd4db6400c79d91d3d44303bd164827caac721 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/copy_data_dir.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash + +# Copyright 2023 Brno University of Techology (author: Johan Rohdin) +# Apache 2.0 + +# Copies wav.scp as well as utt2spk spk2utt if they are available. The script +# can also take a list of speakers or utterances to keep. If provided, only +# utterances/speakers in the list are kept. + +src_dir=$1 +dest_dir=$2 + +shift 2 + +update_wav_path=false +utt_list="" +spk_list="" + +. tools/parse_options.sh || exit 1 + +if [ "$dest_dir" == "$src_dir" ]; then + echo "$0 ERROR: Input directory () and output directory () are the same." + exit 1 +fi + +mkdir -p $dest_dir + + +if [ ! -z "$utt_list" ]; then + echo "UTTLIST" +fi +if [ ! -z "$spk_list" ]; then + echo "SPKLIST" +fi + + + + +#if [ $utt_list != "" ] && [ $spk_list != "" ]; then +if [ ! -z "$utt_list" ] && [ ! -z "$spk_list" ]; then + echo "$0 ERROR: Providing both utt_list and spk_list not supported." + exit 1 +fi + + + +if [ ! -f $src_dir/utt2spk ]; then + echo "$0 WARNING: copy_data_dir.sh: no such file $src_dir/utt2spk" +else + if [ ! -z "$utt_list" ];then + awk 'NR==FNR{a[$1];next}$1 in a{print $0}' $utt_list $src_dir/utt2spk > $dest_dir/utt2spk + elif [ ! -z "$spk_list" ];then + #echo "A" + awk 'NR==FNR{a[$1];next}$2 in a{print $0}' $spk_list $src_dir/utt2spk > $dest_dir/utt2spk + else + cp $src_dir/utt2spk $dest_dir/utt2spk + fi +fi + + +if [ ! -f $src_dir/spk2utt ]; then + echo "$0 WARNING: copy_data_dir.sh: no such file $src_dir/spk2utt" +else + if [ ! -z "$utt_list" ];then + # This will work even if utt2spk doesn't exist and was simpler than reducing spk2utt directly. + cat $scrdir/spk2utt | tools/spk2utt_to_utt2spk.pl \ + | awk 'NR==FNR{a[$1];next}$1 in a{print $0}' $utt_list - \ + | tools/utt2spk_to_spk2utt.pl > $dest_dir/spk2utt + + elif [ ! -z "$spk_list" ];then + awk 'NR==FNR{a[$1];next}$1 in a{print $0}' $spk_list $src_dir/spk2utt > $dest_dir/spk2utt + else + cp $src_dir/spk2utt $dest_dir/spk2utt + fi +fi + + +if [ ! -f $src_dir/wav.scp ]; then + echo "$0 ERROR: copy_data_dir.sh: no such file $src_dir/wav.scp" + exit 1; +else + if [ $update_wav_path == true ];then + src_root_dir=$(readlink -f $src_dir | sed "s:data/.*::") + dest_root_dir=$(readlink -f $dest_dir | sed "s:data/.*::") + cat $src_dir/wav.scp | sed "s:$src_root_dir:$dest_root_dir:" > $dest_dir/wav.scp + else + cp $src_dir/wav.scp $dest_dir/wav.scp + fi +fi + + +# Sanity checks +if [ -f $dest_dir/utt2spk ];then + if [ $( wc -l $dest_dir/utt2spk | cut -f1 -d" ") -ne $( wc -l $dest_dir/wav.scp | cut -f1 -d" " ) ];then + echo "ERROR: Length of utt2spk and wav.scp doesn't match." + exit 1 + fi + if [ -f $src_dir/spk2utt ]; then + if [ $( cat $dest_dir/utt2spk | sort | md5sum | cut -f1 -d" " ) != $( tools/spk2utt_to_utt2spk.pl $dest_dir/spk2utt | sort | md5sum | cut -f1 -d" " ) ];then + echo "ERROR: utt2spk and spk2utt doesn't match." + exit 1 + fi + fi +fi + + + + diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/extract_embedding.sh b/MOSS_Speaker-RoPE/wespeaker/tools/extract_embedding.sh new file mode 100644 index 0000000000000000000000000000000000000000..1f3327d6769699387578f95de59f970c7cfb7f64 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/extract_embedding.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir='exp/XVEC' +model_path='avg_model.pt' +data_type='shard' # shard/raw/feat +data_list='shard.list' # shard.list/raw.list/feat.list +wavs_num= +store_dir= +batch_size=1 +num_workers=1 +nj=4 +reverb_data=data/rirs/lmdb +noise_data=data/musan/lmdb +aug_prob=0.0 +gpus="[0,1]" + +. tools/parse_options.sh +set -e + +embed_dir=${exp_dir}/embeddings/${store_dir} +log_dir=${embed_dir}/log +[ ! -d ${log_dir} ] && mkdir -p ${log_dir} + +# split the data_list file into sub_file, then we can use multi-gpus to extract embeddings +data_num=$(wc -l ${data_list} | awk '{print $1}') +subfile_num=$(($data_num / $nj + 1)) +split -l ${subfile_num} -d -a 3 ${data_list} ${log_dir}/split_ +num_gpus=$(echo $gpus | awk -F ',' '{print NF}') +gpus=(`echo $gpus | cut -d '[' -f2 | cut -d ']' -f1 | tr ',' ' '`) + +for suffix in $(seq 0 $(($nj - 1))); do + idx=$[$suffix % $num_gpus] + suffix=$(printf '%03d' $suffix) + data_list_subfile=${log_dir}/split_${suffix} + embed_ark=${embed_dir}/xvector_${suffix}.ark + CUDA_VISIBLE_DEVICES=${gpus[$idx]} python -u wespeaker/bin/extract.py \ + --config ${exp_dir}/config.yaml \ + --model_path ${model_path} \ + --data_type ${data_type} \ + --data_list ${data_list_subfile} \ + --embed_ark ${embed_ark} \ + --batch-size ${batch_size} \ + --num-workers ${num_workers} \ + --reverb_data ${reverb_data} \ + --noise_data ${noise_data} \ + --aug-prob ${aug_prob} \ + >${log_dir}/split_${suffix}.log 2>&1 & +done + +wait + +cat ${embed_dir}/xvector_*.scp >${embed_dir}/xvector.scp +embed_num=$(wc -l ${embed_dir}/xvector.scp | awk '{print $1}') +if [ $embed_num -eq $wavs_num ]; then + echo "Successfully extract embedding for ${store_dir}" | tee ${embed_dir}/extract.result +else + echo "Failed to extract embedding for ${store_dir}" | tee ${embed_dir}/extract.result +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/extract_embedding_deprecated.sh b/MOSS_Speaker-RoPE/wespeaker/tools/extract_embedding_deprecated.sh new file mode 100644 index 0000000000000000000000000000000000000000..ca9b5f3b43a729d2e5829c0a9777eb89ac9a829b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/extract_embedding_deprecated.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exp_dir='exp/XVEC' +model_path='avg_model.pt' +data_scp='wav.scp/feats.scp' +store_dir='vox2_dev/vox1' +batch_size=1 +num_workers=1 +raw_wav=True +nj=4 +gpus="[0,1]" + +. tools/parse_options.sh +set -e + +embed_dir=${exp_dir}/embeddings/${store_dir} +log_dir=${embed_dir}/log +[ ! -d ${log_dir} ] && mkdir -p ${log_dir} + +# split the data_scp file into sub_file, then we can use multi-gpus to extract embeddings +data_num=$(wc -l ${data_scp} | awk '{print $1}') +subfile_num=$(($data_num / $nj + 1)) +split -l ${subfile_num} -d -a 3 ${data_scp} ${log_dir}/split_ +num_gpus=$(echo $gpus | awk -F ',' '{print NF}') +gpus=(`echo $gpus | cut -d '[' -f2 | cut -d ']' -f1 | tr ',' ' '`) + +for suffix in $(seq 0 $(($nj - 1))); do + idx=$[$suffix % $num_gpus] + suffix=$(printf '%03d' $suffix) + data_scp_subfile=${log_dir}/split_${suffix} + embed_ark=${embed_dir}/xvector_${suffix}.ark + CUDA_VISIBLE_DEVICES=${gpus[$idx]} python3 wespeaker/bin/extract_deprecated.py \ + --config ${exp_dir}/config.yaml \ + --model_path ${model_path} \ + --data_scp ${data_scp_subfile} \ + --embed_ark ${embed_ark} \ + --batch-size ${batch_size} \ + --num-workers ${num_workers} \ + --raw-wav ${raw_wav} \ + >${log_dir}/split_${suffix}.log 2>&1 & + +done + +wait + +cat ${embed_dir}/xvector_*.scp >${embed_dir}/xvector.scp +embed_num=$(wc -l ${embed_dir}/xvector.scp | awk '{print $1}') +if [ $embed_num -eq $data_num ]; then + echo "Success" | tee ${embed_dir}/extract.result +else + echo "Fail" | tee ${embed_dir}/extract.result +fi diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/filter_scp.pl b/MOSS_Speaker-RoPE/wespeaker/tools/filter_scp.pl new file mode 100644 index 0000000000000000000000000000000000000000..b76d37f41be0886470281978bfacf97f6b8ae976 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/filter_scp.pl @@ -0,0 +1,87 @@ +#!/usr/bin/env perl +# Copyright 2010-2012 Microsoft Corporation +# Johns Hopkins University (author: Daniel Povey) + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +# WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# See the Apache 2 License for the specific language governing permissions and +# limitations under the License. + + +# This script takes a list of utterance-ids or any file whose first field +# of each line is an utterance-id, and filters an scp +# file (or any file whose "n-th" field is an utterance id), printing +# out only those lines whose "n-th" field is in id_list. The index of +# the "n-th" field is 1, by default, but can be changed by using +# the -f switch + +$exclude = 0; +$field = 1; +$shifted = 0; + +do { + $shifted=0; + if ($ARGV[0] eq "--exclude") { + $exclude = 1; + shift @ARGV; + $shifted=1; + } + if ($ARGV[0] eq "-f") { + $field = $ARGV[1]; + shift @ARGV; shift @ARGV; + $shifted=1 + } +} while ($shifted); + +if(@ARGV < 1 || @ARGV > 2) { + die "Usage: filter_scp.pl [--exclude] [-f ] id_list [in.scp] > out.scp \n" . + "Prints only the input lines whose f'th field (default: first) is in 'id_list'.\n" . + "Note: only the first field of each line in id_list matters. With --exclude, prints\n" . + "only the lines that were *not* in id_list.\n" . + "Caution: previously, the -f option was interpreted as a zero-based field index.\n" . + "If your older scripts (written before Oct 2014) stopped working and you used the\n" . + "-f option, add 1 to the argument.\n" . + "See also: utils/filter_scp.pl .\n"; +} + + +$idlist = shift @ARGV; +open(F, "<$idlist") || die "Could not open id-list file $idlist"; +while() { + @A = split; + @A>=1 || die "Invalid id-list file line $_"; + $seen{$A[0]} = 1; +} + +if ($field == 1) { # Treat this as special case, since it is common. + while(<>) { + $_ =~ m/\s*(\S+)\s*/ || die "Bad line $_, could not get first field."; + # $1 is what we filter on. + if ((!$exclude && $seen{$1}) || ($exclude && !defined $seen{$1})) { + print $_; + } + } +} else { + while(<>) { + @A = split; + @A > 0 || die "Invalid scp file line $_"; + @A >= $field || die "Invalid scp file line $_"; + if ((!$exclude && $seen{$A[$field-1]}) || ($exclude && !defined $seen{$A[$field-1]})) { + print $_; + } + } +} + +# tests: +# the following should print "foo 1" +# ( echo foo 1; echo bar 2 ) | utils/filter_scp.pl <(echo foo) +# the following should print "bar 2". +# ( echo foo 1; echo bar 2 ) | utils/filter_scp.pl -f 2 <(echo 2) diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/fix_data_dir.sh b/MOSS_Speaker-RoPE/wespeaker/tools/fix_data_dir.sh new file mode 100644 index 0000000000000000000000000000000000000000..1e80eb2dbd160f0aee7c28b54897f4f53ec3de13 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/fix_data_dir.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash + +# Directly copied from https://github.com/kaldi-asr/kaldi/blob/master/egs/wsj/s5/utils/fix_data_dir.sh +# +# This script makes sure that only the segments present in +# all of "feats.scp", "wav.scp" [if present], segments [if present] +# text, and utt2spk are present in any of them. +# It puts the original contents of data-dir into +# data-dir/.backup + +cmd="$@" + +utt_extra_files= +spk_extra_files= + +. tools/parse_options.sh + +if [ $# != 1 ]; then + echo "Usage: tools/fix_data_dir.sh " + echo "e.g.: tools/fix_data_dir.sh data/train" + echo "This script helps ensure that the various files in a data directory" + echo "are correctly sorted and filtered, for example removing utterances" + echo "that have no features (if feats.scp is present)" + exit 1 +fi + +data=$1 + +if [ -f $data/images.scp ]; then + image/fix_data_dir.sh $cmd + exit $? +fi + +mkdir -p $data/.backup + +[ ! -d $data ] && echo "$0: no such directory $data" && exit 1; + +[ ! -f $data/utt2spk ] && echo "$0: no such file $data/utt2spk" && exit 1; + +set -e -o pipefail -u + +tmpdir=$(mktemp -d /tmp/kaldi.XXXX); +trap 'rm -rf "$tmpdir"' EXIT HUP INT PIPE TERM + +export LC_ALL=C + +function check_sorted { + file=$1 + sort -k1,1 -u <$file >$file.tmp + if ! cmp -s $file $file.tmp; then + echo "$0: file $1 is not in sorted order or not unique, sorting it" + mv $file.tmp $file + else + rm $file.tmp + fi +} + +for x in utt2spk spk2utt feats.scp text segments wav.scp cmvn.scp vad.scp \ + reco2file_and_channel spk2gender utt2lang utt2uniq utt2dur reco2dur utt2num_frames; do + if [ -f $data/$x ]; then + cp $data/$x $data/.backup/$x + check_sorted $data/$x + fi +done + + +function filter_file { + filter=$1 + file_to_filter=$2 + cp $file_to_filter ${file_to_filter}.tmp + tools/filter_scp.pl $filter ${file_to_filter}.tmp > $file_to_filter + if ! cmp ${file_to_filter}.tmp $file_to_filter >&/dev/null; then + length1=$(cat ${file_to_filter}.tmp | wc -l) + length2=$(cat ${file_to_filter} | wc -l) + if [ $length1 -ne $length2 ]; then + echo "$0: filtered $file_to_filter from $length1 to $length2 lines based on filter $filter." + fi + fi + rm $file_to_filter.tmp +} + +function filter_recordings { + # We call this once before the stage when we filter on utterance-id, and once + # after. + + if [ -f $data/segments ]; then + # We have a segments file -> we need to filter this and the file wav.scp, and + # reco2file_and_utt, if it exists, to make sure they have the same list of + # recording-ids. + + if [ ! -f $data/wav.scp ]; then + echo "$0: $data/segments exists but not $data/wav.scp" + exit 1; + fi + awk '{print $2}' < $data/segments | sort | uniq > $tmpdir/recordings + n1=$(cat $tmpdir/recordings | wc -l) + [ ! -s $tmpdir/recordings ] && \ + echo "Empty list of recordings (bad file $data/segments)?" && exit 1; + tools/filter_scp.pl $data/wav.scp $tmpdir/recordings > $tmpdir/recordings.tmp + mv $tmpdir/recordings.tmp $tmpdir/recordings + + + cp $data/segments{,.tmp}; awk '{print $2, $1, $3, $4}' <$data/segments.tmp >$data/segments + filter_file $tmpdir/recordings $data/segments + cp $data/segments{,.tmp}; awk '{print $2, $1, $3, $4}' <$data/segments.tmp >$data/segments + rm $data/segments.tmp + + filter_file $tmpdir/recordings $data/wav.scp + [ -f $data/reco2file_and_channel ] && filter_file $tmpdir/recordings $data/reco2file_and_channel + [ -f $data/reco2dur ] && filter_file $tmpdir/recordings $data/reco2dur + true + fi +} + +function filter_speakers { + # throughout this program, we regard utt2spk as primary and spk2utt as derived, so... + tools/utt2spk_to_spk2utt.pl $data/utt2spk > $data/spk2utt + + cat $data/spk2utt | awk '{print $1}' > $tmpdir/speakers + for s in cmvn.scp spk2gender; do + f=$data/$s + if [ -f $f ]; then + filter_file $f $tmpdir/speakers + fi + done + + filter_file $tmpdir/speakers $data/spk2utt + tools/spk2utt_to_utt2spk.pl $data/spk2utt > $data/utt2spk + + for s in cmvn.scp spk2gender $spk_extra_files; do + f=$data/$s + if [ -f $f ]; then + filter_file $tmpdir/speakers $f + fi + done +} + +function filter_utts { + cat $data/utt2spk | awk '{print $1}' > $tmpdir/utts + + ! cat $data/utt2spk | sort | cmp - $data/utt2spk && \ + echo "utt2spk is not in sorted order (fix this yourself)" && exit 1; + + ! cat $data/utt2spk | sort -k2 | cmp - $data/utt2spk && \ + echo "utt2spk is not in sorted order when sorted first on speaker-id " && \ + echo "(fix this by making speaker-ids prefixes of utt-ids)" && exit 1; + + ! cat $data/spk2utt | sort | cmp - $data/spk2utt && \ + echo "spk2utt is not in sorted order (fix this yourself)" && exit 1; + + if [ -f $data/utt2uniq ]; then + ! cat $data/utt2uniq | sort | cmp - $data/utt2uniq && \ + echo "utt2uniq is not in sorted order (fix this yourself)" && exit 1; + fi + + maybe_wav= + maybe_reco2dur= + [ ! -f $data/segments ] && maybe_wav=wav.scp # wav indexed by utts only if segments does not exist. + [ -s $data/reco2dur ] && [ ! -f $data/segments ] && maybe_reco2dur=reco2dur # reco2dur indexed by utts + + maybe_utt2dur= + if [ -f $data/utt2dur ]; then + cat $data/utt2dur | \ + awk '{ if (NF == 2 && $2 > 0) { print }}' > $data/utt2dur.ok || exit 1 + maybe_utt2dur=utt2dur.ok + fi + + maybe_utt2num_frames= + if [ -f $data/utt2num_frames ]; then + cat $data/utt2num_frames | \ + awk '{ if (NF == 2 && $2 > 0) { print }}' > $data/utt2num_frames.ok || exit 1 + maybe_utt2num_frames=utt2num_frames.ok + fi + + for x in feats.scp text segments utt2lang $maybe_wav $maybe_utt2dur $maybe_utt2num_frames; do + if [ -f $data/$x ]; then + tools/filter_scp.pl $data/$x $tmpdir/utts > $tmpdir/utts.tmp + mv $tmpdir/utts.tmp $tmpdir/utts + fi + done + rm $data/utt2dur.ok 2>/dev/null || true + rm $data/utt2num_frames.ok 2>/dev/null || true + + [ ! -s $tmpdir/utts ] && echo "fix_data_dir.sh: no utterances remained: not proceeding further." && \ + rm $tmpdir/utts && exit 1; + + + if [ -f $data/utt2spk ]; then + new_nutts=$(cat $tmpdir/utts | wc -l) + old_nutts=$(cat $data/utt2spk | wc -l) + if [ $new_nutts -ne $old_nutts ]; then + echo "fix_data_dir.sh: kept $new_nutts utterances out of $old_nutts" + else + echo "fix_data_dir.sh: kept all $old_nutts utterances." + fi + fi + + for x in utt2spk utt2uniq feats.scp vad.scp text segments utt2lang utt2dur utt2num_frames $maybe_wav $maybe_reco2dur $utt_extra_files; do + if [ -f $data/$x ]; then + cp $data/$x $data/.backup/$x + if ! cmp -s $data/$x <( tools/filter_scp.pl $tmpdir/utts $data/$x ) ; then + tools/filter_scp.pl $tmpdir/utts $data/.backup/$x > $data/$x + fi + fi + done + +} + +filter_recordings +filter_speakers +filter_utts +filter_speakers +filter_recordings + +tools/utt2spk_to_spk2utt.pl $data/utt2spk > $data/spk2utt + +echo "fix_data_dir.sh: old files are kept in $data/.backup" diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/generate_calibration_trial.py b/MOSS_Speaker-RoPE/wespeaker/tools/generate_calibration_trial.py new file mode 100644 index 0000000000000000000000000000000000000000..a0787b000ee5cac4514fddd8751e47b8aa2b3b07 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/generate_calibration_trial.py @@ -0,0 +1,87 @@ +# Copyright (c) 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2024 Bing Han (hanbing97@sjtu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import fire +import logging +import random +from tqdm import tqdm + + +def main(utt2dur, trial_path, each_trial_num=10000): + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(levelname)s %(message)s') + logging.info('Generate calibration trial ...') + short_spk2utt = {} + long_spk2utt = {} + + with open(utt2dur, 'r') as f: + for line in f.readlines(): + utt, dur = line.strip().split() + dur = float(dur) + spk = utt.split('/')[0] + + if 2 < dur < 6: + if spk not in short_spk2utt: + short_spk2utt[spk] = [] + short_spk2utt[spk].append(utt) + + if dur > 6: + if spk not in long_spk2utt: + long_spk2utt[spk] = [] + long_spk2utt[spk].append(utt) + + long_spks = list(long_spk2utt.keys()) + short_spks = list(short_spk2utt.keys()) + + for spk in long_spks: + if spk not in short_spks: + long_spk2utt.pop(spk, None) + long_spks = list(long_spk2utt.keys()) + for spk in short_spks: + if spk not in long_spks: + short_spk2utt.pop(spk, None) + short_spks = list(short_spk2utt.keys()) + + with open(trial_path, 'w') as f: + for _ in tqdm(range(each_trial_num // 2)): + enroll_spk = random.choice(short_spks) + spk_index = short_spks.index(enroll_spk) + nontarget_spk = random.choice(short_spks[:spk_index] + + short_spks[spk_index + 1:]) + + # short2short + enroll_utt, test_utt = random.choices(short_spk2utt[enroll_spk], + k=2) + f.write("{} {} {}\n".format(enroll_utt, test_utt, 'target')) + test_utt = random.choice(short_spk2utt[nontarget_spk]) + f.write("{} {} {}\n".format(enroll_utt, test_utt, 'nontarget')) + + # short2long + enroll_utt = random.choice(short_spk2utt[enroll_spk]) + test_utt = random.choice(long_spk2utt[enroll_spk]) + f.write("{} {} {}\n".format(enroll_utt, test_utt, 'target')) + test_utt = random.choice(long_spk2utt[nontarget_spk]) + f.write("{} {} {}\n".format(enroll_utt, test_utt, 'nontarget')) + + # long2long + enroll_utt, test_utt = random.choices(long_spk2utt[enroll_spk], + k=2) + f.write("{} {} {}\n".format(enroll_utt, test_utt, 'target')) + test_utt = random.choice(long_spk2utt[nontarget_spk]) + f.write("{} {} {}\n".format(enroll_utt, test_utt, 'nontarget')) + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/make_feat_list.py b/MOSS_Speaker-RoPE/wespeaker/tools/make_feat_list.py new file mode 100644 index 0000000000000000000000000000000000000000..e2182f88dac0572237946842b74b8ac418be8a16 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/make_feat_list.py @@ -0,0 +1,61 @@ +# Copyright (c) 2022 Binbin Zhang(binbzha@qq.com) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import logging +import json + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('feat_file', help='feat file') + parser.add_argument('utt2spk_file', help='utt2spk file') + parser.add_argument('feat_list', help='output feat list file') + args = parser.parse_args() + return args + + +def main(): + args = get_args() + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(levelname)s %(message)s') + + feat_table = {} + with open(args.feat_file, 'r', encoding='utf8') as fin: + for line in fin: + arr = line.strip().split() + key = arr[0] # os.path.splitext(arr[0])[0] + assert len(arr) == 2 + feat_table[key] = arr[1] + + data = [] + with open(args.utt2spk_file, 'r', encoding='utf8') as fin: + for line in fin: + arr = line.strip().split(maxsplit=1) + key = arr[0] # os.path.splitext(arr[0])[0] + spk = arr[1] + assert key in feat_table + feat = feat_table[key] + data.append((key, spk, feat)) + + with open(args.feat_list, 'w', encoding='utf8') as fout: + for key, spk, feat in data: + line = dict(key=key, spk=spk, feat=feat) + json_line = json.dumps(line, ensure_ascii=False) + fout.write(json_line + '\n') + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/make_lmdb.py b/MOSS_Speaker-RoPE/wespeaker/tools/make_lmdb.py new file mode 100644 index 0000000000000000000000000000000000000000..bcd8b08455ca5ed2b4808f9b2e63d0e5d4aaf814 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/make_lmdb.py @@ -0,0 +1,59 @@ +# Copyright (c) 2022 Binbin Zhang (binbzha@qq.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import math +import pickle + +import lmdb +from tqdm import tqdm + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('in_scp_file', help='input scp file') + parser.add_argument('out_lmdb', help='output lmdb') + args = parser.parse_args() + return args + + +def main(): + args = get_args() + db = lmdb.open(args.out_lmdb, map_size=int(math.pow(1024, 4))) # 1TB + # txn is for Transaciton + txn = db.begin(write=True) + keys = [] + with open(args.in_scp_file, 'r', encoding='utf8') as fin: + lines = fin.readlines() + for i, line in enumerate(tqdm(lines)): + arr = line.strip().split() + assert len(arr) == 2 + key, wav = arr[0], arr[1] + keys.append(key) + with open(wav, 'rb') as fin: + data = fin.read() + txn.put(key.encode(), data) + # Write flush to disk + if i % 100 == 0: + txn.commit() + txn = db.begin(write=True) + txn.commit() + with db.begin(write=True) as txn: + txn.put(b'__keys__', pickle.dumps(keys)) + db.sync() + db.close() + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/make_raw_list.py b/MOSS_Speaker-RoPE/wespeaker/tools/make_raw_list.py new file mode 100644 index 0000000000000000000000000000000000000000..50ee9e77e118ca84e167dd5bb464614cc130362b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/make_raw_list.py @@ -0,0 +1,88 @@ +# Copyright (c) 2022 Binbin Zhang(binbzha@qq.com) +# 2023 Zhengyang Chen(chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import logging +import json +import os + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--vad_file', + type=str, + help='vad file', + default='non_exist') + parser.add_argument('wav_file', help='wav file') + parser.add_argument('utt2spk_file', help='utt2spk file') + parser.add_argument('raw_list', help='output raw list file') + args = parser.parse_args() + return args + + +def main(): + args = get_args() + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(levelname)s %(message)s') + + wav_table = {} + with open(args.wav_file, 'r', encoding='utf8') as fin: + for line in fin: + arr = line.strip().split() + key = arr[0] # os.path.splitext(arr[0])[0] + wav_table[key] = ' '.join(arr[1:]) + + if os.path.exists(args.vad_file): + vad_dict = {} + with open(args.vad_file, 'r', encoding='utf8') as fin: + for line in fin: + arr = line.strip().split() + utt, start, end = arr[-3], arr[-2], arr[-1] + if utt not in vad_dict: + vad_dict[utt] = [] + vad_dict[utt].append((start, end)) + else: + vad_dict = None + + data = [] + with open(args.utt2spk_file, 'r', encoding='utf8') as fin: + for line in fin: + arr = line.strip().split(maxsplit=1) + key = arr[0] # os.path.splitext(arr[0])[0] + spk = arr[1] + assert key in wav_table + wav = wav_table[key] + if vad_dict is None: + data.append((key, spk, wav)) + else: + if key not in vad_dict: + continue + vad = vad_dict[key] + data.append((key, spk, wav, vad)) + + with open(args.raw_list, 'w', encoding='utf8') as fout: + for utt_info in data: + if len(utt_info) == 4: + key, spk, wav, vad = utt_info + line = dict(key=key, spk=spk, wav=wav, vad=vad) + else: + key, spk, wav = utt_info + line = dict(key=key, spk=spk, wav=wav) + json_line = json.dumps(line, ensure_ascii=False) + fout.write(json_line + '\n') + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/make_shard_list.py b/MOSS_Speaker-RoPE/wespeaker/tools/make_shard_list.py new file mode 100644 index 0000000000000000000000000000000000000000..809410f6979643f0f08bbae051bfd6223a871fdb --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/make_shard_list.py @@ -0,0 +1,239 @@ +# Copyright (c) 2021 Mobvoi Inc. (authors: Binbin Zhang) +# 2023 Shanghai Jiaotong University (authors: Zhengyang Chen) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import io +import logging +import os +import random +import tarfile +import time +import multiprocessing +import subprocess +from scipy.io import wavfile +import numpy as np +import struct + +AUDIO_FORMAT_SETS = set(['flac', 'mp3', 'm4a', 'ogg', 'opus', 'wav', 'wma']) + + +def write_wav_to_bytesio(audio_data, sample_rate): + audio_data = audio_data.astype(np.int16) + + with io.BytesIO() as wav_stream: + # WAV header values + num_channels = 1 # Mono audio + bytes_per_sample = 2 # Assuming 16-bit audio + + # Write WAV header + wav_stream.write(b'RIFF') + wav_stream.write(b'\x00\x00\x00\x00') # Placeholder for file size + wav_stream.write(b'WAVE') + + # Write format chunk + wav_stream.write(b'fmt ') + wav_stream.write(struct.pack(' None: + print(f"Loading Stage 1 config from: {config}") + configs = parse_config_or_kwargs(config) + + if ( + "dataset_args" not in configs + or "frontend" not in configs["dataset_args"] + ): + print( + "Error: 'dataset_args.frontend' not found in config file " + f"{config}." + ) + return + + frontend_name = configs["dataset_args"]["frontend"] + frontend_args_key = f"{frontend_name}_args" + + if frontend_args_key not in configs["dataset_args"]: + print( + "Error: 'dataset_args.{frontend_args_key}' not found in config " + f"file {config}." + ) + return + + frontend_args = configs["dataset_args"][frontend_args_key] + + if not frontend_args.get("use_lora", False): + print( + "Warning: 'use_lora: True' is not set in " + f"'dataset_args.{frontend_args_key}' of config {config}." + ) + print("Make sure you are using the Stage 1 (LoRA) config file.") + + print(f"Building frontend: {frontend_name}") + frontend_class = frontend_class_dict[frontend_name] + frontend = frontend_class(**frontend_args) + + print(f"Building model: {configs['model']}") + model_class = get_speaker_model(configs["model"]) + model = model_class(**configs["model_args"]) + + model.frontend = frontend + + print(f"Loading LoRA checkpoint from: {checkpoint_in}") + checkpoint = torch.load(checkpoint_in, map_location="cpu") + if "model" in checkpoint: + checkpoint = checkpoint["model"] + + model.load_state_dict(checkpoint, strict=False) + + print("Merging LoRA weights into the base model...") + encoder = getattr(getattr(model, "frontend", None), "encoder", None) + + if isinstance(encoder, PeftModel): + model.frontend.encoder = encoder.merge_and_unload() + print("After merge, encoder type:", type(model.frontend.encoder)) + print("Merge successful.") + else: + print("Error: no PeftModel found to merge.") + print( + " - Check if model.frontend.encoder is an instance of " + "PeftModel" + ) + print(f" - model.frontend type: {type(model.frontend)}") + if hasattr(model, "frontend") and hasattr(model.frontend, "encoder"): + print( + " - model.frontend.encoder type: " + f"{type(model.frontend.encoder)}" + ) + return + + if "projection_args" in configs: + try: + from wespeaker.models.projections import get_projection + + print("Building projection layer...") + projection = get_projection(configs["projection_args"]) + model.add_module("projection", projection) + + for key in ("projection.weight", "projection.bias"): + if key in checkpoint: + print(f"Loading {key} from checkpoint.") + model.state_dict()[key].copy_(checkpoint[key]) + else: + print(f"Warning: {key} not found in LoRA checkpoint.") + except Exception as exc: + print( + "Warning: failed to build or load projection: " + f"{exc}" + ) + + print(f"Saving merged model to: {checkpoint_out}") + torch.save(model.state_dict(), checkpoint_out) + print("Merged model saved successfully.") + + +if __name__ == "__main__": + fire.Fire(merge) diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/parse_options.sh b/MOSS_Speaker-RoPE/wespeaker/tools/parse_options.sh new file mode 100644 index 0000000000000000000000000000000000000000..d6d6197346729a6c0e97e043c98286d85fb0727a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/parse_options.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# Copyright 2012 Johns Hopkins University (Author: Daniel Povey); +# Arnab Ghoshal, Karel Vesely +# 2022 Hongji Wang (jijijiang77@gmail.com) + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +# WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# See the Apache 2 License for the specific language governing permissions and +# limitations under the License. + +# Parse command-line options. +# To be sourced by another script (as in ". parse_options.sh"). +# Option format is: --option-name arg +# and shell variable "option_name" gets set to value "arg." +# The exception is --help, which takes no arguments, but prints the +# $help_message variable (if defined). + +### +### The --conf file options have lower priority to command line +### options, so we need to import them first... +### + +# Now import all the confs specified by command-line, in left-to-right order +for ((argpos = 1; argpos < $#; argpos++)); do + if [ "${!argpos}" == "--conf" ]; then + argpos_plus1=$((argpos + 1)) + conf=${!argpos_plus1} + [ ! -r $conf ] && echo "$0: missing conf '$conf'" && exit 1 + . $conf # source the conf file. + fi +done + +### +### No we process the command line options +### +while true; do + [ -z "${1:-}" ] && break # break if there are no arguments + case "$1" in + # If the enclosing script is called with --help option, print the help + # message and exit. Scripts should put help messages in $help_message + --help | -h) + if [ -z "$help_message" ]; then + echo "No help found." 1>&2 + else printf "$help_message\n" 1>&2; fi + exit 0 + ;; + --*=*) + echo "$0: options to scripts must be of the form --name value, got '$1'" + exit 1 + ;; + # If the first command-line argument begins with "--" (e.g. --foo-bar), + # then work out the variable name as $name, which will equal "foo_bar". + --*) + name=$(echo "$1" | sed s/^--// | sed s/-/_/g) + # Next we test whether the variable in question is undefned-- if so it's + # an invalid option and we die. Note: $0 evaluates to the name of the + # enclosing script. + # The test [ -z ${foo_bar+xxx} ] will return true if the variable foo_bar + # is undefined. We then have to wrap this test inside "eval" because + # foo_bar is itself inside a variable ($name). + eval '[ -z "${'$name'+xxx}" ]' && echo "$0: invalid option $1" 1>&2 && exit 1 + + oldval="$(eval echo \$$name)" + # Work out whether we seem to be expecting a Boolean argument. + if [ "$oldval" == "true" ] || [ "$oldval" == "false" ]; then + was_bool=true + else + was_bool=false + fi + + # Set the variable to the right value-- the escaped quotes make it work if + # the option had spaces, like --cmd "queue.pl -sync y" + eval $name=\"$2\" + + # Check that Boolean-valued arguments are really Boolean. + if $was_bool && [[ "$2" != "true" && "$2" != "false" ]]; then + echo "$0: expected \"true\" or \"false\": $1 $2" 1>&2 + exit 1 + fi + shift 2 + ;; + *) break ;; + esac +done + +# Check for an empty argument to the --cmd option, which can easily occur as a +# result of scripting errors. +[ ! -z "${cmd+xxx}" ] && [ -z "$cmd" ] && echo "$0: empty argument to --cmd option" 1>&2 && exit 1 + +true # so this script returns exit code 0. diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/spk2utt_to_utt2spk.pl b/MOSS_Speaker-RoPE/wespeaker/tools/spk2utt_to_utt2spk.pl new file mode 100644 index 0000000000000000000000000000000000000000..19fb89d501146e360912863d847d6eabb0194511 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/spk2utt_to_utt2spk.pl @@ -0,0 +1,27 @@ +#!/usr/bin/env perl +# Copyright 2010-2011 Microsoft Corporation + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +# WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# See the Apache 2 License for the specific language governing permissions and +# limitations under the License. + + +while(<>){ + @A = split(" ", $_); + @A > 1 || die "Invalid line in spk2utt file: $_"; + $s = shift @A; + foreach $u ( @A ) { + print "$u $s\n"; + } +} + + diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/subset_data_dir.sh b/MOSS_Speaker-RoPE/wespeaker/tools/subset_data_dir.sh new file mode 100644 index 0000000000000000000000000000000000000000..7ae0c663775702659c824072eb20366516be700f --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/subset_data_dir.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# Copyright 2010-2011 Microsoft Corporation +# 2012-2013 Johns Hopkins University (Author: Daniel Povey) +# Apache 2.0 + + +# This script operates on a data directory, such as in data/train/. +# See http://kaldi-asr.org/doc/data_prep.html#data_prep_data +# for what these directories contain. + +# This script creates a subset of that data, consisting of some specified +# number of utterances. (The selected utterances are distributed evenly +# throughout the file, by the program ./subset_scp.pl). + +# There are six options, none compatible with any other. + +# If you give the --per-spk option, it will attempt to select the supplied +# number of utterances for each speaker (typically you would supply a much +# smaller number in this case). + +# If you give the --speakers option, it selects a subset of n randomly +# selected speakers. + +# If you give the --shortest option, it will give you the n shortest utterances. + +# If you give the --first option, it will just give you the n first utterances. + +# If you give the --last option, it will just give you the n last utterances. + +# If you give the --spk-list or --utt-list option, it reads the +# speakers/utterances to keep from /" (note, +# in this case there is no positional parameter; see usage message.) + + +shortest=false +perspk=false +speakers=false +first_opt= +spk_list= +utt_list= + +expect_args=3 +case $1 in + --first|--last) first_opt=$1; shift ;; + --per-spk) perspk=true; shift ;; + --shortest) shortest=true; shift ;; + --speakers) speakers=true; shift ;; + --spk-list) shift; spk_list=$1; shift; expect_args=2 ;; + --utt-list) shift; utt_list=$1; shift; expect_args=2 ;; + --*) echo "$0: invalid option '$1'"; exit 1 +esac + +if [ $# != $expect_args ]; then + echo "Usage:" + echo " subset_data_dir.sh [--speakers|--shortest|--first|--last|--per-spk] " + echo " subset_data_dir.sh [--spk-list ] " + echo " subset_data_dir.sh [--utt-list ] " + echo "By default, randomly selects utterances from the data directory." + echo "With --speakers, randomly selects enough speakers that we have utterances" + echo "With --per-spk, selects utterances per speaker, if available." + echo "With --first, selects the first utterances" + echo "With --last, selects the last utterances" + echo "With --shortest, selects the shortest utterances." + echo "With --spk-list, reads the speakers to keep from " + echo "With --utt-list, reads the utterances to keep from " + exit 1; +fi + +srcdir=$1 +if [[ $spk_list || $utt_list ]]; then + numutt= + destdir=$2 +else + numutt=$2 + destdir=$3 +fi + +export LC_ALL=C + +if [ ! -f $srcdir/utt2spk ]; then + echo "$0: no such file $srcdir/utt2spk" + exit 1 +fi + +if [[ $numutt && $numutt -gt $(wc -l <$srcdir/utt2spk) ]]; then + echo "$0: cannot subset to more utterances than you originally had." + exit 1 +fi + +if $shortest && [ ! -f $srcdir/feats.scp ]; then + echo "$0: you selected --shortest but no feats.scp exist." + exit 1 +fi + +mkdir -p $destdir || exit 1 + +if [[ $spk_list ]]; then + tools/filter_scp.pl "$spk_list" $srcdir/spk2utt > $destdir/spk2utt || exit 1; + tools/spk2utt_to_utt2spk.pl < $destdir/spk2utt > $destdir/utt2spk || exit 1; +elif [[ $utt_list ]]; then + tools/filter_scp.pl "$utt_list" $srcdir/utt2spk > $destdir/utt2spk || exit 1; + tools/utt2spk_to_spk2utt.pl < $destdir/utt2spk > $destdir/spk2utt || exit 1; +elif $speakers; then + tools/shuffle_list.pl < $srcdir/spk2utt | + awk -v numutt=$numutt '{ if (tot < numutt){ print; } tot += (NF-1); }' | + sort > $destdir/spk2utt + tools/spk2utt_to_utt2spk.pl < $destdir/spk2utt > $destdir/utt2spk +elif $perspk; then + awk '{ n='$numutt'; printf("%s ",$1); + skip=1; while(n*(skip+1) <= NF-1) { skip++; } + for(x=2; x<=NF && x <= (n*skip+1); x += skip) { printf("%s ", $x); } + printf("\n"); }' <$srcdir/spk2utt >$destdir/spk2utt + tools/spk2utt_to_utt2spk.pl < $destdir/spk2utt > $destdir/utt2spk +else + if $shortest; then + # Select $numutt shortest utterances. + . ./path.sh + feat-to-len scp:$srcdir/feats.scp ark,t:$destdir/tmp.len || exit 1; + sort -n -k2 $destdir/tmp.len | + awk '{print $1}' | + head -$numutt >$destdir/tmp.uttlist + tools/filter_scp.pl $destdir/tmp.uttlist $srcdir/utt2spk >$destdir/utt2spk + rm $destdir/tmp.uttlist $destdir/tmp.len + else + # Select $numutt random utterances. + tools/subset_scp.pl $first_opt $numutt $srcdir/utt2spk > $destdir/utt2spk || exit 1; + fi + tools/utt2spk_to_spk2utt.pl < $destdir/utt2spk > $destdir/spk2utt +fi + +# Perform filtering. utt2spk and spk2utt files already exist by this point. +# Filter by utterance. +[ -f $srcdir/feats.scp ] && + tools/filter_scp.pl $destdir/utt2spk <$srcdir/feats.scp >$destdir/feats.scp +[ -f $srcdir/vad.scp ] && + tools/filter_scp.pl $destdir/utt2spk <$srcdir/vad.scp >$destdir/vad.scp +[ -f $srcdir/utt2lang ] && + tools/filter_scp.pl $destdir/utt2spk <$srcdir/utt2lang >$destdir/utt2lang +[ -f $srcdir/utt2dur ] && + tools/filter_scp.pl $destdir/utt2spk <$srcdir/utt2dur >$destdir/utt2dur +[ -f $srcdir/utt2num_frames ] && + tools/filter_scp.pl $destdir/utt2spk <$srcdir/utt2num_frames >$destdir/utt2num_frames +[ -f $srcdir/utt2uniq ] && + tools/filter_scp.pl $destdir/utt2spk <$srcdir/utt2uniq >$destdir/utt2uniq +[ -f $srcdir/wav.scp ] && + tools/filter_scp.pl $destdir/utt2spk <$srcdir/wav.scp >$destdir/wav.scp +[ -f $srcdir/utt2warp ] && + tools/filter_scp.pl $destdir/utt2spk <$srcdir/utt2warp >$destdir/utt2warp +[ -f $srcdir/text ] && + tools/filter_scp.pl $destdir/utt2spk <$srcdir/text >$destdir/text + +# Filter by speaker. +[ -f $srcdir/spk2warp ] && + tools/filter_scp.pl $destdir/spk2utt <$srcdir/spk2warp >$destdir/spk2warp +[ -f $srcdir/spk2gender ] && + tools/filter_scp.pl $destdir/spk2utt <$srcdir/spk2gender >$destdir/spk2gender +[ -f $srcdir/cmvn.scp ] && + tools/filter_scp.pl $destdir/spk2utt <$srcdir/cmvn.scp >$destdir/cmvn.scp + +# Filter by recording-id. +if [ -f $srcdir/segments ]; then + tools/filter_scp.pl $destdir/utt2spk <$srcdir/segments >$destdir/segments + # Recording-ids are in segments. + awk '{print $2}' $destdir/segments | sort | uniq >$destdir/reco + # The next line overrides the command above for wav.scp, which would be incorrect. + [ -f $srcdir/wav.scp ] && + tools/filter_scp.pl $destdir/reco <$srcdir/wav.scp >$destdir/wav.scp +else + # No segments; recording-ids are in wav.scp. + awk '{print $1}' $destdir/wav.scp | sort | uniq >$destdir/reco +fi + +[ -f $srcdir/reco2file_and_channel ] && + tools/filter_scp.pl $destdir/reco <$srcdir/reco2file_and_channel >$destdir/reco2file_and_channel +[ -f $srcdir/reco2dur ] && + tools/filter_scp.pl $destdir/reco <$srcdir/reco2dur >$destdir/reco2dur + +# Filter the STM file for proper sclite scoring. +# Copy over the comments from STM file. +[ -f $srcdir/stm ] && + (grep "^;;" $srcdir/stm + tools/filter_scp.pl $destdir/reco $srcdir/stm) >$destdir/stm + +rm $destdir/reco + +# Copy frame_shift if present. +[ -f $srcdir/frame_shift ] && cp $srcdir/frame_shift $destdir + +srcutts=$(wc -l <$srcdir/utt2spk) +destutts=$(wc -l <$destdir/utt2spk) +echo "$0: reducing #utt from $srcutts to $destutts" +exit 0 diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/utt2spk_to_spk2utt.pl b/MOSS_Speaker-RoPE/wespeaker/tools/utt2spk_to_spk2utt.pl new file mode 100644 index 0000000000000000000000000000000000000000..5086699ff85fdcb8667bb9ab054700c53e35fd0c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/utt2spk_to_spk2utt.pl @@ -0,0 +1,38 @@ +#!/usr/bin/env perl +# Copyright 2010-2011 Microsoft Corporation + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +# WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# See the Apache 2 License for the specific language governing permissions and +# limitations under the License. + +# converts an utt2spk file to a spk2utt file. +# Takes input from the stdin or from a file argument; +# output goes to the standard out. + +if ( @ARGV > 1 ) { + die "Usage: utt2spk_to_spk2utt.pl [ utt2spk ] > spk2utt"; +} + +while(<>){ + @A = split(" ", $_); + @A == 2 || die "Invalid line in utt2spk file: $_"; + ($u,$s) = @A; + if(!$seen_spk{$s}) { + $seen_spk{$s} = 1; + push @spklist, $s; + } + push (@{$spk_hash{$s}}, "$u"); +} +foreach $s (@spklist) { + $l = join(' ',@{$spk_hash{$s}}); + print "$s $l\n"; +} diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/vector_mean.py b/MOSS_Speaker-RoPE/wespeaker/tools/vector_mean.py new file mode 100644 index 0000000000000000000000000000000000000000..f7e59021886dfe4e1446120534937c3a955a2656 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/vector_mean.py @@ -0,0 +1,66 @@ +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import kaldiio +import os +import numpy as np + +from tqdm import tqdm +from wespeaker.utils.utils import validate_path + + +def compute_vector_mean(spk2utt, xvector_scp, spk_xvector_ark): + # read spk2utt + spk2utt_dict = {} + with open(spk2utt, 'r', encoding='utf-8') as fin: + lines = fin.readlines() + for line in lines: + line = line.strip().split(' ') + spk2utt_dict[line[0]] = line[1:] + + utt2embs = {} + for utt, emb in kaldiio.load_scp_sequential(xvector_scp): + utt2embs[utt] = emb + + validate_path(spk_xvector_ark) + spk_xvector_ark = os.path.abspath(spk_xvector_ark) + spk_xvector_scp = spk_xvector_ark[:-3] + "scp" + with kaldiio.WriteHelper('ark,scp:' + spk_xvector_ark + "," + + spk_xvector_scp) as writer: + for spk in tqdm(spk2utt_dict.keys()): + utts = spk2utt_dict[spk] + mean_vec = None + utt_num = 0 + for utt in utts: + vec = utt2embs[utt] + if mean_vec is None: + mean_vec = np.zeros_like(vec) + mean_vec += vec + utt_num += 1 + mean_vec = mean_vec / utt_num + writer(spk, mean_vec) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='compute the mean of vector') + parser.add_argument('--spk2utt', type=str, default='', help='spk2utt file') + parser.add_argument('--xvector_scp', + type=str, + default='', + help='xvector file (kaldi format)') + parser.add_argument('--spk_xvector_ark', type=str, default='') + args = parser.parse_args() + + compute_vector_mean(args.spk2utt, args.xvector_scp, args.spk_xvector_ark) diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/wav2dur.py b/MOSS_Speaker-RoPE/wespeaker/tools/wav2dur.py new file mode 100644 index 0000000000000000000000000000000000000000..b53a7fe1d92349110af834c95b2dd118c4695d68 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/wav2dur.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +import sys + +import torchaudio + +torchaudio.set_audio_backend("sox_io") + +scp = sys.argv[1] +dur_scp = sys.argv[2] + +with open(scp, 'r') as f, open(dur_scp, 'w') as fout: + cnt = 0 + total_duration = 0 + for l in f: + items = l.strip().split() + wav_id = items[0] + fname = items[1] + cnt += 1 + waveform, rate = torchaudio.load(fname) + frames = len(waveform[0]) + duration = frames / float(rate) + total_duration += duration + fout.write('{} {}\n'.format(wav_id, duration)) + print('process {} utts'.format(cnt)) + print('total {} s'.format(total_duration)) diff --git a/MOSS_Speaker-RoPE/wespeaker/tools/wav_to_duration.sh b/MOSS_Speaker-RoPE/wespeaker/tools/wav_to_duration.sh new file mode 100644 index 0000000000000000000000000000000000000000..5dea9fb6e6c93f67789657cc9bb0e51a07230f53 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/tools/wav_to_duration.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# split the wav scp, calculate duration and merge + +nj=4 + +. tools/parse_options.sh || exit 1 + +inscp=$1 +outscp=$2 +data=$(dirname ${inscp}) +if [ $# -eq 3 ]; then + logdir=$3 +else + logdir=${data}/log +fi +mkdir -p ${logdir} + +rm -f $logdir/wav_*.slice +rm -f $logdir/wav_*.shape +split --additional-suffix .slice -d -n l/$nj $inscp $logdir/wav_ + +for slice in `ls $logdir/wav_*.slice`; do +{ + name=`basename -s .slice $slice` + python tools/wav2dur.py $slice $logdir/$name.shape 1>$logdir/$name.log +} & +done +wait +cat $logdir/wav_*.shape > $outscp diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/__init__.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b3c631c49e5f388d5afd0df5f04ab1d7070594e1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/__init__.py @@ -0,0 +1,2 @@ +from wespeaker.cli.speaker import load_model # noqa +from wespeaker.cli.speaker import load_model_pt # noqa diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/adapt_plda.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/adapt_plda.py new file mode 100644 index 0000000000000000000000000000000000000000..c0313535cac59abecf17f307b0b860deb8ef53dd --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/adapt_plda.py @@ -0,0 +1,63 @@ +# Copyright (c) 2023 Brno University of Technology +# Shuai Wang (wsstriving@gmail.com) +# +# Python implementation of Kaldi unsupervised PLDA adaptation +# ( https://github.com/kaldi-asr/kaldi/blob/master/src/ivector/plda.cc#L613 ) +# by Daniel Povey. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +from wespeaker.utils.plda.two_cov_plda import TwoCovPLDA + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--adp_scp', + '-ad', + type=str, + required=True, + help='Data for unlabeled adaptation.') + parser.add_argument('--across_class_scale', + '-as', + type=float, + help='Scaling factor for across class covariance.', + default=0.5) + parser.add_argument('--within_class_scale', + '-ws', + type=float, + help='Scaling factor for withn class covariance.', + default=0.5) + parser.add_argument('--mdl_org', + '-mo', + type=str, + required=True, + help='Original PLDA mdl.') + parser.add_argument('--mdl_adp', + '-ma', + type=str, + required=True, + help='Adapted PLDA mdl.') + parser.add_argument('--mdl_format', + '-mf', + type=str, + default='wespeaker', + help='Format of the model wespeaker/kaldi') + + args = parser.parse_args() + + kaldi_format = True if args.mdl_format == 'kaldi' else False + plda = TwoCovPLDA.load_model(args.mdl_org, kaldi_format) + adapt_plda = plda.adapt(args.adp_scp, args.across_class_scale, + args.within_class_scale) + adapt_plda.save_model(args.mdl_adp) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/apply_embd_proc.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/apply_embd_proc.py new file mode 100644 index 0000000000000000000000000000000000000000..a7149ed471c119f905c3738ea08dc7bbaf3b5442 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/apply_embd_proc.py @@ -0,0 +1,77 @@ +# Copyright (c) 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import kaldiio +import numpy as np +from wespeaker.utils.embedding_processing import EmbeddingProcessingChain + +if __name__ == '__main__': + """ + xxx + """ + parser = argparse.ArgumentParser() + parser.add_argument('--path', + type=str, + default='', + help='Path to processing chain.') + parser.add_argument('--input', + type=str, + default='', + help='Input scp file.') + parser.add_argument('--output', + type=str, + default='', + help='Output scp/ark file.') + args = parser.parse_args() + + processingChain = EmbeddingProcessingChain() + processingChain.load(args.path) + + embd = [] + utt = [] + for k, v in kaldiio.load_scp_sequential(args.input): + utt.append(k) + embd.append(v) + embd = np.array(embd) + utt = np.array(utt) + + print("Read {} embeddings of dimension {}.".format(embd.shape[0], + embd.shape[1])) + + embd = processingChain(embd) + + # Store both ark and scp if extention '.ark,scp' or '.scp,ark'. Or, only + # ark if extension is '.ark' + output_file = args.output + if output_file.endswith('ark,scp') or output_file.endswith('scp,ark'): + output_file = output_file.rstrip('ark,scp') + output_file = output_file.rstrip('scp,ark') + with kaldiio.WriteHelper('ark,scp:' + output_file + "ark," + + output_file + 'scp') as writer: + for i, u in enumerate(utt): + e = embd[i] + writer(u, e) + + elif output_file.endswith('ark'): + with kaldiio.WriteHelper('ark:' + output_file) as writer: + for i, u in enumerate(utt): + e = embd[i] + writer(u, e) + else: + raise Exception( + "Invalid file extension of output file {}".format(output_file)) + + print("Wrote {} embeddings of dimension {}.".format( + embd.shape[0], embd.shape[1])) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/average_model.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/average_model.py new file mode 100644 index 0000000000000000000000000000000000000000..a419acf44f711458a9f2e0a3daf5a7c34e7f68d9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/average_model.py @@ -0,0 +1,80 @@ +# Copyright (c) 2020 Mobvoi Inc (Di Wu) +# 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import glob +import re + +import torch + + +def get_args(): + parser = argparse.ArgumentParser(description='average model') + parser.add_argument('--dst_model', required=True, help='averaged model') + parser.add_argument('--src_path', + required=True, + help='src model path for average') + parser.add_argument('--num', + default=5, + type=int, + help='nums for averaged model') + parser.add_argument('--min_epoch', + default=0, + type=int, + help='min epoch used for averaging model') + parser.add_argument( + '--max_epoch', + default=65536, # Big enough + type=int, + help='max epoch used for averaging model') + args = parser.parse_args() + print(args) + return args + + +def main(): + args = get_args() + + path_list = glob.glob('{}/[!avg][!final][!convert]*.pt'.format( + args.src_path)) + path_list = sorted( + path_list, + key=lambda p: int(re.findall(r"(?<=model_)\d*(?=.pt)", p)[0])) + path_list = path_list[-args.num:] + print(path_list) + avg = None + num = args.num + assert num == len(path_list) + for path in path_list: + print('Processing {}'.format(path)) + states = torch.load(path, map_location=torch.device('cpu')) + states = states['model'] if 'model' in states else states + if avg is None: + avg = states + else: + for k in avg.keys(): + avg[k] += states[k] + # average + for k in avg.keys(): + if avg[k] is not None: + # pytorch 1.6 use true_divide instead of /= + avg[k] = torch.true_divide(avg[k], num) + print('Saving to {}'.format(args.dst_model)) + torch.save(avg, args.dst_model) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/compute_det.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/compute_det.py new file mode 100644 index 0000000000000000000000000000000000000000..1fbd5fd5144984ccbe0a078d30b8833df2da949a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/compute_det.py @@ -0,0 +1,48 @@ +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import fire +import numpy as np + +from wespeaker.utils.score_metrics import compute_pmiss_pfa_rbst, plot_det_curve + + +def compute_det(scores_file, det_file): + scores = [] + labels = [] + + with open(scores_file) as readlines: + for line in readlines: + tokens = line.strip().split() + # assert len(tokens) == 4 + scores.append(float(tokens[2])) + labels.append(tokens[3] == 'target') + + scores = np.hstack(scores) + labels = np.hstack(labels) + + fnr, fpr = compute_pmiss_pfa_rbst(scores, labels) + plot_det_curve(fnr, fpr, det_file) + print("DET curve saved in {}".format(det_file)) + + +def main(*scores_files): + for scores_file in scores_files: + det_file = scores_file + ".det.png" + compute_det(scores_file, det_file) + + +if __name__ == '__main__': + fire.Fire(main) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/compute_metrics.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/compute_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..834dd5348d2b972f391d191acbb391265b399cf3 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/compute_metrics.py @@ -0,0 +1,59 @@ +# Copyright (c) 2022 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import fire +import numpy as np + +from wespeaker.utils.score_metrics import (compute_pmiss_pfa_rbst, compute_eer, + compute_c_norm) + + +def compute_metrics(scores_file, p_target=0.01, c_miss=1, c_fa=1): + scores = [] + labels = [] + + with open(scores_file) as readlines: + for line in readlines: + tokens = line.strip().split() + # assert len(tokens) == 4 + scores.append(float(tokens[2])) + labels.append(tokens[3] == 'target') + + scores = np.hstack(scores) + labels = np.hstack(labels) + + fnr, fpr = compute_pmiss_pfa_rbst(scores, labels) + eer, thres = compute_eer(fnr, fpr, scores) + + min_dcf = compute_c_norm(fnr, + fpr, + p_target=p_target, + c_miss=c_miss, + c_fa=c_fa) + print("---- {} -----".format(os.path.basename(scores_file))) + print("EER = {0:.3f}".format(100 * eer)) + print("minDCF (p_target:{} c_miss:{} c_fa:{}) = {:.3f}".format( + p_target, c_miss, c_fa, min_dcf)) + + +def main(p_target=0.01, c_miss=1, c_fa=1, *scores_files): + for scores_file in scores_files: + compute_metrics(scores_file, p_target, c_miss, c_fa) + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/eval_plda.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/eval_plda.py new file mode 100644 index 0000000000000000000000000000000000000000..13faaf5a8d94d745c33ab1bd898a6151f8778562 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/eval_plda.py @@ -0,0 +1,50 @@ +# Copyright (c) 2022 Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +from wespeaker.utils.plda.two_cov_plda import TwoCovPLDA + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--type', + type=str, + default='2cov', + help='which type of plda to use, 2cov|kaldi') + parser.add_argument('--enroll_scp_path', + type=str, + help='enroll embeddings') + parser.add_argument('--indomain_scp_path', + type=str, + help='embeddings to compute meanvec') + parser.add_argument('--test_scp_path', type=str, help='test embeddings') + parser.add_argument('--utt2spk', + type=str, + help='utt2spk for the enroll speakers') + parser.add_argument('--model_path', type=str, help='pretrained plda path') + parser.add_argument('--score_path', + type=str, + help='score file to write to') + parser.add_argument('--trial', type=str, help='trial file to score upon') + parser.add_argument('--multisession_avg', default=False, action="store_true", + help='Whether to score multisession by average instead ' + 'of by-the-book. Default False.') + + args = parser.parse_args() + + kaldi_format = True if args.type == 'kaldi' else False + plda = TwoCovPLDA.load_model(args.model_path, kaldi_format) + plda.eval_sv(args.enroll_scp_path, args.utt2spk, args.test_scp_path, + args.trial, args.score_path, args.multisession_avg, + args.indomain_scp_path) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/export_jit.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/export_jit.py new file mode 100644 index 0000000000000000000000000000000000000000..7516d4665612d4828c9ee78760b1a3b92f2f298a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/export_jit.py @@ -0,0 +1,69 @@ +# Copyright (c) 2020 Mobvoi Inc. (authors: Binbin Zhang, Di Wu) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function + +import argparse +import os + +import torch +import yaml + +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.utils.checkpoint import load_checkpoint + + +def get_args(): + parser = argparse.ArgumentParser(description='export your script model') + parser.add_argument('--config', required=True, help='config file') + parser.add_argument('--checkpoint', required=True, help='checkpoint model') + parser.add_argument('--output_file', required=True, help='output file') + parser.add_argument('--output_quant_file', + default=None, + help='output quantized model file') + args = parser.parse_args() + return args + + +def main(): + args = get_args() + # No need gpu for model export + os.environ['CUDA_VISIBLE_DEVICES'] = '-1' + + with open(args.config, 'r') as fin: + configs = yaml.load(fin, Loader=yaml.FullLoader) + model = get_speaker_model(configs['model'])(**configs['model_args']) + print(model) + + load_checkpoint(model, args.checkpoint) + model.eval() + # Export jit torch script model + + script_model = torch.jit.script(model) + script_model.save(args.output_file) + print('Export model successfully, see {}'.format(args.output_file)) + + # Export quantized jit torch script model + if args.output_quant_file: + quantized_model = torch.quantization.quantize_dynamic( + model, {torch.nn.Linear}, dtype=torch.qint8) + print(quantized_model) + script_quant_model = torch.jit.script(quantized_model) + script_quant_model.save(args.output_quant_file) + print('Export quantized model successfully, ' + 'see {}'.format(args.output_quant_file)) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/export_mnn.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/export_mnn.py new file mode 100644 index 0000000000000000000000000000000000000000..30490b78400abcceadb29a6e34d733989c868c4e --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/export_mnn.py @@ -0,0 +1,96 @@ +# Copyright (c) 2024, Chengdong Liang(liangchengdongd@qq.com) +# All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function + +import os +import argparse + +import numpy as np +import torch +import MNN +import onnxruntime as ort + + +def get_args(): + parser = argparse.ArgumentParser(description='export your script model') + parser.add_argument('--onnx_model', required=True, help='onnx model') + parser.add_argument('--output_model', required=True, help='output file') + parser.add_argument('--num_frames', + default=-1, + type=int, + help='fix number of frames') + args = parser.parse_args() + return args + + +def test_onnx_inference(in0, model_path): + so = ort.SessionOptions() + so.inter_op_num_threads = 1 + so.intra_op_num_threads = 1 + session = ort.InferenceSession(model_path, sess_options=so) + output = session.run(output_names=['embs'], input_feed={'feats': in0}) + return output[0] + + +def test_mnn_inference(in0, model_path): + config = {} + config["precision"] = "high" + config["backend"] = 0 + config["numThread"] = 1 + + rt = MNN.nn.create_runtime_manager((config, )) + net = MNN.nn.load_module_from_file(model_path, ["feats"], ["embs"], + runtime_manager=rt) + + input_tensor = MNN.expr.convert(in0, MNN.expr.NC4HW4) + output_tensor = net.forward(input_tensor) + output_tensor = MNN.expr.convert(output_tensor, MNN.expr.NCHW) + output = output_tensor.read() + return output + + +def main(): + args = get_args() + # 1. convert onnx to mnn + if args.num_frames > 0: + os.system( + "MNNConvert -f ONNX --modelFile {} --MNNModel {} --bizCode MNN \ + --saveStaticModel".format(args.onnx_model, args.output_model)) + else: + os.system( + "MNNConvert -f ONNX --modelFile {} --MNNModel {} --bizCode MNN". + format(args.onnx_model, args.output_model)) + print("Exported MNN model to ", args.output_model) + # 2. print model info + os.system("MNNConvert -f MNN --modelFile {} --info".format( + args.output_model)) + # 3. check precision + torch.manual_seed(0) + if args.num_frames > 0: + in0 = torch.rand(1, args.num_frames, 80, dtype=torch.float) + else: + in0 = torch.rand(1, 200, 80, dtype=torch.float) + mnn_out = test_mnn_inference(in0.numpy(), args.output_model) + onnx_out = test_onnx_inference(in0.numpy(), args.onnx_model) + if np.allclose(onnx_out, mnn_out, rtol=1e-05, atol=1e-02): + print("Export mnn model successfully, " + "and the output accuracy check passed!") + else: + print("Export mnn model successfully, but onnx and mnn have different" + " outputs when given the same input, please check!") + + +if __name__ == "__main__": + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/export_onnx.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/export_onnx.py new file mode 100644 index 0000000000000000000000000000000000000000..b1665936006165950748ee9d35a746aeb2bedb06 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/export_onnx.py @@ -0,0 +1,113 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. +# Shuai Wang (wsstriving@gmail.com) +# All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import print_function + +import argparse + +import numpy as np +import torch +import torch.nn as nn +import yaml + +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.utils.checkpoint import load_checkpoint + + +def get_args(): + parser = argparse.ArgumentParser(description='export your script model') + parser.add_argument('--config', required=True, help='config file') + parser.add_argument('--checkpoint', required=True, help='checkpoint model') + parser.add_argument('--output_model', required=True, help='output file') + parser.add_argument('--num_frames', + default=-1, + type=int, + help='fix number of frames') + parser.add_argument('--mean_vec', + required=False, + default=None, + help='mean vector') + args = parser.parse_args() + return args + + +def main(): + args = get_args() + + with open(args.config, 'r') as fin: + configs = yaml.load(fin, Loader=yaml.FullLoader) + + model = get_speaker_model(configs['model'])(**configs['model_args']) + load_checkpoint(model, args.checkpoint) + model.eval() + + if args.mean_vec: + mean_vec = torch.tensor(np.load(args.mean_vec), dtype=torch.float32) + else: + embed_dim = configs["model_args"]["embed_dim"] + mean_vec = torch.zeros(embed_dim, dtype=torch.float32) + + class Model(nn.Module): + + def __init__(self, model, mean_vec=None): + super(Model, self).__init__() + self.model = model + self.register_buffer("mean_vec", mean_vec) + + def forward(self, feats): + outputs = self.model(feats) # embed or (embed_a, embed_b) + embeds = outputs[-1] if isinstance(outputs, tuple) else outputs + embeds = embeds - self.mean_vec + return embeds + + model = Model(model, mean_vec) + model.eval() + + feat_dim = configs['model_args'].get('feat_dim', 80) + if 'feature_args' in configs: # deprecated IO + num_frms = configs['feature_args'].get('num_frms', 200) + else: # UIO + num_frms = configs['dataset_args'].get('num_frms', 200) + + if args.num_frames > 0: + num_frms = args.num_frames + dynamic_axes = None + else: + dynamic_axes = {'feats': {0: 'B', 1: 'T'}, 'embs': {0: 'B'}} + + dummy_input = torch.ones(1, num_frms, feat_dim) + torch.onnx.export(model, + dummy_input, + args.output_model, + do_constant_folding=True, + verbose=False, + opset_version=14, + input_names=['feats'], + output_names=['embs'], + dynamic_axes=dynamic_axes) + + # You may further generate tensorrt engine: + # trtexec --onnx=avg_model.onnx --minShapes=feats:1x200x80 \ + # --optShapes=feats:64x200x80 --maxShapes=feats:128x200x80 \ + # --fp16 + # Notice T = 200 is not a must, you may change it to other size: + # trtexec --onnx=avg_model.onnx --minShapes=feats:1x100x80 \ + # --optShapes=feats:64x200x80 --maxShapes=feats:128x500x80 \ + # --fp16 + # If it is an model with QDQ nodes, please add --int8 + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/extract.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/extract.py new file mode 100644 index 0000000000000000000000000000000000000000..dd3608520f0ba79a3d27662e8f20b08ba80fbc88 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/extract.py @@ -0,0 +1,143 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +import os + +import fire +import kaldiio +import torch +from torch.utils.data import DataLoader +from tqdm import tqdm + +from wespeaker.dataset.dataset import Dataset +from wespeaker.dataset.dataset_utils import apply_cmvn, spec_aug +from wespeaker.frontend import * +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.utils.checkpoint import load_checkpoint +from wespeaker.utils.utils import parse_config_or_kwargs, validate_path + + +def extract(config='conf/config.yaml', **kwargs): + # parse configs first + configs = parse_config_or_kwargs(config, **kwargs) + + model_path = configs['model_path'] + embed_ark = configs['embed_ark'] + batch_size = configs.get('batch_size', 1) + num_workers = configs.get('num_workers', 1) + + # Since the input length is not fixed, we set the built-in cudnn + # auto-tuner to False + torch.backends.cudnn.benchmark = False + + test_conf = copy.deepcopy(configs['dataset_args']) + frontend_type = test_conf.get('frontend', 'fbank') + + if frontend_type == 'tfmel': + print('Loading checkpoint for tfmel model ...') + ckpt = torch.load(model_path, map_location='cpu', weights_only=False) + if 'model_config' in ckpt: + model_config = ckpt['model_config'].copy() + model_config['spec'] = None + else: + model_config = configs['model_args'] + model = get_speaker_model(configs['model'])(**model_config) + print(f'Initializing {frontend_type} frontend ...') + frontend_args_dict = test_conf.get(frontend_type + "_args", {}).copy() + frontend_args_dict.setdefault('sample_rate', + test_conf['resample_rate']) + frontend = frontend_class_dict[frontend_type](**frontend_args_dict) + model.add_module("frontend", frontend) + if hasattr(model, 'prepare_for_frontend'): + model.prepare_for_frontend(frontend_type) + load_checkpoint(model, model_path) + else: + model = get_speaker_model(configs['model'])(**configs['model_args']) + if frontend_type != 'fbank': + frontend_args = frontend_type + "_args" + print('Initializing frontend model (this could take some time) ...') + frontend = frontend_class_dict[frontend_type]( + **test_conf[frontend_args], sample_rate=test_conf['resample_rate']) + model.add_module("frontend", frontend) + print('Loading checkpoint ...') + load_checkpoint(model, model_path) + print('Finished !!! Start extracting ...') + device = torch.device("cuda") + model.to(device).eval() + + # test_configs + # test_conf = copy.deepcopy(configs['dataset_args']) + test_conf['speed_perturb'] = False + if 'fbank_args' in test_conf: + test_conf['fbank_args']['dither'] = 0.0 + test_conf['spec_aug'] = False + test_conf['shuffle'] = False + test_conf['aug_prob'] = configs.get('aug_prob', 0.0) + test_conf['filter'] = False + + dataset = Dataset(configs['data_type'], + configs['data_list'], + test_conf, + spk2id_dict={}, + whole_utt=(batch_size == 1), + reverb_lmdb_file=configs.get('reverb_data', None), + noise_lmdb_file=configs.get('noise_data', None), + repeat_dataset=False) + dataloader = DataLoader(dataset, + shuffle=False, + batch_size=batch_size, + num_workers=num_workers, + prefetch_factor=4) + + validate_path(embed_ark) + embed_ark = os.path.abspath(embed_ark) + embed_scp = embed_ark[:-3] + "scp" + + with torch.no_grad(): + with kaldiio.WriteHelper('ark,scp:' + embed_ark + "," + + embed_scp) as writer: + for _, batch in tqdm(enumerate(dataloader)): + utts = batch['key'] + if frontend_type == 'fbank': + features = batch['feat'] + features = features.float().to(device) # (B,T,F) + else: # 's3prl', 'tfmel', etc. + wavs = batch['wav'] # (B,1,W) + wavs = wavs.squeeze(1).float().to(device) # (B,W) + wavs_len = torch.LongTensor([wavs.shape[1]]).repeat( + wavs.shape[0]).to(device) # (B) + features, _ = model.frontend(wavs, wavs_len) + + # apply cmvn + if test_conf.get('cmvn', True): + features = apply_cmvn(features, + **test_conf.get('cmvn_args', {})) + # spec augmentation + if test_conf.get('spec_aug', False): + features = spec_aug(features, **test_conf['spec_aug_args']) + + # Forward through model + outputs = model(features) # embed or (embed_a, embed_b) + embeds = outputs[-1] if isinstance(outputs, tuple) else outputs + embeds = embeds.cpu().detach().numpy() # (B,F) + + for i, utt in enumerate(utts): + embed = embeds[i] + writer(utt, embed) + + +if __name__ == '__main__': + fire.Fire(extract) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/extract_deprecated.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/extract_deprecated.py new file mode 100644 index 0000000000000000000000000000000000000000..4cff0226b141c6e3efd2d3c810aa5939852dc608 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/extract_deprecated.py @@ -0,0 +1,93 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import fire +import kaldiio +import torch +from torch.utils.data import DataLoader +from tqdm import tqdm + +from wespeaker.dataset.dataset_deprecated import FeatList_LableDict_Dataset +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.utils.checkpoint import load_checkpoint +from wespeaker.utils.file_utils import read_scp +from wespeaker.utils.utils import parse_config_or_kwargs, validate_path + + +def extract(config='conf/config.yaml', **kwargs): + # parse configs first + configs = parse_config_or_kwargs(config, **kwargs) + + model_path = configs['model_path'] + data_scp = configs['data_scp'] + embed_ark = configs['embed_ark'] + batch_size = configs.get('batch_size', 1) + num_workers = configs.get('num_workers', 1) + raw_wav = configs.get('raw_wav', True) + feat_dim = configs['feature_args'].get('feat_dim', 80) + num_frms = configs['feature_args'].get('num_frms', 200) + + # Since the input length is not fixed, we set the built-in cudnn + # auto-tuner to False + torch.backends.cudnn.benchmark = False + + model = get_speaker_model(configs['model'])(**configs['model_args']) + load_checkpoint(model, model_path) + device = torch.device("cuda") + model.to(device).eval() + + # prepare dataset and dataloader + data_list = read_scp(data_scp) + dataset = FeatList_LableDict_Dataset(data_list, + utt2spkid_dict={}, + whole_utt=(batch_size == 1), + raw_wav=raw_wav, + feat_dim=feat_dim, + num_frms=num_frms) + dataloader = DataLoader(dataset, + shuffle=False, + batch_size=batch_size, + num_workers=num_workers, + prefetch_factor=4) + + validate_path(embed_ark) + embed_ark = os.path.abspath(embed_ark) + embed_scp = embed_ark[:-3] + "scp" + + with torch.no_grad(): + with kaldiio.WriteHelper('ark,scp:' + embed_ark + "," + + embed_scp) as writer: + t_bar = tqdm(ncols=100, + total=len(dataloader), + desc='extract_embed: ') + for i, (utts, feats, _) in enumerate(dataloader): + t_bar.update() + + feats = feats.float().to(device) # (B,T,F) + # Forward through model + outputs = model(feats) # embed or (embed_a, embed_b) + embeds = outputs[-1] if isinstance(outputs, tuple) else outputs + embeds = embeds.cpu().detach().numpy() # (B,F) + + for i, utt in enumerate(utts): + embed = embeds[i] + writer(utt, embed) + + t_bar.close() + + +if __name__ == '__main__': + fire.Fire(extract) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/infer_onnx.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/infer_onnx.py new file mode 100644 index 0000000000000000000000000000000000000000..eb89efbe9a255b43117d14d1d2f5c4dd520c1e54 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/infer_onnx.py @@ -0,0 +1,72 @@ +# Copyright (c) 2022, Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +import onnxruntime as ort +import torch +import torchaudio +import torchaudio.compliance.kaldi as kaldi + + +def get_args(): + parser = argparse.ArgumentParser(description='infer example using onnx') + parser.add_argument('--onnx_path', required=True, help='onnx path') + parser.add_argument('--wav_path', required=True, help='wav path') + args = parser.parse_args() + return args + + +def compute_fbank(wav_path, + num_mel_bins=80, + frame_length=25, + frame_shift=10, + dither=0.0): + """ Extract fbank, simlilar to the one in wespeaker.dataset.processor, + While integrating the wave reading and CMN. + """ + waveform, sample_rate = torchaudio.load(wav_path) + waveform = waveform * (1 << 15) + mat = kaldi.fbank(waveform, + num_mel_bins=num_mel_bins, + frame_length=frame_length, + frame_shift=frame_shift, + dither=dither, + sample_frequency=sample_rate, + window_type='hamming', + use_energy=False) + # CMN, without CVN + mat = mat - torch.mean(mat, dim=0) + return mat + + +def main(): + args = get_args() + + so = ort.SessionOptions() + so.inter_op_num_threads = 1 + so.intra_op_num_threads = 1 + session = ort.InferenceSession(args.onnx_path, sess_options=so) + + wav_path = args.wav_path + feats = compute_fbank(wav_path) + feats = feats.unsqueeze(0).numpy() # add batch dimension + + embeddings = session.run(output_names=['embs'], + input_feed={'feats': feats}) + print(embeddings[0].shape) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/prep_embd_proc.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/prep_embd_proc.py new file mode 100644 index 0000000000000000000000000000000000000000..445eab0809041e2086c88bf5dd1252dd219ebdac --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/prep_embd_proc.py @@ -0,0 +1,32 @@ +# Copyright (c) 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +from wespeaker.utils.embedding_processing import EmbeddingProcessingChain + +if __name__ == '__main__': + """ + xxx + """ + parser = argparse.ArgumentParser() + parser.add_argument('--chain', + type=str, + default='whitening | length-norm ', + help='') + parser.add_argument('--path', type=str) + args = parser.parse_args() + + processingChain = EmbeddingProcessingChain(chain=args.chain) + processingChain.save(args.path) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/score.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/score.py new file mode 100644 index 0000000000000000000000000000000000000000..d91153e80333ca2734f075fd33d4a2a66409e01c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/score.py @@ -0,0 +1,95 @@ +# Copyright (c) 2022 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from pathlib import Path + +import fire +import kaldiio +import numpy as np +from sklearn.metrics.pairwise import cosine_similarity +from tqdm import tqdm + + +def calculate_mean_from_kaldi_vec(scp_path): + vec_num = 0 + mean_vec = None + + for _, vec in kaldiio.load_scp_sequential(scp_path): + if mean_vec is None: + mean_vec = np.zeros_like(vec) + mean_vec += vec + vec_num += 1 + + return mean_vec / vec_num + + +def trials_cosine_score(eval_scp_path='', + store_dir='', + mean_vec=None, + trials=()): + if mean_vec is None or not os.path.exists(mean_vec): + mean_vec = 0.0 + else: + mean_vec = np.load(mean_vec) + + # each embedding may be accessed multiple times, here we pre-load them + # into the memory + emb_dict = {} + for utt, emb in kaldiio.load_scp_sequential(eval_scp_path): + emb = emb - mean_vec + emb_dict[utt] = emb + + for trial in trials: + store_path = os.path.join(store_dir, + os.path.basename(trial) + '.score') + with open(trial, 'r') as trial_r, open(store_path, 'w') as w_f: + lines = trial_r.readlines() + for line in tqdm(lines, + desc='scoring trial {}'.format( + os.path.basename(trial))): + segs = line.strip().split() + emb1, emb2 = emb_dict[segs[0]], emb_dict[segs[1]] + cos_score = cosine_similarity(emb1.reshape(1, -1), + emb2.reshape(1, -1))[0][0] + + if len(segs) == 3: # enroll_name test_name target/nontarget + w_f.write('{} {} {:.5f} {}\n'.format( + segs[0], segs[1], cos_score, segs[2])) + else: # enroll_name test_name + w_f.write('{} {} {:.5f}\n'.format(segs[0], segs[1], + cos_score)) + + +def main(exp_dir, eval_scp_path, cal_mean, cal_mean_dir, *trials): + + print(cal_mean) + if not cal_mean: + print("Do not do mean normalization for evaluation embeddings.") + mean_vec_path = None + else: + scp_path = os.path.join(cal_mean_dir, 'xvector.scp') + print("Calculate mean statistics from {}.".format(scp_path)) + mean_vec = calculate_mean_from_kaldi_vec(scp_path) + mean_vec_path = os.path.join(cal_mean_dir, 'mean_vec.npy') + np.save(mean_vec_path, mean_vec) + + # scoring trials + store_score_dir = os.path.join(exp_dir, 'scores') + Path(store_score_dir).mkdir(parents=True, exist_ok=True) + trials_cosine_score(eval_scp_path, store_score_dir, mean_vec_path, trials) + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/score_calibration.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/score_calibration.py new file mode 100644 index 0000000000000000000000000000000000000000..d095ae4d50a061f9c803f40d052c95c38f5bc10f --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/score_calibration.py @@ -0,0 +1,168 @@ +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2024 Bing Han (hanbing97@sjtu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import fire +import numpy as np +from tqdm import tqdm + +import torch +import torch.nn as nn +import torch.optim as optim + +from wespeaker.utils.file_utils import read_table + + +def gather_calibration_factors(wav_dur_scp, max_dur, score_norm_file, + calibration_factor_file, drop_duration=False): + if not drop_duration: + wav_idx, dur_list = zip(*read_table(wav_dur_scp)) + wavidx2dur = { + idx: min(float(dur), max_dur) + for idx, dur in zip(wav_idx, dur_list) + } + + def reorder_values(value_1, value_2): + max_value = max(value_1, value_2) + min_value = min(value_1, value_2) + return "{:.4f} {:.4f} {:.4f} {:.4f}".format(min_value, max_value, + max_value - min_value, + max_value / min_value) + + # read factor from asnorm results + assert os.path.exists( + score_norm_file), "score norm file ({}) does not exist !!!".format( + score_norm_file) + + with open(score_norm_file, 'r', encoding='utf-8') as fin: + with open(calibration_factor_file, 'w', encoding='utf-8') as fout: + lines = fin.readlines() + for line in tqdm(lines): + line = line.strip().split() + idx1, idx2 = line[0], line[1] + if drop_duration: + dur_str = "" + else: + dur_str = reorder_values(wavidx2dur[idx1], wavidx2dur[idx2]) + mag_str = reorder_values(float(line[4]), float(line[5])) + cohort_mean_str = reorder_values(float(line[6]), + float(line[7])) + fout.write('{} {} {} {} {} {} {}\n'.format( + line[0], line[1], line[3], line[2], dur_str, mag_str, + cohort_mean_str)) + + +class LinearModel(nn.Module): + + def __init__(self, input_dim): + super(LinearModel, self).__init__() + self.linear = nn.Linear(input_dim, 1) + nn.init.constant_(self.linear.weight, 1.0 / input_dim) + nn.init.constant_(self.linear.bias, 0) + + def forward(self, x): + out = self.linear(x) + return out + + +def cllr(target_llrs, nontarget_llrs): + """ + Calculate the CLLR of the scores + """ + + def negative_log_sigmoid(lodds): + """-log(sigmoid(log_odds))""" + return torch.log1p(torch.exp(-lodds)) + + return 0.5 * (torch.mean(negative_log_sigmoid(target_llrs)) + torch.mean( + negative_log_sigmoid(-nontarget_llrs))) / np.log(2) + + +def train_calibration_model(calibration_factor_file, save_model_path): + max_epochs = 50 + target_llrs_list = [] + nontarget_llrs_list = [] + with open(calibration_factor_file, 'r', encoding='utf-8') as fin: + lines = fin.readlines() + for line in lines: + line = line.strip().split() + if line[2] == "tgt" or line[2] == "target": + target_llrs_list.append([float(v) for v in line[3:]]) + else: + nontarget_llrs_list.append([float(v) for v in line[3:]]) + + # build training set + target_llrs = torch.tensor(target_llrs_list, dtype=torch.float64) + nontarget_llrs = torch.tensor(nontarget_llrs_list, dtype=torch.float64) + start_cllr = cllr(target_llrs, nontarget_llrs) + + # create model + model = LinearModel(target_llrs.shape[-1]) + model.double() + criterion = cllr + + # build optimizer + optimizer = optim.LBFGS(model.parameters(), lr=0.01) + + best_loss = 1000000.0 + for i in range(max_epochs): + + def closure(): + optimizer.zero_grad() + new_nontarget_llrs = model(nontarget_llrs) + new_target_llrs = model(target_llrs) + loss = criterion(new_target_llrs, new_nontarget_llrs) + loss.backward() + return loss + + loss = optimizer.step(closure) + if (best_loss - loss < 1e-4): + break + else: + if loss < best_loss: + best_loss = loss + + torch.save(model.state_dict(), save_model_path) + + +def infer_calibration(calibration_factor_file, save_model_path, + calibration_score_file): + llrs_list = [] + with open(calibration_factor_file, 'r', encoding='utf-8') as fin: + lines = fin.readlines() + for line in lines: + line = line.strip().split() + llrs_list.append([float(v) for v in line[3:]]) + + llrs = torch.tensor(llrs_list, dtype=torch.float64) + + model = LinearModel(llrs.shape[-1]) + model.load_state_dict(torch.load(save_model_path)) + model.eval() + model.double() + outputs = model(llrs) + + with open(calibration_score_file, "w", encoding='utf-8') as fout: + for i, s in enumerate(lines): + line = lines[i].strip().split() + score = outputs[i].item() + fout.write('{} {} {} {}\n'.format(line[0], line[1], score, + line[2])) + + +if __name__ == "__main__": + fire.Fire() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/score_norm.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/score_norm.py new file mode 100644 index 0000000000000000000000000000000000000000..73b85121dbe196aa96059e67816d1f2b646d956b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/score_norm.py @@ -0,0 +1,119 @@ +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os + +import fire +import kaldiio +import numpy as np +from tqdm import tqdm + +from wespeaker.utils.file_utils import read_table + + +def get_mean_std(emb, cohort, top_n): + emb = emb / np.sqrt(np.sum(emb**2, axis=1, keepdims=True)) + cohort = cohort / np.sqrt(np.sum(cohort**2, axis=1, keepdims=True)) + emb_cohort_score = np.matmul(emb, cohort.T) + emb_cohort_score = np.sort(emb_cohort_score, axis=1)[:, ::-1] + emb_cohort_score_topn = emb_cohort_score[:, :top_n] + + emb_mean = np.mean(emb_cohort_score_topn, axis=1) + emb_std = np.std(emb_cohort_score_topn, axis=1) + + return emb_mean, emb_std + + +def split_embedding(utt_list, emb_scp, mean_vec): + embs = [] + utt2idx = {} + utt2emb = {} + for utt, emb in kaldiio.load_scp_sequential(emb_scp): + emb = emb - mean_vec + utt2emb[utt] = emb + + for utt in utt_list: + embs.append(utt2emb[utt]) + utt2idx[utt] = len(embs) - 1 + + return np.array(embs), utt2idx + + +def main(score_norm_method, + top_n, + trial_score_file, + score_norm_file, + cohort_emb_scp, + eval_emb_scp, + mean_vec_path=None): + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(levelname)s %(message)s') + # get embedding + if not mean_vec_path: + print("Do not do mean normalization for evaluation embeddings.") + mean_vec = 0.0 + else: + assert os.path.exists( + mean_vec_path), "mean_vec file ({}) does not exist !!!".format( + mean_vec_path) + mean_vec = np.load(mean_vec_path) + + # get embedding + logging.info('get embedding ...') + + enroll_list, test_list, _, _ = zip(*read_table(trial_score_file)) + enroll_list = sorted(list(set(enroll_list))) # remove overlap and sort + test_list = sorted(list(set(test_list))) + enroll_emb, enroll_utt2idx = split_embedding(enroll_list, eval_emb_scp, + mean_vec) + test_emb, test_utt2idx = split_embedding(test_list, eval_emb_scp, mean_vec) + + cohort_list, _ = zip(*read_table(cohort_emb_scp)) + cohort_emb, _ = split_embedding(cohort_list, cohort_emb_scp, mean_vec) + + logging.info("computing normed score ...") + if score_norm_method == "asnorm": + top_n = top_n + elif score_norm_method == "snorm": + top_n = cohort_emb.shape[0] + else: + raise ValueError(score_norm_method) + enroll_mean, enroll_std = get_mean_std(enroll_emb, cohort_emb, top_n) + test_mean, test_std = get_mean_std(test_emb, cohort_emb, top_n) + + # score norm + with open(trial_score_file, 'r', encoding='utf-8') as fin: + with open(score_norm_file, 'w', encoding='utf-8') as fout: + lines = fin.readlines() + for line in tqdm(lines): + line = line.strip().split() + enroll_idx = enroll_utt2idx[line[0]] + test_idx = test_utt2idx[line[1]] + score = float(line[2]) + normed_score = 0.5 * ( + (score - enroll_mean[enroll_idx]) / enroll_std[enroll_idx] + + (score - test_mean[test_idx]) / test_std[test_idx]) + # compute mag mean for score calibration + enroll_mag = np.linalg.norm(enroll_emb[enroll_idx]) + test_mag = np.linalg.norm(test_emb[test_idx]) + fout.write( + '{} {} {:.5f} {} {:.4f} {:.4f} {:.4f} {:.4f}\n'.format( + line[0], line[1], normed_score, line[3], enroll_mag, + test_mag, enroll_mean[enroll_idx], + test_mean[test_idx])) + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/train.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/train.py new file mode 100644 index 0000000000000000000000000000000000000000..7700d63c5c6c7721d79b69ba6f01d734c988efa3 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/train.py @@ -0,0 +1,266 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +from pprint import pformat + +import fire +import tableprint as tp +import torch +import torch.distributed as dist +import yaml +from torch.utils.data import DataLoader + +import wespeaker.utils.schedulers as schedulers +from wespeaker.dataset.dataset import Dataset +from wespeaker.frontend import * +from wespeaker.models.projections import get_projection +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.utils.checkpoint import load_checkpoint, save_checkpoint +from wespeaker.utils.executor import run_epoch +from wespeaker.utils.file_utils import read_table +from wespeaker.utils.utils import get_logger, parse_config_or_kwargs, set_seed, \ + spk2id + + +def train(config='conf/config.yaml', **kwargs): + """Trains a model on the given features and spk labels. + + :config: A training configuration. Note that all parameters in the + config can also be manually adjusted with --ARG VALUE + :returns: None + """ + configs = parse_config_or_kwargs(config, **kwargs) + checkpoint = configs.get('checkpoint', None) + # dist configs + local_rank = int(os.environ.get('LOCAL_RANK', 0)) + rank = int(os.environ.get('RANK', 0)) + world_size = int(os.environ['WORLD_SIZE']) + gpu = int(configs['gpus'][local_rank]) + torch.cuda.set_device(gpu) + dist.init_process_group(backend='nccl') + + model_dir = os.path.join(configs['exp_dir'], "models") + if rank == 0: + try: + os.makedirs(model_dir) + except IOError: + print("[warning] " + model_dir + " already exists !!!") + if checkpoint is None: + print("[error] checkpoint is null !") + exit(1) + dist.barrier(device_ids=[gpu]) # let the rank 0 mkdir first + + logger = get_logger(configs['exp_dir'], 'train.log') + if world_size > 1: + logger.info('training on multiple gpus, this gpu {}'.format(gpu)) + + if rank == 0: + logger.info("exp_dir is: {}".format(configs['exp_dir'])) + logger.info("<== Passed Arguments ==>") + # Print arguments into logs + for line in pformat(configs).split('\n'): + logger.info(line) + + # seed + set_seed(configs['seed'] + rank) + + # train data + train_label = configs['train_label'] + train_utt_spk_list = read_table(train_label) + spk2id_dict = spk2id(train_utt_spk_list) + if rank == 0: + logger.info("<== Data statistics ==>") + logger.info("train data num: {}, spk num: {}".format( + len(train_utt_spk_list), len(spk2id_dict))) + + # dataset and dataloader + train_dataset = Dataset(configs['data_type'], + configs['train_data'], + configs['dataset_args'], + spk2id_dict, + reverb_lmdb_file=configs.get('reverb_data', None), + noise_lmdb_file=configs.get('noise_data', None)) + train_dataloader = DataLoader(train_dataset, **configs['dataloader_args']) + batch_size = configs['dataloader_args']['batch_size'] + if configs['dataset_args'].get('sample_num_per_epoch', 0) > 0: + sample_num_per_epoch = configs['dataset_args']['sample_num_per_epoch'] + else: + sample_num_per_epoch = len(train_utt_spk_list) + if (configs['data_type'] != 'feat' + and configs['dataset_args'].get('speed_perturb', False) + and configs['dataset_args'].get('speed_perturb_mode') + == 'expanded'): + sample_num_per_epoch *= 3 + epoch_iter = sample_num_per_epoch // world_size // batch_size + if rank == 0: + logger.info("<== Dataloaders ==>") + logger.info("train dataloaders created") + logger.info('epoch iteration number: {}'.format(epoch_iter)) + + # model: frontend (optional) => speaker model => projection layer + logger.info("<== Model ==>") + frontend_type = configs['dataset_args'].get('frontend', 'fbank') + if frontend_type != "fbank": + frontend_args = frontend_type + "_args" + frontend = frontend_class_dict[frontend_type]( + **configs['dataset_args'][frontend_args], + sample_rate=configs['dataset_args']['resample_rate']) + configs['model_args']['feat_dim'] = frontend.output_size() + model = get_speaker_model(configs['model'])(**configs['model_args']) + model.add_module("frontend", frontend) + else: + model = get_speaker_model(configs['model'])(**configs['model_args']) + if rank == 0: + num_params = sum(param.numel() for param in model.parameters()) + logger.info('speaker_model size: {}'.format(num_params)) + # For model_init, only frontend and speaker model are needed !!! + if configs['model_init'] is not None: + logger.info('Load initial model from {}'.format(configs['model_init'])) + load_checkpoint(model, configs['model_init']) + elif checkpoint is None: + logger.info('Train model from scratch ...') + # projection layer + configs['projection_args']['embed_dim'] = configs['model_args'][ + 'embed_dim'] + configs['projection_args']['num_class'] = len(spk2id_dict) + configs['projection_args']['do_lm'] = configs.get('do_lm', False) + if configs['data_type'] != 'feat' and configs['dataset_args'][ + 'speed_perturb']: + # diff speed is regarded as diff spk + configs['projection_args']['num_class'] *= 3 + if configs.get('do_lm', False): + logger.info( + 'No speed perturb while doing large margin fine-tuning') + configs['dataset_args']['speed_perturb'] = False + projection = get_projection(configs['projection_args']) + model.add_module("projection", projection) + if rank == 0: + # print model + for line in pformat(model).split('\n'): + logger.info(line) + # !!!IMPORTANT!!! + # Try to export the model by script, if fails, we should refine + # the code to satisfy the script export requirements + if frontend_type == 'fbank': + try: + script_model = torch.jit.script(model) + script_model.save(os.path.join(model_dir, 'init.zip')) + except Exception as e: + logger.warning(f"Failed to save jit script: {e}") + + # If specify checkpoint, load some info from checkpoint. + # For checkpoint, frontend, speaker model, and projection layer + # are all needed !!! + if checkpoint is not None: + load_checkpoint(model, checkpoint) + start_epoch = int(re.findall(r"(?<=model_)\d*(?=.pt)", + checkpoint)[0]) + 1 + logger.info('Load checkpoint: {}'.format(checkpoint)) + else: + start_epoch = 1 + logger.info('start_epoch: {}'.format(start_epoch)) + + # ddp_model + model.cuda() + ddp_model = torch.nn.parallel.DistributedDataParallel(model) + device = torch.device("cuda") + + criterion = getattr(torch.nn, configs['loss'])(**configs['loss_args']) + if rank == 0: + logger.info("<== Loss ==>") + logger.info("loss criterion is: " + configs['loss']) + + if 'initial_lr' in configs['scheduler_args']: + configs['optimizer_args']['lr'] = ( + configs['scheduler_args']['initial_lr'] + ) + optimizer = getattr(torch.optim, + configs['optimizer'])(ddp_model.parameters(), + **configs['optimizer_args']) + if rank == 0: + logger.info("<== Optimizer ==>") + logger.info("optimizer is: " + configs['optimizer']) + + # scheduler + configs['scheduler_args']['num_epochs'] = configs['num_epochs'] + configs['scheduler_args']['epoch_iter'] = epoch_iter + # here, we consider the batch_size 64 as the base, the learning rate will be + # adjusted according to the batchsize and world_size used in different setup + configs['scheduler_args']['scale_ratio'] = 1.0 * world_size * configs[ + 'dataloader_args']['batch_size'] / 64 + scheduler = getattr(schedulers, + configs['scheduler'])(optimizer, + **configs['scheduler_args']) + if rank == 0: + logger.info("<== Scheduler ==>") + logger.info("scheduler is: " + configs['scheduler']) + + # margin scheduler + configs['margin_update']['epoch_iter'] = epoch_iter + margin_scheduler = getattr(schedulers, configs['margin_scheduler'])( + model=model, **configs['margin_update']) + if rank == 0: + logger.info("<== MarginScheduler ==>") + + # save config.yaml + if rank == 0: + saved_config_path = os.path.join(configs['exp_dir'], 'config.yaml') + with open(saved_config_path, 'w') as fout: + data = yaml.dump(configs) + fout.write(data) + + # training + dist.barrier(device_ids=[gpu]) # synchronize here + if rank == 0: + logger.info("<========== Training process ==========>") + header = ['Epoch', 'Batch', 'Lr', 'Margin', 'Loss', "Acc"] + for line in tp.header(header, width=10, style='grid').split('\n'): + logger.info(line) + dist.barrier(device_ids=[gpu]) # synchronize here + + scaler = torch.cuda.amp.GradScaler(enabled=configs['enable_amp']) + for epoch in range(start_epoch, configs['num_epochs'] + 1): + train_dataset.set_epoch(epoch) + + run_epoch(train_dataloader, + epoch_iter, + ddp_model, + criterion, + optimizer, + scheduler, + margin_scheduler, + epoch, + logger, + scaler, + device=device, + configs=configs) + + if rank == 0: + if epoch % configs['save_epoch_interval'] == 0 or epoch > configs[ + 'num_epochs'] - configs['num_avg']: + save_checkpoint( + model, os.path.join(model_dir, + 'model_{}.pt'.format(epoch))) + + if rank == 0: + os.symlink('model_{}.pt'.format(configs['num_epochs']), + os.path.join(model_dir, 'final_model.pt')) + logger.info(tp.bottom(len(header), width=10, style='grid')) + + +if __name__ == '__main__': + fire.Fire(train) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/train_deprecated.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/train_deprecated.py new file mode 100644 index 0000000000000000000000000000000000000000..bf1ca61e9cfd9b3c27a78de640ce94f278ef103a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/train_deprecated.py @@ -0,0 +1,231 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +from pprint import pformat + +import fire +import tableprint as tp +import torch +import torch.distributed as dist +import yaml +from torch.utils.data import DataLoader +from torch.utils.data.distributed import DistributedSampler + +import wespeaker.utils.schedulers as schedulers +from wespeaker.dataset.dataset_deprecated import FeatList_LableDict_Dataset +from wespeaker.models.projections import get_projection +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.utils.checkpoint import load_checkpoint, save_checkpoint +from wespeaker.utils.executor_deprecated import run_epoch +from wespeaker.utils.file_utils import read_scp +from wespeaker.utils.utils import get_logger, parse_config_or_kwargs, set_seed, \ + spk2id + + +def train(config='conf/config.yaml', **kwargs): + """Trains a model on the given features and spk labels. + + :config: A training configuration. Note that all parameters in the + config can also be manually adjusted with --ARG VALUE + :returns: None + """ + + configs = parse_config_or_kwargs(config, **kwargs) + checkpoint = configs.get('checkpoint', None) + # dist configs + rank = int(os.environ['LOCAL_RANK']) + world_size = int(os.environ['WORLD_SIZE']) + gpu = int(configs['gpus'][rank]) + os.environ['CUDA_VISIBLE_DEVICES'] = str(gpu) + dist.init_process_group(backend='nccl') + + model_dir = os.path.join(configs['exp_dir'], "models") + if rank == 0: + try: + os.makedirs(model_dir) + except IOError: + print(model_dir + " already exists !!!") + if checkpoint is None: + exit(1) + dist.barrier() # let the rank 0 mkdir first + + logger = get_logger(configs['exp_dir'], 'train.log') + if world_size > 1: + logger.info('training on multiple gpus, this gpu {}'.format(gpu)) + + if rank == 0: + logger.info("exp_dir is: {}".format(configs['exp_dir'])) + logger.info("<== Passed Arguments ==>") + # Print arguments into logs + for line in pformat(configs).split('\n'): + logger.info(line) + + # seed + set_seed(configs['seed'] + rank) + + # wav/feat + train_scp = configs['dataset_args']['train_scp'] + train_label = configs['dataset_args']['train_label'] + train_data_list = read_scp(train_scp) + if rank == 0: + logger.info("<== Feature ==>") + logger.info("train wav/feat num: {}".format(len(train_data_list))) + + # spk label + train_utt_spk_list = read_scp(train_label) + spk2id_dict = spk2id(train_utt_spk_list) + train_utt2spkid_dict = { + utt_spk[0]: spk2id_dict[utt_spk[1]] + for utt_spk in train_utt_spk_list + } + if rank == 0: + logger.info("<== Labels ==>") + logger.info("train label num: {}, spk num: {}".format( + len(train_utt2spkid_dict), len(spk2id_dict))) + + # dataset and dataloader + configs['feature_args']['feat_dim'] = configs['model_args']['feat_dim'] + train_dataset = FeatList_LableDict_Dataset(train_data_list, + train_utt2spkid_dict, + **configs['feature_args'], + **configs['dataset_args']) + train_sampler = DistributedSampler(train_dataset, shuffle=True) + train_dataloader = DataLoader(train_dataset, + sampler=train_sampler, + **configs['dataloader_args']) + if rank == 0: + logger.info("<== Dataloaders ==>") + logger.info("train dataloaders created") + + # model + logger.info("<== Model ==>") + model = get_speaker_model(configs['model'])(**configs['model_args']) + if configs['model_init'] is not None: + logger.info('Load initial model from {}'.format(configs['model_init'])) + load_checkpoint(model, configs['model_init']) + else: + logger.info('Train model from scratch...') + # projection layer + configs['projection_args']['embed_dim'] = configs['model_args'][ + 'embed_dim'] + configs['projection_args']['num_class'] = len(spk2id_dict) + if configs['feature_args']['raw_wav'] and configs['dataset_args'][ + 'speed_perturb']: + # diff speed is regarded as diff spk + configs['projection_args']['num_class'] *= 3 + configs['projection_args']['do_lm'] = configs.get('do_lm', False) + projection = get_projection(configs['projection_args']) + model.add_module("projection", projection) + if rank == 0: + # print model + for line in pformat(model).split('\n'): + logger.info(line) + # !!!IMPORTANT!!! + # Try to export the model by script, if fails, we should refine + # the code to satisfy the script export requirements + script_model = torch.jit.script(model) + script_model.save(os.path.join(model_dir, 'init.zip')) + + # If specify checkpoint, load some info from checkpoint. + if checkpoint is not None: + load_checkpoint(model, checkpoint) + start_epoch = int(re.findall(r"(?<=model_)\d*(?=.pt)", + checkpoint)[0]) + 1 + logger.info('checkpoint: {}'.format(checkpoint)) + else: + start_epoch = 1 + logger.info('start_epoch: {}'.format(start_epoch)) + + # ddp_model + model.cuda() + ddp_model = torch.nn.parallel.DistributedDataParallel(model) + device = torch.device("cuda") + + criterion = getattr(torch.nn, configs['loss'])(**configs['loss_args']) + if rank == 0: + logger.info("<== Loss ==>") + logger.info("loss criterion is: " + configs['loss']) + + configs['optimizer_args']['lr'] = configs['scheduler_args']['initial_lr'] + optimizer = getattr(torch.optim, + configs['optimizer'])(ddp_model.parameters(), + **configs['optimizer_args']) + if rank == 0: + logger.info("<== Optimizer ==>") + logger.info("optimizer is: " + configs['optimizer']) + + # scheduler + configs['scheduler_args']['num_epochs'] = configs['num_epochs'] + configs['scheduler_args']['epoch_iter'] = len(train_dataloader) + scheduler = getattr(schedulers, + configs['scheduler'])(optimizer, + **configs['scheduler_args']) + if rank == 0: + logger.info("<== Scheduler ==>") + logger.info("scheduler is: " + configs['scheduler']) + + # margin scheduler + configs['margin_update']['epoch_iter'] = len(train_dataloader) + margin_scheduler = getattr(schedulers, configs['margin_scheduler'])( + model=model, **configs['margin_update']) + if rank == 0: + logger.info("<== MarginScheduler ==>") + + # save config.yaml + if rank == 0: + saved_config_path = os.path.join(configs['exp_dir'], 'config.yaml') + with open(saved_config_path, 'w') as fout: + data = yaml.dump(configs) + fout.write(data) + + # training + dist.barrier() # synchronize here + if rank == 0: + logger.info("<========== Training process ==========>") + header = ['Epoch', 'Batch', 'Lr', 'Margin', 'Loss', "Acc"] + for line in tp.header(header, width=10, style='grid').split('\n'): + logger.info(line) + dist.barrier() # synchronize here + + for epoch in range(start_epoch, configs['num_epochs'] + 1): + train_sampler.set_epoch(epoch) + + run_epoch(train_dataloader, + ddp_model, + criterion, + optimizer, + scheduler, + margin_scheduler, + epoch, + logger, + log_batch_interval=configs['log_batch_interval'], + device=device) + + if rank == 0: + if epoch % configs['save_epoch_interval'] == 0 or epoch >= configs[ + 'num_epochs'] - configs['num_avg']: + save_checkpoint( + model, os.path.join(model_dir, + 'model_{}.pt'.format(epoch))) + + if rank == 0: + os.symlink('model_{}.pt'.format(configs['num_epochs']), + os.path.join(model_dir, 'final_model.pt')) + logger.info(tp.bottom(len(header), width=10, style='grid')) + + +if __name__ == '__main__': + fire.Fire(train) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/train_plda.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/train_plda.py new file mode 100644 index 0000000000000000000000000000000000000000..554978af7948852e1bb6014f4b24453b2d590278 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/train_plda.py @@ -0,0 +1,48 @@ +# Copyright (c) 2022 Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os + +from wespeaker.utils.plda.two_cov_plda import TwoCovPLDA + +if __name__ == '__main__': + """ + Currently, we only support the two-cov version, + more variants will be added in next release. + """ + parser = argparse.ArgumentParser() + parser.add_argument('--type', + type=str, + default='2cov', + help='which type of plda to use, we only support ' + 'kaldi 2cov version currently') + parser.add_argument('--scp_path', + type=str, + help='the plda training embedding.scp file') + parser.add_argument('--utt2spk', type=str, help='utt2spk file') + parser.add_argument('--indim', + type=int, + help='the dimension of input embeddings') + parser.add_argument('--exp_dir', type=str) + parser.add_argument('--iter', type=int, default=5) + args = parser.parse_args() + + if args.type == '2cov': + plda = TwoCovPLDA(scp_file=args.scp_path, + utt2spk_file=args.utt2spk, + embed_dim=args.indim) + plda.train(args.iter) + model_path = os.path.join(args.exp_dir, 'plda') + plda.save_model(model_path) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/update_embd_proc.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/update_embd_proc.py new file mode 100644 index 0000000000000000000000000000000000000000..d8a8d7c54ff13a95bc775914b79b07d418a38b7d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/bin/update_embd_proc.py @@ -0,0 +1,45 @@ +# Copyright (c) 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +from wespeaker.utils.embedding_processing import EmbeddingProcessingChain + +if __name__ == '__main__': + """ + xxx + """ + parser = argparse.ArgumentParser() + parser.add_argument('--in_path', + type=str, + default='', + help='Path where to load original processing chain.') + parser.add_argument('--out_path', + type=str, + default='', + help='Path where to save updated processing chain.') + parser.add_argument('--link_no_to_remove', + type=int, + default='', + help='Input scp file.') + parser.add_argument( + '--new_link', + type=str, + default='', + help='new link, e.g., "mean-subtract --scp new_scp_for_mean.scp".') + args = parser.parse_args() + + processingChain = EmbeddingProcessingChain() + processingChain.load(args.in_path) + processingChain.update_link(args.link_no_to_remove, args.new_link) + processingChain.save(args.out_path) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/__init__.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/hub.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/hub.py new file mode 100644 index 0000000000000000000000000000000000000000..c1d4e6db86ccff36a957b508e9c61261f560463b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/hub.py @@ -0,0 +1,126 @@ +# Copyright (c) 2022 Mddct(hamddct@gmail.com) +# 2023 Binbin Zhang(binbzha@qq.com) +# 2024 Shuai Wang(wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import requests +import sys +from pathlib import Path +import tarfile +import zipfile +from urllib.request import urlretrieve + +import tqdm + + +def download(url: str, dest: str, only_child=True): + """download from url to dest""" + assert os.path.exists(dest) + print("Downloading {} to {}".format(url, dest)) + + def progress_hook(t): + last_b = [0] + + def update_to(b=1, bsize=1, tsize=None): + if tsize not in (None, -1): + t.total = tsize + displayed = t.update((b - last_b[0]) * bsize) + last_b[0] = b + return displayed + + return update_to + + # *.tar.gz + name = url.split("?")[0].split("/")[-1] + file_path = os.path.join(dest, name) + with tqdm.tqdm( + unit="B", unit_scale=True, unit_divisor=1024, miniters=1, desc=(name) + ) as t: + urlretrieve( + url, filename=file_path, reporthook=progress_hook(t), data=None + ) + t.total = t.n + + if name.endswith((".tar.gz", ".tar")): + with tarfile.open(file_path) as f: + if not only_child: + f.extractall(dest) + else: + for tarinfo in f: + if "/" not in tarinfo.name: + continue + name = os.path.basename(tarinfo.name) + fileobj = f.extractfile(tarinfo) + with open(os.path.join(dest, name), "wb") as writer: + writer.write(fileobj.read()) + + elif name.endswith(".zip"): + with zipfile.ZipFile(file_path, "r") as zip_ref: + if not only_child: + zip_ref.extractall(dest) + else: + for member in zip_ref.namelist(): + member_path = os.path.relpath( + member, start=os.path.commonpath(zip_ref.namelist()) + ) + if "/" not in member_path: + continue + name = os.path.basename(member_path) + with zip_ref.open(member_path) as source, open( + os.path.join(dest, name), "wb" + ) as target: + target.write(source.read()) + + +class Hub(object): + Assets = { + "chinese": "cnceleb_resnet34.tar.gz", + "english": "voxceleb_resnet221_LM.tar.gz", + "campplus": "campplus_cn_common_200k.tar.gz", + "eres2net": "eres2net_cn_commom_200k.tar.gz", + "vblinkp": "voxblink2_samresnet34.zip", + "vblinkf": "voxblink2_samresnet34_ft.zip", + "w2vbert2_mfa": "voxceleb_voxblink2_w2v_bert2_lora_adapterMFA_lm.tar.gz", + } + + def __init__(self) -> None: + pass + + @staticmethod + def get_model(lang: str) -> str: + if lang not in Hub.Assets.keys(): + print("ERROR: Unsupported lang {} !!!".format(lang)) + sys.exit(1) + model = Hub.Assets[lang] + wespeaker_home = os.environ.get("WESPEAKER_HOME", Path.home() / ".wespeaker") + model_dir = os.path.join(wespeaker_home, lang) + if not os.path.exists(model_dir): + os.makedirs(model_dir) + if set(["avg_model.pt", "config.yaml"]).issubset( + set(os.listdir(model_dir)) + ): + return model_dir + else: + response = requests.get( + "https://modelscope.cn/api/v1/datasets/wenet/wespeaker_pretrained_models/oss/tree" # noqa + ) + model_info = next( + data + for data in response.json()["Data"] + if data["Key"] == model + ) + model_url = model_info["Url"] + download(model_url, model_dir) + return model_dir diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/speaker.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/speaker.py new file mode 100644 index 0000000000000000000000000000000000000000..e39f50b529746707b8b09d92756dc684f7e74662 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/speaker.py @@ -0,0 +1,402 @@ +# Copyright (c) 2023 Binbin Zhang (binbzha@qq.com) +# Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +import numpy as np +from silero_vad import load_silero_vad, read_audio, get_speech_timestamps +import torch +import torchaudio +import torchaudio.compliance.kaldi as kaldi +import yaml +import kaldiio +from tqdm import tqdm + +from wespeaker.cli.hub import Hub +from wespeaker.cli.utils import get_args +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.utils.checkpoint import load_checkpoint +from wespeaker.diar.umap_clusterer import cluster +from wespeaker.diar.extract_emb import subsegment +from wespeaker.diar.make_rttm import merge_segments +from wespeaker.utils.utils import set_seed +from wespeaker.frontend import frontend_class_dict + + +class Speaker: + + def __init__(self, model_dir: str): + set_seed() + self.model = load_model_pt(model_dir) + self.vad = load_silero_vad() + self.table = {} + self.resample_rate = 16000 + self.apply_vad = False + self.device = torch.device('cpu') + self.wavform_norm = False + self.window_type = 'hamming' + + # diarization parmas + self.diar_min_duration = 0.255 + self.diar_window_secs = 1.5 + self.diar_period_secs = 0.75 + self.diar_frame_shift = 10 + self.diar_batch_size = 32 + self.diar_subseg_cmn = True + + def set_wavform_norm(self, wavform_norm: bool): + self.wavform_norm = wavform_norm + + def set_window_type(self, window_type: str): + self.window_type = window_type + + def set_resample_rate(self, resample_rate: int): + self.resample_rate = resample_rate + + def set_vad(self, apply_vad: bool): + self.apply_vad = apply_vad + + def set_device(self, device: str): + self.device = torch.device(device) + self.model = self.model.to(self.device) + + def set_diarization_params(self, + min_duration: float = 0.255, + window_secs: float = 1.5, + period_secs: float = 0.75, + frame_shift: int = 10, + batch_size: int = 32, + subseg_cmn: bool = True): + self.diar_min_duration = min_duration + self.diar_window_secs = window_secs + self.diar_period_secs = period_secs + self.diar_frame_shift = frame_shift + self.diar_batch_size = batch_size + self.diar_subseg_cmn = subseg_cmn + + def compute_features(self, wavform, sample_rate=16000, cmn=True): + if self.model.frontend_type == 'fbank': + feat = kaldi.fbank(wavform, + num_mel_bins=80, + frame_length=25, + frame_shift=10, + sample_frequency=sample_rate, + window_type=self.window_type) # [T, D] + if cmn: + feat = feat - torch.mean(feat, dim=0) + feat = feat.unsqueeze(0) # [1, T, D] + else: + wavform_lens = torch.LongTensor([wavform.shape[1]]).repeat( + wavform.shape[0]).to(self.device) + with torch.no_grad(): + feat, _ = self.model.frontend(wavform, wavform_lens) + return feat + + def extract_embedding_from_feats(self, fbanks, batch_size, subseg_cmn): + fbanks_array = np.stack(fbanks) + if subseg_cmn: + fbanks_array = fbanks_array - np.mean( + fbanks_array, axis=1, keepdims=True) + embeddings = [] + fbanks_array = torch.from_numpy(fbanks_array).to(self.device) + for i in tqdm(range(0, fbanks_array.shape[0], batch_size)): + batch_feats = fbanks_array[i:i + batch_size] + with torch.no_grad(): + batch_embs = self.model(batch_feats) + batch_embs = batch_embs[-1] if isinstance( + batch_embs, tuple) else batch_embs + embeddings.append(batch_embs.detach().cpu().numpy()) + embeddings = np.vstack(embeddings) + return embeddings + + def extract_embedding(self, audio_path: str): + pcm, sample_rate = torchaudio.load(audio_path, + normalize=self.wavform_norm) + return self.extract_embedding_from_pcm(pcm, sample_rate) + + def extract_embedding_from_pcm(self, pcm: torch.Tensor, sample_rate: int): + if self.apply_vad: + # TODO(Binbin Zhang): Refine the segments logic, here we just + # suppose there is only silence at the start/end of the speech + vad_sample_rate = 16000 + wav = pcm + if wav.size(0) > 1: + wav = wav.mean(dim=0, keepdim=True) + + if sample_rate != vad_sample_rate: + transform = torchaudio.transforms.Resample( + orig_freq=sample_rate, new_freq=vad_sample_rate) + wav = transform(wav) + segments = get_speech_timestamps(wav, + self.vad, + return_seconds=True) + pcmTotal = torch.Tensor() + if len(segments) > 0: # remove all the silence + for segment in segments: + start = int(segment['start'] * sample_rate) + end = int(segment['end'] * sample_rate) + pcmTemp = pcm[0, start:end] + pcmTotal = torch.cat([pcmTotal, pcmTemp], 0) + pcm = pcmTotal.unsqueeze(0) + else: # all silence, nospeech + return None + pcm = pcm.to(torch.float) + if sample_rate != self.resample_rate: + pcm = torchaudio.transforms.Resample( + orig_freq=sample_rate, new_freq=self.resample_rate)(pcm) + feats = self.compute_features(pcm, + sample_rate=self.resample_rate, + cmn=True) + with torch.no_grad(): + outputs = self.model(feats) + outputs = outputs[-1] if isinstance(outputs, tuple) else outputs + embedding = outputs[0].to(torch.device('cpu')) + return embedding + + def extract_embedding_list(self, scp_path: str): + names = [] + embeddings = [] + with open(scp_path, 'r') as read_scp: + for line in tqdm(read_scp): + name, wav_path = line.strip().split() + names.append(name) + embedding = self.extract_embedding(wav_path) + embeddings.append(embedding.detach().numpy()) + return names, embeddings + + def compute_similarity(self, audio_path1: str, audio_path2: str) -> float: + e1 = self.extract_embedding(audio_path1) + e2 = self.extract_embedding(audio_path2) + if e1 is None or e2 is None: + return 0.0 + else: + return self.cosine_similarity(e1, e2) + + def cosine_similarity(self, e1, e2): + cosine_score = torch.dot(e1, e2) / (torch.norm(e1) * torch.norm(e2)) + cosine_score = cosine_score.item() + return (cosine_score + 1.0) / 2 # normalize: [-1, 1] => [0, 1] + + def register(self, name: str, audio_path: str): + if name in self.table: + print('Speaker {} already registered, ignore'.format(name)) + else: + self.table[name] = self.extract_embedding(audio_path) + + def recognize(self, audio_path: str): + q = self.extract_embedding(audio_path) + best_score = 0.0 + best_name = '' + for name, e in self.table.items(): + score = self.cosine_similarity(q, e) + if best_score < score: + best_score = score + best_name = name + result = {} + result['name'] = best_name + result['confidence'] = best_score + return result + + def diarize(self, audio_path: str, utt: str = "unk"): + assert self.model.frontend_type == 'fbank', \ + "Diarization only supports fbank frontend" + pcm, sample_rate = torchaudio.load(audio_path, normalize=False) + # 1. vad + wav = read_audio(audio_path) + vad_segments = get_speech_timestamps(wav, + self.vad, + return_seconds=True) + if not vad_segments: + return [] + # 2. extact fbanks + subsegs, subseg_fbanks = [], [] + window_fs = int(self.diar_window_secs * 1000) // self.diar_frame_shift + period_fs = int(self.diar_period_secs * 1000) // self.diar_frame_shift + for item in vad_segments: + begin, end = item['start'], item['end'] + if end - begin >= self.diar_min_duration: + begin_idx = int(begin * sample_rate) + end_idx = int(end * sample_rate) + tmp_wavform = pcm[0, begin_idx:end_idx].unsqueeze(0).to( + torch.float) + fbank = self.compute_features(tmp_wavform, + sample_rate=sample_rate, + cmn=False) + tmp_subsegs, tmp_subseg_fbanks = subsegment( + fbank=fbank.squeeze(0), + seg_id="{:08d}-{:08d}".format(int(begin * 1000), + int(end * 1000)), + window_fs=window_fs, + period_fs=period_fs, + frame_shift=self.diar_frame_shift) + subsegs.extend(tmp_subsegs) + subseg_fbanks.extend(tmp_subseg_fbanks) + + # 3. extract embedding + embeddings = self.extract_embedding_from_feats(subseg_fbanks, + self.diar_batch_size, + self.diar_subseg_cmn) + + # 4. cluster + subseg2label = [] + labels = cluster(embeddings) + for (_subseg, _label) in zip(subsegs, labels): + # b, e = process_seg_id(_subseg, frame_shift=self.diar_frame_shift) + # subseg2label.append([b, e, _label]) + begin_ms, end_ms, begin_frames, end_frames = _subseg.split('-') + begin = (int(begin_ms) + + int(begin_frames) * self.diar_frame_shift) / 1000.0 + end = (int(begin_ms) + + int(end_frames) * self.diar_frame_shift) / 1000.0 + subseg2label.append([begin, end, _label]) + + # 5. merged segments + # [[utt, ([begin, end, label], [])], [utt, ([], [])]] + merged_segment_to_labels = merge_segments({utt: subseg2label}) + + return merged_segment_to_labels + + def diarize_list(self, scp_path: str): + utts = [] + segment2labels = [] + with open(scp_path, 'r', encoding='utf-8') as read_scp: + for line in tqdm(read_scp): + utt, wav_path = line.strip().split() + utts.append(utt) + segment2label = self.diarize(wav_path, utt) + segment2labels.append(segment2label) + return utts, segment2labels + + def make_rttm(self, merged_segment_to_labels, outfile): + with open(outfile, 'w', encoding='utf-8') as fin: + for (utt, begin, end, label) in merged_segment_to_labels: + fin.write( + "SPEAKER {} {} {:.3f} {:.3f} {} \n". + format(utt, 1, float(begin), + float(end) - float(begin), label)) + + +def load_or_download(model_name_or_path: str): + if model_name_or_path in Hub.Assets: + model_dir = Hub.get_model(model_name_or_path) + else: + model_dir = model_name_or_path + return model_dir + + +def load_model(model_name_or_path: str) -> Speaker: + return Speaker(load_or_download(model_name_or_path)) + + +# Load the pytorch pt model which contains all the details. +# And we can use the pt model as a third party pytorch nn.Module for training +def load_model_pt(model_name_or_path: str): + """There are the following files in the `model_dir`: + - config.yaml: the model config file + - avg_model.pt: the pytorch model file + """ + model_dir = load_or_download(model_name_or_path) + required_files = ['config.yaml', 'avg_model.pt'] + for file in required_files: + if not os.path.exists(os.path.join(model_dir, file)): + raise FileNotFoundError(f"{file} not found in {model_dir}") + # Read config file + with open(os.path.join(model_dir, 'config.yaml'), 'r') as f: + config = yaml.load(f, Loader=yaml.FullLoader) + # load model + model = get_speaker_model(config['model'])(**config['model_args']) + frontend_type = 'fbank' + if 'dataset_args' in config and 'frontend' in config['dataset_args']: + frontend_type = config['dataset_args']['frontend'] + print(config) + if frontend_type != 'fbank': + frontend_args = frontend_type + "_args" + print('Initializing frontend model (this could take some time) ...') + frontend = frontend_class_dict[frontend_type]( + **config['dataset_args'][frontend_args], + sample_rate=config['dataset_args']['resample_rate']) + model.add_module("frontend", frontend) + load_checkpoint(model, os.path.join(model_dir, 'avg_model.pt')) + model.eval() + model.frontend_type = frontend_type + return model + + +def main(): + args = get_args() + if args.pretrain == "": + if args.campplus: + model = load_model("campplus") + model.set_wavform_norm(True) + model.set_window_type('povey') + elif args.eres2net: + model = load_model("eres2net") + model.set_wavform_norm(True) + model.set_window_type('povey') + elif args.vblinkp: + model = load_model("vblinkp") + elif args.vblinkf: + model = load_model("vblinkf") + elif args.w2vbert2_mfa: + model = load_model("w2vbert2_mfa") + else: + model = load_model(args.language) + else: + model = load_model(args.pretrain) + model.set_resample_rate(args.resample_rate) + model.set_vad(args.vad) + model.set_device(args.device) + model.set_diarization_params(min_duration=args.diar_min_duration, + window_secs=args.diar_window_secs, + period_secs=args.diar_period_secs, + frame_shift=args.diar_frame_shift, + batch_size=args.diar_emb_bs, + subseg_cmn=args.diar_subseg_cmn) + if args.task == 'embedding': + embedding = model.extract_embedding(args.audio_file) + if embedding is not None: + np.savetxt(args.output_file, embedding.detach().numpy()) + print('Succeed, see {}'.format(args.output_file)) + else: + print('Fails to extract embedding') + elif args.task == 'embedding_kaldi': + names, embeddings = model.extract_embedding_list(args.wav_scp) + embed_ark = args.output_file + ".ark" + embed_scp = args.output_file + ".scp" + with kaldiio.WriteHelper('ark,scp:' + embed_ark + "," + + embed_scp) as writer: + for name, embedding in zip(names, embeddings): + writer(name, embedding) + elif args.task == 'similarity': + print(model.compute_similarity(args.audio_file, args.audio_file2)) + elif args.task == 'diarization': + diar_result = model.diarize(args.audio_file) + if args.output_file is None: + for (_, start, end, spkid) in diar_result: + print("{:.3f}\t{:.3f}\t{:d}".format(start, end, spkid)) + else: + model.make_rttm(diar_result, args.output_file) + elif args.task == 'diarization_list': + utts, segment2labels = model.diarize_list(args.wav_scp) + assert args.output_file is not None + model.make_rttm(np.vstack(segment2labels), args.output_file) + else: + print('Unsupported task {}'.format(args.task)) + sys.exit(-1) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/utils.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..3056f70e56ad0380e0827303ef9caacc84e67c6b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/cli/utils.py @@ -0,0 +1,121 @@ +# Copyright (c) 2023 Binbin Zhang (binbzha@qq.com) +# Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('-t', + '--task', + choices=[ + 'embedding', + 'embedding_kaldi', + 'similarity', + 'diarization', + 'diarization_list', + ], + default='embedding', + help='task type') + parser.add_argument('-l', + '--language', + choices=[ + 'chinese', + 'english', + ], + default='chinese', + help='language type') + parser.add_argument( + '--campplus', + action='store_true', + help='whether to use the damo/speech_campplus_sv_zh-cn_16k-common model' + ) + parser.add_argument( + '--eres2net', + action='store_true', + help='whether to use the damo/speech_eres2net_sv_zh-cn_16k-common model' + ) + parser.add_argument( + '--vblinkp', + action='store_true', + help='whether to use the samresnet34 model pretrained on voxblink2' + ) + parser.add_argument( + '--vblinkf', + action='store_true', + help="whether to use the samresnet34 model pretrained on voxblink2 and" + "fintuned on voxceleb2" + ) + parser.add_argument( + '--w2vbert2_mfa', + action='store_true', + help="whether to use the w2v-bert2 model, " + "the paper link: https://www.arxiv.org/pdf/2510.04213" + ) + parser.add_argument('-p', + '--pretrain', + type=str, + default="", + help='model directory') + parser.add_argument('--device', + type=str, + default='cpu', + help="device type (most commonly cpu or cuda," + "but also potentially mps, xpu, xla or meta)" + "and optional device ordinal for the device type.") + parser.add_argument('--audio_file', help='audio file') + parser.add_argument('--audio_file2', + help='audio file2, specifically for similarity task') + parser.add_argument('--wav_scp', + help='path to wav.scp, for extract and saving ' + 'kaldi-stype embeddings') + parser.add_argument('--resample_rate', + type=int, + default=16000, + help='resampling rate') + parser.add_argument('--vad', + action='store_true', + help='whether to do VAD or not') + parser.add_argument('--output_file', + default=None, + help='output file to save speaker embedding ' + 'or save diarization result') + # diarization params + parser.add_argument('--diar_min_duration', + type=float, + default=0.255, + help='VAD min duration') + parser.add_argument('--diar_window_secs', + type=float, + default=1.5, + help='the window seconds in embedding extraction') + parser.add_argument('--diar_period_secs', + type=float, + default=0.75, + help='the shift seconds in embedding extraction') + parser.add_argument('--diar_frame_shift', + type=int, + default=10, + help='frame shift in fbank extraction (ms)') + parser.add_argument('--diar_emb_bs', + type=int, + default=32, + help='batch size for embedding extraction') + parser.add_argument('--diar_subseg_cmn', + type=bool, + default=True, + help='do cmn after or before fbank sub-segmentation') + args = parser.parse_args() + return args diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..ae0be89378747ff6a5fc027ce333fadfca44b421 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset.py @@ -0,0 +1,273 @@ +# Copyright (c) 2021 Mobvoi Inc. (authors: Binbin Zhang) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random + +import torch +import torch.distributed as dist +from torch.utils.data import IterableDataset + +from wespeaker.utils.file_utils import read_lists +from wespeaker.dataset.lmdb_data import LmdbData +import wespeaker.dataset.processor as processor + + +class Processor(IterableDataset): + + def __init__(self, source, f, *args, **kw): + assert callable(f) + self.source = source + self.f = f + self.args = args + self.kw = kw + + def set_epoch(self, epoch): + self.source.set_epoch(epoch) + + def __iter__(self): + """ Return an iterator over the source dataset processed by the + given processor. + """ + assert self.source is not None + assert callable(self.f) + return self.f(iter(self.source), *self.args, **self.kw) + + def apply(self, f): + assert callable(f) + return Processor(self, f, *self.args, **self.kw) + + +class DistributedSampler: + + def __init__(self, shuffle=True, partition=True): + self.epoch = -1 + self.update() + self.shuffle = shuffle + self.partition = partition + + def update(self): + assert dist.is_available() + if dist.is_initialized(): + self.rank = dist.get_rank() + self.world_size = dist.get_world_size() + else: + self.rank = 0 + self.world_size = 1 + worker_info = torch.utils.data.get_worker_info() + if worker_info is None: + self.worker_id = 0 + self.num_workers = 1 + else: + self.worker_id = worker_info.id + self.num_workers = worker_info.num_workers + return dict(rank=self.rank, + world_size=self.world_size, + worker_id=self.worker_id, + num_workers=self.num_workers) + + def set_epoch(self, epoch): + self.epoch = epoch + + def sample(self, data): + """ Sample data according to rank/world_size/num_workers + + Args: + data(List): input data list + + Returns: + List: data list after sample + """ + data = list(range(len(data))) + if self.partition: + if self.shuffle: + random.Random(self.epoch).shuffle(data) + data = data[self.rank::self.world_size] + data = data[self.worker_id::self.num_workers] + return data + + +class DataList(IterableDataset): + + def __init__(self, + lists, + shuffle=True, + partition=True, + repeat_dataset=True): + self.lists = lists + self.repeat_dataset = repeat_dataset + self.sampler = DistributedSampler(shuffle, partition) + + def set_epoch(self, epoch): + self.sampler.set_epoch(epoch) + + def __iter__(self): + sampler_info = self.sampler.update() + indexes = self.sampler.sample(self.lists) + if not self.repeat_dataset: + for index in indexes: + data = dict(src=self.lists[index]) + data.update(sampler_info) + yield data + else: + indexes_len = len(indexes) + counter = 0 + while True: + index = indexes[counter % indexes_len] + counter += 1 + data = dict(src=self.lists[index]) + data.update(sampler_info) + yield data + + +def Dataset(data_type, + data_list_file, + configs, + spk2id_dict, + whole_utt=False, + reverb_lmdb_file=None, + noise_lmdb_file=None, + repeat_dataset=True): + """ Construct dataset from arguments + + We have two shuffle stage in the Dataset. The first is global + shuffle at shards tar/raw/feat file level. The second is local shuffle + at training samples level. + + Args: + data_type(str): shard/raw/feat + data_list_file: data list file + configs: dataset configs + spk2id_dict: spk2id dict + reverb_lmdb_file: reverb data source lmdb file + noise_lmdb_file: noise data source lmdb file + whole_utt: use whole utt or random chunk + repeat_dataset: True for training while False for testing + """ + assert data_type in ['shard', 'raw', 'feat'] + frontend_type = configs.get('frontend', 'fbank') + frontend_args = frontend_type + "_args" + + def _get_frame_params(): + """Get frame_shift (ms) and frame_length (ms) from config. + Supports both fbank (frame_shift/frame_length in ms) and + tfmel-style (hop_length/win_length in samples) parameters. + """ + args = configs.get(frontend_args, {}) + resample_rate = configs.get('resample_rate', 16000) + hop_length = args.get('hop_length', None) + win_length = args.get('win_length', None) + if hop_length is not None: + fs = hop_length * 1000 // resample_rate + else: + fs = args.get('frame_shift', 10) + if win_length is not None: + fl = win_length * 1000 // resample_rate + else: + fl = args.get('frame_length', 25) + return fs, fl + + lists = read_lists(data_list_file) + shuffle = configs.get('shuffle', False) + # Global shuffle + dataset = DataList(lists, shuffle=shuffle, repeat_dataset=repeat_dataset) + if data_type == 'shard': + dataset = Processor(dataset, processor.url_opener) + dataset = Processor(dataset, processor.tar_file_and_group) + elif data_type == 'raw': + dataset = Processor(dataset, processor.parse_raw) + else: + dataset = Processor(dataset, processor.parse_feat) + + if configs.get('filter', True): + # Filter the data with unwanted length + filter_conf = configs.get('filter_args', {}) + frame_shift, _ = _get_frame_params() + dataset = Processor(dataset, + processor.filter, + frame_shift=frame_shift, + data_type=data_type, + **filter_conf) + + # Local shuffle + if shuffle: + dataset = Processor(dataset, processor.shuffle, + **configs['shuffle_args']) + + # spk2id + dataset = Processor(dataset, processor.spk_to_id, spk2id_dict) + + if data_type == 'feat': + if not whole_utt: + # random chunk + chunk_len = num_frms = configs.get('num_frms', 200) + dataset = Processor(dataset, processor.random_chunk, chunk_len, + 'feat') + else: + # resample + resample_rate = configs.get('resample_rate', 16000) + dataset = Processor(dataset, processor.resample, resample_rate) + # speed perturb + speed_perturb_flag = configs.get('speed_perturb', True) + if speed_perturb_flag: + speed_perturb_mode = configs.get('speed_perturb_mode', 'random') + if speed_perturb_mode == 'expanded': + dataset = Processor(dataset, processor.speed_perturb_expand, + len(spk2id_dict)) + if shuffle: + dataset = Processor(dataset, processor.shuffle, + **configs['shuffle_args']) + else: + dataset = Processor(dataset, processor.speed_perturb, + len(spk2id_dict)) + if not whole_utt: + # random chunk + num_frms = configs.get('num_frms', 200) + frame_shift, frame_length = _get_frame_params() + chunk_len = ((num_frms - 1) * frame_shift + + frame_length) * resample_rate // 1000 + dataset = Processor(dataset, processor.random_chunk, chunk_len, + data_type) + # add reverb & noise + aug_prob = configs.get('aug_prob', 0.6) + if (reverb_lmdb_file and noise_lmdb_file) and (aug_prob > 0.0): + reverb_data = LmdbData(reverb_lmdb_file) + noise_data = LmdbData(noise_lmdb_file) + dataset = Processor(dataset, processor.add_reverb_noise, + reverb_data, noise_data, resample_rate, + aug_prob) + # compute fbank + if frontend_type == 'fbank': + dataset = Processor(dataset, processor.compute_fbank, + **configs['fbank_args']) + + # !!!IMPORTANT NOTICE!!! + # To support different frontends (including ssl pretrained models), + # we have to move apply_cmvn and spec_aug out of the dataset pipeline + # which runs totally in cpus. + # These two modules are now used in wespeaker/utils/executor.py (train) + # and wespeaker/bin/extract.py (test), which runs in gpus. + ''' + # apply cmvn + dataset = Processor(dataset, processor.apply_cmvn) + + # spec augmentation + spec_aug_flag = configs.get('spec_aug', True) + if spec_aug_flag: + dataset = Processor(dataset, processor.spec_aug, + **configs['spec_aug_args']) + ''' + return dataset diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset_deprecated.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset_deprecated.py new file mode 100644 index 0000000000000000000000000000000000000000..9ccecdcf0c24bd4a91086c8b52c1b4fdf91a351a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset_deprecated.py @@ -0,0 +1,196 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import numpy as np +import torch +from torch.utils.data import Dataset + +import kaldiio +from scipy import signal +from scipy.io import wavfile +import torchaudio.compliance.kaldi as kaldi + +from wespeaker.utils.file_utils import read_scp +from wespeaker.dataset.dataset_utils_deprecated import (get_random_chunk, + speed_perturb, + spec_augmentation) + + +class FeatList_LableDict_Dataset(Dataset): + """ + shuffle wav.scp/feats.scp, load all labels into cpu memory + """ + + def __init__(self, data_list, utt2spkid_dict, whole_utt=False, **kwargs): + super(FeatList_LableDict_Dataset, self).__init__() + self.data_list = data_list + self.length = len(data_list) + self.utt2spkid_dict = utt2spkid_dict + self.whole_utt = whole_utt # True means batch_size=1 !! + + # feat config + self.raw_wav = kwargs.get('raw_wav', True) + self.feat_dim = kwargs.get('feat_dim', 80) + self.num_frms = kwargs.get('num_frms', 200) + # chunk config (sample rate is 16kHZ) + self.chunk_len = (self.num_frms - + 1) * 160 + 400 if self.raw_wav else self.num_frms + + # dataset config (for wav augmentation only) + if self.raw_wav: + self.speed_perturb = kwargs.get('speed_perturb', False) + self.aug_prob = kwargs.get('aug_prob', 0.0) + self.musan_scp = kwargs.get('musan_scp', '') + self.rirs_scp = kwargs.get('rirs_scp', '') + if self.aug_prob > 0.0: + self.augment_wav = Augment_Wav(self.musan_scp, self.rirs_scp) + self.spec_aug = kwargs.get('spec_aug', False) + + # used for calculate the spk id after speed perturb + self.spk_num = len(set(utt2spkid_dict.values())) + + def __getitem__(self, idx): + utt, data_path = self.data_list[idx] + spkid = self.utt2spkid_dict[utt] if utt in self.utt2spkid_dict else -1 + + speed_perturb_idx = 0 + if self.raw_wav: + # load wav file + sr, waveform = wavfile.read(data_path) + # kaldiio.load_mat() is a little slower than wavfile.read(), + # but supports cloud io (e.g., kaldiio.load_mat( + # 'ffmpeg -i http://ip/xxx.wav -ac 1 -ar 16000 -f wav - |')) + + # speed perturb + if self.speed_perturb: + speed_perturb_idx = random.randint(0, 2) + waveform = speed_perturb(waveform, + speed_perturb_idx=speed_perturb_idx) + # chunk/pad + if not self.whole_utt: + waveform = get_random_chunk(waveform, self.chunk_len) + # augment wav + if self.aug_prob > random.random(): + waveform = self.augment_wav.process(waveform) + # make fbank feature + feat_tensor = kaldi.fbank(torch.FloatTensor(waveform).unsqueeze(0), + num_mel_bins=self.feat_dim, + frame_shift=10, + frame_length=25, + dither=1.0, + sample_frequency=16000, + window_type='hamming', + use_energy=False) + feat = feat_tensor.detach().numpy() + else: + # load feat + feat = kaldiio.load_mat(data_path) + # chunk/pad + if not self.whole_utt: + feat = get_random_chunk(feat, self.chunk_len) + + # cmn, without cvn + feat = feat - np.mean(feat, axis=0) # (T,F) + + # spec augmentation + if self.spec_aug: + feat = spec_augmentation(feat) + + return utt, feat, spkid + self.spk_num * speed_perturb_idx + + def __len__(self): + return self.length + + +class Augment_Wav: + + def __init__(self, musan_scp, rirs_scp): + + self.noise_snr = { + 'noise': [0, 15], + 'speech': [13, 20], + 'music': [5, 15] + } + self.num_noise = {'noise': [1, 1], 'speech': [3, 7], 'music': [1, 1]} + + self.rir_list = read_scp(rirs_scp) + + # {'noise': noise_list, 'speech': speech_list, 'music': music_list} + self.noise_dict = {} + with open(musan_scp, 'r') as fp: + for line in fp.readlines(): + segs = line.strip().split() + noise_type = segs[0].split('/')[0] + + if noise_type not in self.noise_dict: + self.noise_dict[noise_type] = [] + # utt_name wav_path + self.noise_dict[noise_type].append((segs[0], segs[1])) + + def additive_noise(self, noise_type, audio): + """ + :param noise_type: 'noise', 'speech', 'music' + :param audio: numpy array, (audio_len,) + """ + audio = audio.astype(np.float32) + audio_len = audio.shape[0] + audio_db = 10 * np.log10(np.mean(audio**2) + 1e-4) + + num_noise = self.num_noise[noise_type] + noise_idx_list = random.sample( + self.noise_dict[noise_type], + random.randint(num_noise[0], num_noise[1])) + noise_list = [] + for _, noise_path in noise_idx_list: + _, noise_audio = wavfile.read(noise_path) + noise_audio = get_random_chunk(noise_audio, + audio_len).astype(np.float32) + + noise_snr = random.uniform(self.noise_snr[noise_type][0], + self.noise_snr[noise_type][1]) + noise_db = 10 * np.log10(np.mean(noise_audio**2) + 1e-4) + noise_list.append( + np.sqrt(10**((audio_db - noise_db - noise_snr) / 10)) * + noise_audio) + + return np.sum(np.stack(noise_list), axis=0) + audio + + def reverberate(self, audio): + """ + :param audio: numpy array, (audio_len,) + """ + audio = audio.astype(np.float32) + audio_len = audio.shape[0] + + _, rir_wav = random.choice(self.rir_list) + _, rir_audio = wavfile.read(rir_wav) + rir_audio = rir_audio.astype(np.float32) + rir_audio = rir_audio / np.sqrt(np.sum(rir_audio**2)) + + return signal.convolve(audio, rir_audio, mode='full')[:audio_len] + + def process(self, audio): + augtype = random.randint(1, 4) + # print("augtype", augtype) + if augtype == 1: + audio = self.reverberate(audio) + elif augtype == 2: + audio = self.additive_noise('music', audio) + elif augtype == 3: + audio = self.additive_noise('speech', audio) + elif augtype == 4: + audio = self.additive_noise('noise', audio) + + return audio diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset_utils.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..feda2feb334213426b33591271ff1e5eca54132c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset_utils.py @@ -0,0 +1,53 @@ +# Copyright (c) 2024 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import torch + + +def apply_cmvn(feats, norm_mean=True, norm_var=False): + # feats batch: (B,T,F) + if norm_mean: + feats = feats - torch.mean(feats, dim=1, keepdim=True) + if norm_var: + feats = feats / torch.sqrt(torch.var(feats, dim=1, keepdim=True) + 1e-7) + + return feats + + +def spec_aug(feats, num_t_mask=1, num_f_mask=1, max_t=10, max_f=8, prob=0.6): + # feats batch: (B,T,F) + # do spec_aug on all batch samples using a same group of params randomly + # TODO (hongji): do spec_aug on each sample separately + if random.random() < prob: + x = feats + assert isinstance(x, torch.Tensor) + # y = x.clone().detach() + y = x.detach() # inplace operation + _, max_frames, max_freq = y.shape + # time mask + for i in range(num_t_mask): + start = random.randint(0, max_frames - 1) + length = random.randint(1, max_t) + end = min(max_frames, start + length) + y[:, start:end, :] = 0 + # freq mask + for i in range(num_f_mask): + start = random.randint(0, max_freq - 1) + length = random.randint(1, max_f) + end = min(max_freq, start + length) + y[:, :, start:end] = 0 + feats = y + + return feats diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset_utils_deprecated.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset_utils_deprecated.py new file mode 100644 index 0000000000000000000000000000000000000000..bc91fc4ebd163b79abc99b2a2cf41328c3e2b02c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/dataset_utils_deprecated.py @@ -0,0 +1,84 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import random +from scipy import signal + + +def get_random_chunk(data, chunk_len): + # chunking: randomly select a range of size min(chunk_len, len). + data_len = len(data) + data_shape = data.shape + adjust_chunk_len = min(data_len, chunk_len) + chunk_start = random.randint(0, data_len - adjust_chunk_len) + + data = data[chunk_start:chunk_start + adjust_chunk_len] + # padding if needed + if adjust_chunk_len < chunk_len: + chunk_shape = chunk_len if len(data_shape) == 1 else (chunk_len, + data.shape[1]) + data = np.resize(data, chunk_shape) # repeating + + return data + + +def spec_augmentation(x, + num_t_mask=1, + num_f_mask=1, + max_t=10, + max_f=8, + prob=0.5): + """ do spec augmentation on x + + Args: + x: input feature, T * F 2D + num_t_mask: number of time mask to apply + num_f_mask: number of freq mask to apply + max_t: max width of time mask + max_f: max width of freq mask + + Returns: + augmented feature (x) + """ + if random.random() > prob: + return x + + y = x # np.copy(x) + max_frames = y.shape[0] + max_freq = y.shape[1] + + # time mask + for i in range(num_t_mask): + start = random.randint(0, max_frames - 1) + length = random.randint(1, max_t) + end = min(max_frames, start + length) + y[start:end, :] = 0 + # freq mask + for i in range(num_f_mask): + start = random.randint(0, max_freq - 1) + length = random.randint(1, max_f) + end = min(max_freq, start + length) + y[:, start:end] = 0 + return y + + +def speed_perturb(x, speed_perturb_idx=0): + speed_list = [1.0, 0.9, 1.1] + speed = speed_list[speed_perturb_idx] + + x = x.astype(np.float32) + y = signal.resample(x, int(len(x) / speed)) + + return y diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/lmdb_data.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/lmdb_data.py new file mode 100644 index 0000000000000000000000000000000000000000..7999777cbb97404c929c302aceeae50ab3217258 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/lmdb_data.py @@ -0,0 +1,53 @@ +# Copyright (c) 2022 Binbin Zhang (binbzha@qq.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import pickle + +import lmdb + + +class LmdbData: + + def __init__(self, lmdb_file): + self.db = lmdb.open(lmdb_file, + readonly=True, + lock=False, + readahead=False) + with self.db.begin(write=False) as txn: + obj = txn.get(b'__keys__') + assert obj is not None + self.keys = pickle.loads(obj) + assert isinstance(self.keys, list) + + def random_one(self): + assert len(self.keys) > 0 + index = random.randint(0, len(self.keys) - 1) + key = self.keys[index] + with self.db.begin(write=False) as txn: + value = txn.get(key.encode()) + assert value is not None + return key, value + + def __del__(self): + self.db.close() + + +if __name__ == '__main__': + import sys + db = LmdbData(sys.argv[1]) + key, _ = db.random_one() + print(key) + key, _ = db.random_one() + print(key) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/processor.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/processor.py new file mode 100644 index 0000000000000000000000000000000000000000..08c62718a9d1de2c1cbcaf72bec5cd94ae13feb7 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/dataset/processor.py @@ -0,0 +1,587 @@ +# Copyright (c) 2021 Mobvoi Inc. (authors: Binbin Zhang) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import kaldiio +import json +import logging +import random +import tarfile +from subprocess import PIPE, Popen +from urllib.parse import urlparse + +import numpy as np +from scipy import signal +from scipy.io import wavfile +import torch +import torchaudio +import torchaudio.compliance.kaldi as kaldi + +AUDIO_FORMAT_SETS = set(['flac', 'mp3', 'm4a', 'ogg', 'opus', 'wav', 'wma']) + + +def url_opener(data): + """ Give url or local file, return file descriptor + Inplace operation. + + Args: + data(Iterable[str]): url or local file list + + Returns: + Iterable[{src, stream}] + """ + for sample in data: + assert 'src' in sample + # TODO(Binbin Zhang): support HTTP + url = sample['src'] + try: + pr = urlparse(url) + # local file + if pr.scheme == '' or pr.scheme == 'file': + stream = open(url, 'rb') + # network file, such as HTTP(HDFS/OSS/S3)/HTTPS/SCP + else: + cmd = f'wget -q -O - {url}' + process = Popen(cmd, shell=True, stdout=PIPE) + sample.update(process=process) + stream = process.stdout + sample.update(stream=stream) + yield sample + except Exception as ex: + logging.warning('Failed to open {}'.format(url)) + + +def tar_file_and_group(data): + """ Expand a stream of open tar files into a stream of tar file contents. + And groups the file with same prefix + + Args: + data: Iterable[{src, stream}] + + Returns: + Iterable[{key, wav, spk, sample_rate}] + """ + for sample in data: + assert 'stream' in sample + stream = tarfile.open(fileobj=sample['stream'], mode="r:*") + prev_prefix = None + example = {} + valid = True + for tarinfo in stream: + name = tarinfo.name + pos = name.rfind('.') + assert pos > 0 + prefix, postfix = name[:pos], name[pos + 1:] + if prev_prefix is not None and prefix != prev_prefix: + example['key'] = prev_prefix + if valid: + yield example + example = {} + valid = True + with stream.extractfile(tarinfo) as file_obj: + try: + if postfix in ['spk']: + example[postfix] = file_obj.read().decode( + 'utf8').strip() + elif postfix in AUDIO_FORMAT_SETS: + waveform, sample_rate = torchaudio.load(file_obj) + example['wav'] = waveform + example['sample_rate'] = sample_rate + else: + example[postfix] = file_obj.read() + except Exception as ex: + valid = False + logging.warning('error to parse {}'.format(name)) + prev_prefix = prefix + if prev_prefix is not None: + example['key'] = prev_prefix + yield example + stream.close() + if 'process' in sample: + sample['process'].communicate() + sample['stream'].close() + + +def parse_raw(data): + """ Parse key/wav/spk from json line + + Args: + data: Iterable[str], str is a json line has key/wav/spk + + Returns: + Iterable[{key, wav, spk, sample_rate}] + """ + + def read_audio(wav): + if wav.endswith('|'): + p = Popen(wav[:-1], shell=True, stdout=PIPE) + data = p.stdout.read() + waveform, sample_rate = torchaudio.load(io.BytesIO(data)) + else: + waveform, sample_rate = torchaudio.load(wav) + return waveform, sample_rate + + def apply_vad(waveform, sample_rate, vad): + voice_part_list = [] + for start, end in vad: + start, end = float(start), float(end) + start, end = int(start * sample_rate), int(end * sample_rate) + voice_part_list.append(waveform[:, start:end]) + waveform = torch.cat(voice_part_list, dim=1) + return waveform, sample_rate + + for sample in data: + assert 'src' in sample + json_line = sample['src'] + obj = json.loads(json_line) + assert 'key' in obj + assert 'wav' in obj + assert 'spk' in obj + key = obj['key'] + wav_file = obj['wav'] + spk = obj['spk'] + try: + waveform, sample_rate = read_audio(wav_file) + if 'vad' in obj: + waveform, sample_rate = apply_vad(waveform, sample_rate, + obj['vad']) + example = dict(key=key, + spk=spk, + wav=waveform, + sample_rate=sample_rate) + yield example + except Exception as ex: + logging.warning('Failed to read {}'.format(wav_file)) + + +def parse_feat(data): + """ Parse key/feat/spk from json line + + Args: + data: Iterable[str], str is a json line has key/feat/spk + + Returns: + Iterable[{key, feat, spk}] + """ + for sample in data: + assert 'src' in sample + json_line = sample['src'] + obj = json.loads(json_line) + assert 'key' in obj + assert 'feat' in obj + assert 'spk' in obj + key = obj['key'] + feat_ark = obj['feat'] + spk = obj['spk'] + try: + feat = torch.from_numpy(kaldiio.load_mat(feat_ark)) + example = dict(key=key, spk=spk, feat=feat) + yield example + except Exception as ex: + logging.warning('Failed to load {}'.format(feat_ark)) + + +def shuffle(data, shuffle_size=2500): + """ Local shuffle the data + + Args: + data: Iterable[{key, wav/feat, spk}] + shuffle_size: buffer size for shuffle + + Returns: + Iterable[{key, wav/feat, spk}] + """ + buf = [] + for sample in data: + buf.append(sample) + if len(buf) >= shuffle_size: + random.shuffle(buf) + for x in buf: + yield x + buf = [] + # The sample left over + random.shuffle(buf) + for x in buf: + yield x + + +def spk_to_id(data, spk2id): + """ Parse spk id + + Args: + data: Iterable[{key, wav/feat, spk}] + spk2id: Dict[str, int] + + Returns: + Iterable[{key, wav/feat, label}] + """ + for sample in data: + assert 'spk' in sample + if sample['spk'] in spk2id: + label = spk2id[sample['spk']] + else: + label = -1 + sample['label'] = label + yield sample + + +def resample(data, resample_rate=16000): + """ Resample data. + Inplace operation. + Args: + data: Iterable[{key, wav, label, sample_rate}] + resample_rate: target resample rate + Returns: + Iterable[{key, wav, label, sample_rate}] + """ + for sample in data: + assert 'sample_rate' in sample + assert 'wav' in sample + sample_rate = sample['sample_rate'] + waveform = sample['wav'] + if sample_rate != resample_rate: + sample['sample_rate'] = resample_rate + sample['wav'] = torchaudio.transforms.Resample( + orig_freq=sample_rate, new_freq=resample_rate)(waveform) + yield sample + + +def speed_perturb(data, num_spks): + """ Apply speed perturb to the data. + Inplace operation. + + Args: + data: Iterable[{key, wav, label, sample_rate}] + + Returns: + Iterable[{key, wav, label, sample_rate}] + """ + speeds = [1.0, 0.9, 1.1] + for sample in data: + assert 'sample_rate' in sample + assert 'wav' in sample + sample_rate = sample['sample_rate'] + waveform = sample['wav'] + speed_idx = random.randint(0, 2) + if speed_idx > 0: + wav, _ = torchaudio.sox_effects.apply_effects_tensor( + waveform, sample_rate, + [['speed', str(speeds[speed_idx])], ['rate', + str(sample_rate)]]) + sample['wav'] = wav + sample['label'] = sample['label'] + num_spks * speed_idx + + yield sample + + +def speed_perturb_expand(data, num_spks): + """Expand each sample into original, 0.9x, and 1.1x speed variants. + + This matches the W2V-BERT recipe where speed variants are treated as + separate samples with separate speaker-label offsets. + """ + speeds = [1.0, 0.9, 1.1] + for sample in data: + assert 'sample_rate' in sample + assert 'wav' in sample + sample_rate = sample['sample_rate'] + waveform = sample['wav'] + label = sample['label'] + for speed_idx, speed in enumerate(speeds): + speed_sample = sample.copy() + if speed_idx > 0: + wav, _ = torchaudio.sox_effects.apply_effects_tensor( + waveform, sample_rate, + [['speed', str(speed)], ['rate', str(sample_rate)]]) + speed_sample['wav'] = wav + speed_sample['label'] = label + num_spks * speed_idx + yield speed_sample + + +def get_random_chunk(data, chunk_len): + """ Get random chunk + + Args: + data: torch.Tensor (random len) + chunk_len: chunk length + + Returns: + torch.Tensor (exactly chunk_len) + """ + data_len = len(data) + data_shape = data.shape + # random chunk + if data_len >= chunk_len: + chunk_start = random.randint(0, data_len - chunk_len) + data = data[chunk_start:chunk_start + chunk_len] + # re-clone the data to avoid memory leakage + if type(data) == torch.Tensor: + data = data.clone() + else: # np.array + data = data.copy() + else: + # padding + repeat_factor = chunk_len // data_len + 1 + repeat_shape = repeat_factor if len(data_shape) == 1 else ( + repeat_factor, 1) + if type(data) == torch.Tensor: + data = data.repeat(repeat_shape) + else: # np.array + data = np.tile(data, repeat_shape) + data = data[:chunk_len] + + return data + + +def filter(data, + min_num_frames=100, + max_num_frames=800, + frame_shift=10, + data_type='shard/raw/feat'): + """ Filter the utterance with very short duration and random chunk the + utterance with very long duration. + + Args: + data: Iterable[{key, wav, label, sample_rate}] + min_num_frames: minimum number of frames of acoustic features + max_num_frames: maximum number of frames of acoustic features + frame_shift: the frame shift of the acoustic features (ms) + Returns: + Iterable[{key, wav, label, sample_rate}] + """ + for sample in data: + assert 'key' in sample + + if data_type == 'feat': + assert 'feat' in sample + feat = sample['feat'] + if len(feat) < min_num_frames: + continue + elif len(feat) > max_num_frames: + feat = get_random_chunk(feat, max_num_frames) + sample['feat'] = feat + else: + assert 'sample_rate' in sample + assert 'wav' in sample + sample_rate = sample['sample_rate'] + wav = sample['wav'][0] + + min_len = int(frame_shift / 1000 * min_num_frames * sample_rate) + max_len = int(frame_shift / 1000 * max_num_frames * sample_rate) + + if len(wav) < min_len: + continue + elif len(wav) > max_len: + wav = get_random_chunk(wav, max_len) + sample['wav'] = wav.unsqueeze(0) + + yield sample + + +def random_chunk(data, chunk_len, data_type='shard/raw/feat'): + """ Random chunk the data into chunk_len + + Args: + data: Iterable[{key, wav/feat, label}] + chunk_len: chunk length for each sample + + Returns: + Iterable[{key, wav/feat, label}] + """ + for sample in data: + assert 'key' in sample + + if data_type == 'feat': + assert 'feat' in sample + feat = sample['feat'] + feat = get_random_chunk(feat, chunk_len) + sample['feat'] = feat + else: + assert 'wav' in sample + wav = sample['wav'][0] + wav = get_random_chunk(wav, chunk_len) + sample['wav'] = wav.unsqueeze(0) + yield sample + + +def add_reverb_noise(data, + reverb_source, + noise_source, + resample_rate=16000, + aug_prob=0.6): + """ Add reverb & noise aug + + Args: + data: Iterable[{key, wav, label, sample_rate}] + reverb_source: reverb LMDB data source + noise_source: noise LMDB data source + resample_rate: resample rate for reverb/noise data + aug_prob: aug probability + + Returns: + Iterable[{key, wav, label, sample_rate}] + """ + for sample in data: + assert 'wav' in sample + assert 'key' in sample + if aug_prob > random.random(): + aug_type = random.randint(1, 2) + if aug_type == 1: + # add reverberation + audio = sample['wav'].numpy()[0] + audio_len = audio.shape[0] + + _, rir_data = reverb_source.random_one() + rir_sr, rir_audio = wavfile.read(io.BytesIO(rir_data)) + rir_audio = rir_audio.astype(np.float32) + if rir_sr != resample_rate: + rir_audio = signal.resample( + rir_audio, + int(len(rir_audio) / rir_sr * resample_rate)) + rir_audio = rir_audio / np.sqrt(np.sum(rir_audio**2)) + out_audio = signal.convolve(audio, rir_audio, + mode='full')[:audio_len] + else: + # add additive noise + audio = sample['wav'].numpy()[0] + audio_len = audio.shape[0] + audio_db = 10 * np.log10(np.mean(audio**2) + 1e-4) + + key, noise_data = noise_source.random_one() + if key.startswith('noise'): + snr_range = [0, 15] + elif key.startswith('speech'): + snr_range = [10, 30] + elif key.startswith('music'): + snr_range = [5, 15] + else: + snr_range = [0, 15] + noise_sr, noise_audio = wavfile.read(io.BytesIO(noise_data)) + noise_audio = noise_audio.astype(np.float32) / (1 << 15) + if noise_sr != resample_rate: + # Since the noise audio could be very long, it must be + # chunked first before resampled (to save time) + noise_audio = get_random_chunk( + noise_audio, int(audio_len / resample_rate * noise_sr)) + noise_audio = signal.resample(noise_audio, audio_len) + else: + noise_audio = get_random_chunk(noise_audio, audio_len) + noise_snr = random.uniform(snr_range[0], snr_range[1]) + noise_db = 10 * np.log10(np.mean(noise_audio**2) + 1e-4) + noise_audio = np.sqrt(10**( + (audio_db - noise_db - noise_snr) / 10)) * noise_audio + out_audio = audio + noise_audio + + # normalize into [-1, 1] + out_audio = out_audio / (np.max(np.abs(out_audio)) + 1e-4) + sample['wav'] = torch.from_numpy(out_audio).unsqueeze(0) + + yield sample + + +def compute_fbank(data, + num_mel_bins=80, + frame_length=25, + frame_shift=10, + dither=1.0): + """ Extract fbank + + Args: + data: Iterable[{key, wav, label, sample_rate}] + + Returns: + Iterable[{key, feat, label, sample_rate}] + """ + for sample in data: + assert 'sample_rate' in sample + assert 'wav' in sample + assert 'key' in sample + assert 'label' in sample + sample_rate = sample['sample_rate'] + waveform = sample['wav'] + waveform = waveform * (1 << 15) + # Only keep key, feat, label + mat = kaldi.fbank(waveform, + num_mel_bins=num_mel_bins, + frame_length=frame_length, + frame_shift=frame_shift, + dither=dither, + sample_frequency=sample_rate, + window_type='hamming', + use_energy=False) + yield dict(key=sample['key'], label=sample['label'], feat=mat) + + +def apply_cmvn(data, norm_mean=True, norm_var=False): + """ Apply CMVN + + Args: + data: Iterable[{key, feat, label}] + + Returns: + Iterable[{key, feat, label}] + """ + for sample in data: + assert 'key' in sample + assert 'feat' in sample + assert 'label' in sample + mat = sample['feat'] + if norm_mean: + mat = mat - torch.mean(mat, dim=0) + if norm_var: + mat = mat / torch.sqrt(torch.var(mat, dim=0) + 1e-8) + yield dict(key=sample['key'], label=sample['label'], feat=mat) + + +def spec_aug(data, num_t_mask=1, num_f_mask=1, max_t=10, max_f=8, prob=0.6): + """ Do spec augmentation + Inplace operation + + Args: + data: Iterable[{key, feat, label}] + num_t_mask: number of time mask to apply + num_f_mask: number of freq mask to apply + max_t: max width of time mask + max_f: max width of freq mask + prob: prob of spec_aug + + Returns + Iterable[{key, feat, label}] + """ + for sample in data: + if random.random() < prob: + assert 'feat' in sample + x = sample['feat'] + assert isinstance(x, torch.Tensor) + # y = x.clone().detach() + y = x.detach() # inplace operation + max_frames = y.size(0) + max_freq = y.size(1) + # time mask + for i in range(num_t_mask): + start = random.randint(0, max_frames - 1) + length = random.randint(1, max_t) + end = min(max_frames, start + length) + y[start:end, :] = 0 + # freq mask + for i in range(num_f_mask): + start = random.randint(0, max_freq - 1) + length = random.randint(1, max_f) + end = min(max_freq, start + length) + y[:, start:end] = 0 + sample['feat'] = y + yield sample diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/__init__.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/extract_emb.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/extract_emb.py new file mode 100644 index 0000000000000000000000000000000000000000..6e0f4cfa8402db1cbce9c06e74ffb5d6c2ad1949 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/extract_emb.py @@ -0,0 +1,169 @@ +# Copyright (c) 2022 Xu Xiang +# 2022 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import argparse +import kaldiio +from collections import OrderedDict + +import numpy as np +from tqdm import tqdm + +import onnxruntime as ort +from wespeaker.utils.utils import validate_path + + +def init_session(source, device): + # Initialize ONNX session + if device == "cpu": + providers = ["CPUExecutionProvider"] + elif device == "cuda": + providers = ["CUDAExecutionProvider"] + else: + raise ValueError + + opts = ort.SessionOptions() + opts.inter_op_num_threads = 1 + opts.intra_op_num_threads = 1 + opts.log_severity_level = 1 + session = ort.InferenceSession(source, + sess_options=opts, + providers=providers) + return session + + +def read_fbank(scp_file): + fbank_dict = OrderedDict() + + for utt, fbank in kaldiio.load_scp_sequential(scp_file): + fbank_dict[utt] = fbank + return fbank_dict + + +def subsegment(fbank, seg_id, window_fs, period_fs, frame_shift): + subsegs = [] + subseg_fbanks = [] + + seg_begin, seg_end = seg_id.split('-')[-2:] + seg_length = (int(seg_end) - int(seg_begin)) // frame_shift + + # We found that the num_frames + 2 equals to seg_length, which is caused + # by the implementation of torchaudio.compliance.kaldi.fbank. + # Thus, here seg_length is used to get the subsegs. + num_frames, feat_dim = fbank.shape + if seg_length <= window_fs: + subseg = seg_id + "-{:08d}-{:08d}".format(0, seg_length) + subseg_fbank = np.resize(fbank, (window_fs, feat_dim)) + + subsegs.append(subseg) + subseg_fbanks.append(subseg_fbank) + else: + max_subseg_begin = seg_length - window_fs + period_fs + for subseg_begin in range(0, max_subseg_begin, period_fs): + subseg_end = min(subseg_begin + window_fs, seg_length) + subseg = seg_id + "-{:08d}-{:08d}".format(subseg_begin, subseg_end) + subseg_fbank = np.resize(fbank[subseg_begin:subseg_end], + (window_fs, feat_dim)) + + subsegs.append(subseg) + subseg_fbanks.append(subseg_fbank) + + return subsegs, subseg_fbanks + + +def extract_embeddings(fbanks, batch_size, session, subseg_cmn): + fbanks_array = np.stack(fbanks) + if subseg_cmn: + fbanks_array = fbanks_array - np.mean( + fbanks_array, axis=1, keepdims=True) + + embeddings = [] + for i in tqdm(range(0, fbanks_array.shape[0], batch_size)): + batch_feats = fbanks_array[i:i + batch_size] + batch_embs = session.run(input_feed={'feats': batch_feats}, + output_names=['embs'])[0].squeeze() + + embeddings.append(batch_embs) + embeddings = np.vstack(embeddings) + + return embeddings + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--scp', required=True, help='wav scp') + parser.add_argument('--ark-path', + required=True, + help='path to store embedding ark') + parser.add_argument('--source', required=True, help='onnx model') + parser.add_argument('--device', + default='cuda', + help='inference device type: cpu or cuda') + parser.add_argument('--batch-size', + type=int, + default=96, + help='batch size for embedding extraction') + parser.add_argument('--frame-shift', + type=int, + default=10, + help='frame shift in fbank extraction (ms)') + parser.add_argument('--window-secs', + type=float, + default=1.50, + help='the window seconds in embedding extraction') + parser.add_argument('--period-secs', + type=float, + default=0.75, + help='the shift seconds in embedding extraction') + parser.add_argument('--subseg-cmn', + default=True, + type=lambda x: x.lower() == 'true', + help='do cmn after or before fbank sub-segmentation') + args = parser.parse_args() + + return args + + +def main(): + args = get_args() + + # transform duration to frame number + window_fs = int(args.window_secs * 1000) // args.frame_shift + period_fs = int(args.period_secs * 1000) // args.frame_shift + + session = init_session(args.source, args.device) + fbank_dict = read_fbank(args.scp) + + subsegs, subseg_fbanks = [], [] + for seg_id, fbank in fbank_dict.items(): + tmp_subsegs, tmp_subseg_fbanks = subsegment(fbank, seg_id, window_fs, + period_fs, + args.frame_shift) + subsegs.extend(tmp_subsegs) + subseg_fbanks.extend(tmp_subseg_fbanks) + embeddings = extract_embeddings(subseg_fbanks, args.batch_size, session, + args.subseg_cmn) + + validate_path(args.ark_path) + emb_ark = os.path.abspath(args.ark_path) + emb_scp = emb_ark[:-3] + "scp" + + with kaldiio.WriteHelper('ark,scp:' + emb_ark + "," + emb_scp) as writer: + for i, subseg_id in enumerate(subsegs): + writer(subseg_id, embeddings[i]) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_fbank.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_fbank.py new file mode 100644 index 0000000000000000000000000000000000000000..66d8359b2788bfc3faaabc88e56deda45028a1a9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_fbank.py @@ -0,0 +1,126 @@ +# Copyright (c) 2022 Xu Xiang +# 2022 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import argparse +import kaldiio +from collections import OrderedDict + +from tqdm import tqdm + +import torch +import torchaudio +import torchaudio.compliance.kaldi as kaldi + +from wespeaker.utils.utils import validate_path + + +def read_scp(scp_file): + utt_to_wav = OrderedDict() + for line in open(scp_file, 'r'): + utt, wav = line.strip().split() + utt_to_wav[utt] = wav + + return utt_to_wav + + +def read_segments(segments_file): + utt_to_segments = OrderedDict() + for line in open(segments_file, 'r'): + seg, utt, begin, end = line.strip().split() + begin, end = float(begin), float(end) + if utt not in utt_to_segments: + utt_to_segments[utt] = [(seg, begin, end)] + else: + utt_to_segments[utt].append((seg, begin, end)) + + return utt_to_segments + + +def get_speech_segments(utt_to_wav, utt_to_segments): + speech_segments_id = [] + speech_segments = [] + + for utt, wav_path in utt_to_wav.items(): + segments = utt_to_segments[utt] + signal, sr = torchaudio.load(wav_path) + signal = signal.squeeze() + + for seg, begin, end in segments: + speech_segments_id.append(seg) + speech_segments.append(signal[int(begin * sr):int(end * sr)]) + + return speech_segments_id, speech_segments + + +def compute_fbank(wav, + num_mel_bins=80, + frame_length=25, + frame_shift=10, + dither=0.0, + sample_frequency=16000, + subseg_cmn=True): + + wav = wav.unsqueeze(0) * (1 << 15) + feat = kaldi.fbank(wav, + num_mel_bins=num_mel_bins, + frame_length=frame_length, + frame_shift=frame_shift, + dither=dither, + sample_frequency=sample_frequency, + window_type='hamming', + use_energy=False) + if not subseg_cmn: + feat = feat - torch.mean(feat, dim=0) # CMN + + return feat.cpu().numpy() + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--scp', required=True, help='wav scp') + parser.add_argument('--segments', required=True, help='vad segments') + parser.add_argument('--ark-path', + required=True, + help='path to store feat ark') + parser.add_argument('--subseg-cmn', + default=True, + type=lambda x: x.lower() == 'true', + help='do cmn after or before fbank sub-segmentation') + args = parser.parse_args() + + return args + + +def main(): + args = get_args() + + utt_to_wav = read_scp(args.scp) + utt_to_segments = read_segments(args.segments) + speech_segments_id, speech_segments = get_speech_segments( + utt_to_wav, utt_to_segments) + + validate_path(args.ark_path) + feat_ark = os.path.abspath(args.ark_path) + feat_scp = feat_ark[:-3] + "scp" + + with kaldiio.WriteHelper('ark,scp:' + feat_ark + "," + feat_scp) as writer: + for i, speech_seg in enumerate(tqdm(speech_segments)): + fbank_feat = compute_fbank(speech_seg, subseg_cmn=args.subseg_cmn) + writer(speech_segments_id[i], fbank_feat) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_oracle_sad.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_oracle_sad.py new file mode 100644 index 0000000000000000000000000000000000000000..13055db78c2f417ec379701623f748c8394a3e98 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_oracle_sad.py @@ -0,0 +1,87 @@ +# Copyright (c) 2022 Xu Xiang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +from collections import OrderedDict + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--rttm', required=True, help='reference rttm') + parser.add_argument('--min-duration', + required=True, + type=float, + help='min duration') + args = parser.parse_args() + + return args + + +def read_rttm(rttm_file): + utt_to_segments = OrderedDict() + + for line in open(rttm_file, 'r'): + line = line.strip().split() + utt, begin, duration = line[1], line[3], line[4] + begin = float(begin) + end = begin + float(duration) + if utt not in utt_to_segments: + utt_to_segments[utt] = [(begin, end)] + else: + utt_to_segments[utt].append((begin, end)) + + for utt in utt_to_segments.keys(): + utt_to_segments[utt].sort() + + return utt_to_segments + + +def merge_segments(utt_to_segments, min_duration): + utt_to_merged_segments = OrderedDict() + + for utt, segments in utt_to_segments.items(): + utt_to_merged_segments[utt] = [] + if len(segments) > 0: + (begin, end) = segments[0] + for (b, e) in segments[1:]: + assert begin <= b + if b <= end: + end = max(end, e) + else: + if end - begin >= min_duration: + utt_to_merged_segments[utt].append((begin, end)) + begin, end = b, e + + if end - begin >= min_duration: + utt_to_merged_segments[utt].append((begin, end)) + + return utt_to_merged_segments + + +def main(): + args = get_args() + + utt_to_segments = read_rttm(args.rttm) + utt_to_merged_segments = merge_segments(utt_to_segments, args.min_duration) + + segments_line_spec = "{}-{:08d}-{:08d} {} {:.3f} {:.3f}" + for utt, segments in utt_to_merged_segments.items(): + for (begin, end) in segments: + print( + segments_line_spec.format(utt, int(begin * 1000), + int(end * 1000), utt, begin, end)) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_rttm.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_rttm.py new file mode 100644 index 0000000000000000000000000000000000000000..4a8754303ec04e2080317868297e0012259cbdc5 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_rttm.py @@ -0,0 +1,86 @@ +# Copyright (c) 2022 Xu Xiang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +from collections import OrderedDict + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--labels', + required=True, + help='class labels generated by clusterer') + parser.add_argument('--channel', + type=int, + default=1, + help='channel number in RTTM format') + args = parser.parse_args() + + return args + + +def read_labels(labels_file, frame_shift=10): + utt_to_subseg_labels = OrderedDict() + for line in open(labels_file, 'r'): + subseg, label = line.strip().split() + utt, begin_ms, end_ms, begin_frames, end_frames = subseg.split('-') + begin = (int(begin_ms) + int(begin_frames) * frame_shift) / 1000.0 + end = (int(begin_ms) + int(end_frames) * frame_shift) / 1000.0 + if utt not in utt_to_subseg_labels: + utt_to_subseg_labels[utt] = [(begin, end, label)] + else: + utt_to_subseg_labels[utt].append((begin, end, label)) + return utt_to_subseg_labels + + +def merge_segments(utt_to_subseg_labels): + merged_segment_to_labels = [] + + for utt, subseg_to_labels in utt_to_subseg_labels.items(): + if len(subseg_to_labels) == 0: + continue + + (begin, end, label) = subseg_to_labels[0] + e = end # when there is only one subseg, we assign end to e + for (b, e, la) in subseg_to_labels[1:]: + if b <= end and la == label: + end = e + elif b > end: + merged_segment_to_labels.append((utt, begin, end, label)) + begin, end, label = b, e, la + elif b <= end and la != label: + pivot = (b + end) / 2.0 + merged_segment_to_labels.append((utt, begin, pivot, label)) + begin, end, label = pivot, e, la + else: + raise ValueError + merged_segment_to_labels.append((utt, begin, e, label)) + + return merged_segment_to_labels + + +def main(): + args = get_args() + + subseg_to_labels = read_labels(args.labels) + channel = args.channel + + merged_segment_to_labels = merge_segments(subseg_to_labels) + rttm_line_spec = "SPEAKER {} {} {:.3f} {:.3f} {} " + for (utt, begin, end, label) in merged_segment_to_labels: + print(rttm_line_spec.format(utt, channel, begin, end - begin, label)) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_system_sad.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_system_sad.py new file mode 100644 index 0000000000000000000000000000000000000000..a4a98911233a152628912dffa48b9a20ea94784d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/make_system_sad.py @@ -0,0 +1,80 @@ +# Copyright (c) 2022-2024 Xu Xiang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["OPENBLAS_NUM_THREADS"] = "1" +os.environ["MKL_NUM_THREADS"] = "1" +os.environ["VECLIB_MAXIMUM_THREADS"] = "1" +os.environ["NUMEXPR_NUM_THREADS"] = "1" + +import functools +import concurrent.futures +import argparse + +import torch +import silero_vad +from wespeaker.utils.file_utils import read_scp + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--scp', required=True, help='wav scp') + parser.add_argument('--min-duration', + required=True, + type=float, + help='min duration') + args = parser.parse_args() + + return args + + +def vad(utt_wav_pair, + min_duration, + sampling_rate=16000, + threshold=0.18): + model = silero_vad.load_silero_vad() + + utt, wav = utt_wav_pair + + wav = silero_vad.read_audio(wav, sampling_rate=sampling_rate) + speech_timestamps = silero_vad.get_speech_timestamps( + wav, model, sampling_rate=sampling_rate, threshold=threshold) + + vad_result = "" + for item in speech_timestamps: + begin = item['start'] / sampling_rate + end = item['end'] / sampling_rate + if end - begin >= min_duration: + vad_result += "{}-{:08d}-{:08d} {} {:.3f} {:.3f}\n".format( + utt, int(begin * 1000), int(end * 1000), utt, begin, end) + + return vad_result + + +def main(): + args = get_args() + + run_vad = functools.partial(vad, min_duration=args.min_duration) + utt_wav_pair_list = read_scp(args.scp) + + with concurrent.futures.ProcessPoolExecutor() as executor: + print(''.join(executor.map(run_vad, utt_wav_pair_list)), end='') + + +if __name__ == '__main__': + torch.set_num_threads(1) + + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/spectral_clusterer.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/spectral_clusterer.py new file mode 100644 index 0000000000000000000000000000000000000000..d6cd30211c1efaf02a9ddcb9ce5ca54adc3e3a1c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/spectral_clusterer.py @@ -0,0 +1,130 @@ +# Copyright (c) 2022 Xu Xiang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["OPENBLAS_NUM_THREADS"] = "1" +os.environ["MKL_NUM_THREADS"] = "1" +os.environ["VECLIB_MAXIMUM_THREADS"] = "1" +os.environ["NUMEXPR_NUM_THREADS"] = "1" +import argparse +from collections import OrderedDict +import concurrent.futures as cf +import kaldiio + +import numpy as np +import scipy.linalg +from sklearn.cluster._kmeans import k_means +from wespeaker.utils.utils import validate_path + + +def cluster(embeddings, p=.01, num_spks=None, min_num_spks=1, max_num_spks=20): + # Define utility functions + def cosine_similarity(M): + M = M / np.linalg.norm(M, axis=1, keepdims=True) + return 0.5 * (1.0 + np.dot(M, M.T)) + + def prune(M, p): + m = M.shape[0] + if m < 1000: + n = max(m - 10, 2) + else: + n = int((1.0 - p) * m) + + for i in range(m): + indexes = np.argsort(M[i, :]) + low_indexes, high_indexes = indexes[0:n], indexes[n:m] + M[i, low_indexes] = 0.0 + M[i, high_indexes] = 1.0 + return 0.5 * (M + M.T) + + def laplacian(M): + M[np.diag_indices(M.shape[0])] = 0.0 + D = np.diag(np.sum(np.abs(M), axis=1)) + return D - M + + def spectral(M, num_spks, min_num_spks, max_num_spks): + eig_values, eig_vectors = scipy.linalg.eigh(M) + num_spks = num_spks if num_spks is not None \ + else np.argmax(np.diff(eig_values[:max_num_spks + 1])) + 1 + num_spks = max(num_spks, min_num_spks) + return eig_vectors[:, :num_spks] + + def kmeans(data): + k = data.shape[1] + # centroids, labels = scipy.cluster.vq.kmeans2(data, k, minit='++') + _, labels, _ = k_means(data, k, random_state=None, n_init=10) + return labels + + # Fallback for trivial cases + if len(embeddings) <= 2: + return [0] * len(embeddings) + + # Compute similarity matrix + similarity_matrix = cosine_similarity(np.array(embeddings)) + # Prune matrix with p interval + pruned_similarity_matrix = prune(similarity_matrix, p) + # Compute Laplacian + laplacian_matrix = laplacian(pruned_similarity_matrix) + # Compute spectral embeddings + spectral_embeddings = spectral(laplacian_matrix, num_spks, min_num_spks, + max_num_spks) + # Assign class labels + labels = kmeans(spectral_embeddings) + + return labels + + +def read_emb(scp): + emb_dict = OrderedDict() + for sub_seg_id, emb in kaldiio.load_scp_sequential(scp): + utt = sub_seg_id.split('-')[0] + if utt not in emb_dict: + emb_dict[utt] = {} + emb_dict[utt]['sub_seg'] = [] + emb_dict[utt]['embs'] = [] + emb_dict[utt]['sub_seg'].append(sub_seg_id) + emb_dict[utt]['embs'].append(emb) + subsegs_list = [] + embeddings_list = [] + for utt, utt_emb_dict in emb_dict.items(): + subsegs_list.append(utt_emb_dict['sub_seg']) + embeddings_list.append(np.stack(utt_emb_dict['embs'])) + return subsegs_list, embeddings_list + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--scp', required=True, help='embedding scp') + parser.add_argument('--output', required=True, help='output label file') + args = parser.parse_args() + return args + + +def main(): + args = get_args() + subsegs_list, embeddings_list = read_emb(args.scp) + validate_path(args.output) + with cf.ProcessPoolExecutor() as executor, open(args.output, 'w') as f: + for (subsegs, labels) in zip(subsegs_list, + executor.map(cluster, embeddings_list)): + [ + print(subseg, label, file=f) + for (subseg, label) in zip(subsegs, labels) + ] + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/umap_clusterer.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/umap_clusterer.py new file mode 100644 index 0000000000000000000000000000000000000000..23b38ead0465697ec32026d6719989f88bc60753 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/diar/umap_clusterer.py @@ -0,0 +1,267 @@ +# Copyright (c) 2023 Xu Xiang +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["OPENBLAS_NUM_THREADS"] = "1" +os.environ["MKL_NUM_THREADS"] = "1" +os.environ["VECLIB_MAXIMUM_THREADS"] = "1" +os.environ["NUMEXPR_NUM_THREADS"] = "1" +os.environ["NUMBA_NUM_THREADS"] = "1" + +import argparse +import concurrent.futures +from collections import OrderedDict, defaultdict +import functools +import heapq + +import numpy as np + +import kaldiio +import umap +import hdbscan + + +class PAHC: + def __init__(self, merge_cutoff=0.3, min_cluster_size=3, absorb_cutoff=0.0): + self.merge_cutoff = merge_cutoff + self.min_cluster_size = min_cluster_size + self.absorb_cutoff = absorb_cutoff + + def fit_predict(self, labels, embeddings): + self.initialize(labels, embeddings) + self.merge_cluster() + self.absorb_cluster() + labels = self.relabel_cluster() + return labels + + def initialize(self, labels, embeddings): + self.labels = labels + self.embeddings = embeddings + self.active_clusters = set([]) + self.label_map = {} + self.cost_map = {} + self.heap = [] + self.next_index = -1 + + self.build_label_map() + self.build_cost_map() + + def merge_cluster(self): + while self.heap: + _, (i, j) = heapq.heappop(self.heap) + if i in self.active_clusters and j in self.active_clusters: + self.merge(i, j) + + def absorb_cluster(self): + minor_clusters = set() + major_clusters = set() + for k, indexes in self.label_map.items(): + if len(indexes) < self.min_cluster_size: + minor_clusters.add(k) + else: + major_clusters.add(k) + + if len(major_clusters) > 0: + for i in minor_clusters: + max_cost = -np.inf + for j in major_clusters: + pair = (i, j) if i < j else (j, i) + i_indexes, j_indexes = self.label_map[i], self.label_map[j] + factor = len(i_indexes) * len(j_indexes) + normalized_cost = self.cost_map[pair] / factor + if normalized_cost > max_cost: + max_cost = normalized_cost + closest_cluster = j + if max_cost >= self.absorb_cutoff: + self.label_map[closest_cluster].extend(self.label_map[i]) + self.eliminate(i) + + def relabel_cluster(self): + labels = [-1] * len(self.labels) + + for label, indexes in self.label_map.items(): + for index in indexes: + labels[index] = label + i = 0 + label_to_label = {} + for label in labels: + if label not in label_to_label: + label_to_label[label] = i + i += 1 + for i in range(len(labels)): + labels[i] = label_to_label[labels[i]] + return labels + + def eliminate(self, i): + del self.label_map[i] + self.active_clusters.remove(i) + + def build_label_map(self): + self.label_map = defaultdict(list) + + for i, label in enumerate(self.labels): + self.label_map[label].append(i) + + self.num_labeled = len(self.label_map) + + if -1 in self.label_map: + self.num_labeled -= 1 + for i, j in zip(range(self.num_labeled, + self.num_labeled + len(self.label_map[-1])), + self.label_map[-1]): + self.label_map[i].append(j) + del self.label_map[-1] + + def build_cost_map(self): + N = len(self.label_map) + self.active_clusters = set(range(N)) + self.next_index = N + + for i in range(N): + for j in range(i + 1, N): + i_indexes, j_indexes = self.label_map[i], self.label_map[j] + + if i < self.num_labeled and j < self.num_labeled: + self.cost_map[(i, j)] = -np.inf + continue + + self.cost_map[(i, j)] = self.compute_cost(i_indexes, j_indexes) + + factor = len(i_indexes) * len(j_indexes) + normalized_cost = self.cost_map[(i, j)] / factor + if normalized_cost >= self.merge_cutoff: + heapq.heappush(self.heap, (-normalized_cost, (i, j))) + + def compute_cost(self, i_indexes, j_indexes): + i_embedding = sum([ + self.l2norm(self.embeddings[i_index]) for i_index in i_indexes]) + j_embedding = sum([ + self.l2norm(self.embeddings[j_index]) for j_index in j_indexes]) + return np.dot(i_embedding, j_embedding) + + def merge(self, i, j): + i_indexes, j_indexes = self.label_map[i], self.label_map[j] + + for k, _ in self.label_map.items(): + if k == i or k == j: + continue + pair1 = (k, i) if k < i else (i, k) + pair2 = (k, j) if k < j else (j, k) + cost = self.cost_map[pair1] + self.cost_map[pair2] + self.cost_map[(k, self.next_index)] = cost + + factor = (len(i_indexes) + len(j_indexes)) * len(self.label_map[k]) + normalized_cost = cost / factor + if normalized_cost >= self.merge_cutoff: + heapq.heappush(self.heap, (-normalized_cost, + (k, self.next_index))) + + self.label_map[self.next_index] = i_indexes + j_indexes + self.active_clusters.add(self.next_index) + self.eliminate(i) + self.eliminate(j) + self.next_index += 1 + + def l2norm(self, x, axis=0, keepdims=True): + return x / np.linalg.norm(x, axis=axis, keepdims=keepdims) + + +def get_args(): + parser = argparse.ArgumentParser(description='') + parser.add_argument('--scp', required=True, help='embedding scp') + parser.add_argument('--output', required=True, help='output label file') + parser.add_argument('--n_neighbors', required=False, default=16, + help="The size of the local neighborhood UMAP " + "will look at when attempting to learn " + "the manifold structure of the data. " + "This means that low values of n_neighbors " + "will force UMAP to concentrate on " + "very local structure (potentially to " + "the detriment of the big picture), " + "while large values will push UMAP to " + "look at larger neighborhoods of each point " + "when estimating the manifold structure of " + "the data, losing fine detail structure for " + "the sake of getting the broader of the data.") + parser.add_argument('--min_dist', required=False, default=0.05, + help="The minimum distance between points in " + "the low dimensional representation.") + args = parser.parse_args() + return args + + +def read_emb(scp): + emb_dict = OrderedDict() + for sub_seg_id, emb in kaldiio.load_scp_sequential(scp): + utt = sub_seg_id.split('-')[0] + if utt not in emb_dict: + emb_dict[utt] = {} + emb_dict[utt]['sub_seg'] = [] + emb_dict[utt]['embs'] = [] + emb_dict[utt]['sub_seg'].append(sub_seg_id) + emb_dict[utt]['embs'].append(emb) + subsegs_list = [] + embeddings_list = [] + for utt, utt_emb_dict in emb_dict.items(): + subsegs_list.append(utt_emb_dict['sub_seg']) + embeddings_list.append(np.stack(utt_emb_dict['embs'])) + return subsegs_list, embeddings_list + + +def cluster(embeddings, n_neighbors=16, min_dist=0.05): + # Fallback + if len(embeddings) <= 2: + return [0] * len(embeddings) + + umap_embeddings = umap.UMAP(n_components=min(32, len(embeddings) - 2), + metric='cosine', + n_neighbors=n_neighbors, + min_dist=min_dist, + random_state=2023, + n_jobs=1).fit_transform(np.array(embeddings)) + + labels = hdbscan.HDBSCAN(allow_single_cluster=True, + min_cluster_size=4, + approx_min_span_tree=False, + core_dist_n_jobs=1).fit_predict(umap_embeddings) + + labels = PAHC(merge_cutoff=0.3, + min_cluster_size=3, + absorb_cutoff=0.0).fit_predict(labels, embeddings) + return labels + + +if __name__ == '__main__': + args = get_args() + + subsegs_list, embeddings_list = read_emb(args.scp) + + os.makedirs(os.path.dirname(args.output), exist_ok=True) + + n_neighbors, min_dist = int(args.n_neighbors), float(args.min_dist) + + run_cluster = functools.partial(cluster, + n_neighbors=n_neighbors, + min_dist=min_dist) + + with concurrent.futures.ProcessPoolExecutor() as executor: + with open(args.output, 'w') as fd: + for (subsegs, labels) in zip(subsegs_list, + executor.map(run_cluster, + embeddings_list)): + [print(subseg, + label, + file=fd) for (subseg, label) in zip(subsegs, labels)] diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/__init__.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1c7139d9e84d831141952bc0d69f7ab49c40c94c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/__init__.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from .s3prl import S3prlFrontend +from .whisper_encoder import whisper_encoder +from .w2vbert import W2VBertFrontend +from .tfmel import TFMelFrontend + +frontend_class_dict = { + 'fbank': None, + 's3prl': S3prlFrontend, + 'whisper_encoder': whisper_encoder, + 'w2vbert': W2VBertFrontend, + 'tfmel': TFMelFrontend, +} diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/s3prl.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/s3prl.py new file mode 100644 index 0000000000000000000000000000000000000000..37bf8c8ee9f44469327042df761cb322be94fce8 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/s3prl.py @@ -0,0 +1,93 @@ +# Copyright (c) 2024 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import torch +import torch.nn as nn + +import s3prl +from s3prl.nn import Featurizer, S3PRLUpstream + + +class S3prlFrontend(nn.Module): + """Speech Pretrained Representation Frontend.""" + + def __init__(self, + upstream_args: dict, + download_dir: str = "./s3prl_hub", + multilayer_feature: bool = True, + layer: int = -1, + frozen: bool = False, + frame_shift: int = 20, + frame_length: int = 20, + sample_rate: int = 16000): + super().__init__() + + self.multilayer_feature = multilayer_feature + self.layer = layer + self.frozen = frozen + + if download_dir is not None: + s3prl.util.download.set_dir(download_dir) + + assert upstream_args.get("name", + None) in S3PRLUpstream.available_names() + self.upstream = S3PRLUpstream( + upstream_args.get("name"), + path_or_url=upstream_args.get("path_or_url", None), + normalize=upstream_args.get("normalize", False), + extra_conf=upstream_args.get("extra_conf", None), + ) + if getattr(self.upstream.upstream, "model", None): + if getattr(self.upstream.upstream.model, "feature_grad_mult", + None) is not None: + self.upstream.upstream.model.feature_grad_mult = 1.0 + self.upstream.eval() + + if layer != -1: + layer_selections = [layer] + assert not multilayer_feature, \ + "multilayer_feature must be False if layer is specified" + else: + layer_selections = None + self.featurizer = Featurizer(self.upstream, + layer_selections=layer_selections) + + assert self.featurizer.downsample_rate == sample_rate * frame_shift // 1000 + + if self.frozen: + for param in self.upstream.parameters(): + param.requires_grad_(False) + else: + for name, param in self.upstream.named_parameters(): + if "mask_emb" in name: + param.requires_grad_(False) + + def output_size(self): + return self.featurizer.output_size + + def forward(self, input: torch.Tensor, input_lengths: torch.LongTensor): + with torch.no_grad() if self.frozen else contextlib.nullcontext(): + feats, feats_lens = self.upstream(input, input_lengths) + if self.layer != -1: + layer = self.layer + feats, feats_lens = feats[layer], feats_lens[layer] + return feats, feats_lens + + if self.multilayer_feature: + feats, feats_lens = self.featurizer(feats, feats_lens) + else: + feats, feats_lens = self.featurizer(feats[-1:], feats_lens[-1:]) + + return feats, feats_lens diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/tfmel.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/tfmel.py new file mode 100644 index 0000000000000000000000000000000000000000..9d899c2bf9c4ce45059d8be248bb91ba8b345b2b --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/tfmel.py @@ -0,0 +1,420 @@ +# Copyright (c) 2026 Bosen Xu (2332974001@qq.com) +# Based on original code from PalabraAI/redimnet2 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from scipy.signal import windows + + +def hz2mel(hz): + return 2595 * np.log10(1 + hz / 700.) + + +def mel2hz(mel): + return 700 * (10 ** (mel / 2595.0) - 1) + + +def get_filterbanks(low_freq=20, high_freq=7600, nfilt=80, nfft=512, samplerate=16000): + lowmel = hz2mel(low_freq) + highmel = hz2mel(high_freq) + melpoints = np.linspace(lowmel, highmel, nfilt + 2) + lower_edge_mel = melpoints[:-2].reshape(1, -1) + center_mel = melpoints[1:-1].reshape(1, -1) + upper_edge_mel = melpoints[2:].reshape(1, -1) + spectrogram_bins_mel = hz2mel( + np.linspace(0, samplerate // 2, nfft))[1:].reshape(-1, 1) + lower_slopes = (spectrogram_bins_mel - lower_edge_mel) / ( + center_mel - lower_edge_mel) + upper_slopes = (upper_edge_mel - spectrogram_bins_mel) / ( + upper_edge_mel - center_mel) + mel_weights_matrix = np.maximum( + 0.0, np.minimum(lower_slopes, upper_slopes)) + return np.vstack([np.zeros((1, nfilt)), mel_weights_matrix])[:, :].astype('float32') + + +class NormalizeAudio(nn.Module): + def __init__(self, eps: float = 1e-6): + super().__init__() + self.eps = eps + + def forward(self, x): + if x.ndim == 2: + x = x.unsqueeze(1) + return (x - x.mean(dim=2, keepdims=True)) / ( + x.std(dim=2, keepdims=True, unbiased=False) + self.eps) + + +class PreEmphasis(nn.Module): + def __init__(self, coef: float = 0.97): + super().__init__() + self.coef = coef + self.register_buffer( + 'flipped_filter', + torch.FloatTensor([-self.coef, 1.]).unsqueeze(0).unsqueeze(0)) + + def forward(self, x): + if x.ndim == 2: + x = x.unsqueeze(1) + x = F.pad(x, (1, 0), 'reflect') + return F.conv1d(x, self.flipped_filter).squeeze(1) + + +class FbankAug(nn.Module): + def __init__(self, freq_mask_width=(0, 8), time_mask_width=(0, 10), + freq_start_bin=0): + super().__init__() + self.time_mask_width = time_mask_width + self.freq_mask_width = freq_mask_width + self.freq_start_bin = freq_start_bin + + def mask_along_axis(self, x, dim): + original_size = x.shape + batch, fea, time = x.shape + if dim == 1: + D = fea + width_range = self.freq_mask_width + else: + D = time + width_range = self.time_mask_width + mask_len = torch.randint( + width_range[0], width_range[1], (batch, 1), + device=x.device).unsqueeze(2) + mask_pos = torch.randint( + self.freq_start_bin, max(1, D - mask_len.max()), + (batch, 1), device=x.device).unsqueeze(2) + arange = torch.arange(D, device=x.device).view(1, 1, -1) + mask = (mask_pos <= arange) * (arange < (mask_pos + mask_len)) + mask = mask.any(dim=1) + if dim == 1: + mask = mask.unsqueeze(2) + else: + mask = mask.unsqueeze(1) + x = x.masked_fill_(mask, 0.0) + return x.view(*original_size) + + def forward(self, x): + x = self.mask_along_axis(x, dim=2) + x = self.mask_along_axis(x, dim=1) + return x + + +class SpectralFeaturesTF(nn.Module): + def __init__( + self, + frame_length=400, + frame_step=160, + fft_length=512, + sample_rate=16000, + window='hann', + normalize_spectrogram=False, + normalize_signal=False, + eps=1e-8, + mode='melbanks', + low_freq=20, + high_freq=7600, + num_bins=80, + log_mels=True, + fft_mode='abs', + sqrt_real_imag=False, + return_img=False, + **kwargs): + super().__init__() + self.length = frame_length + self.shift = frame_step + self.sqrt_real_imag = sqrt_real_imag + self.normalize_spectrogram = normalize_spectrogram + self.normalize_signal = normalize_signal + self.window = window + self.eps = eps + self.nfft = fft_length if fft_length else frame_length + self.samplerate = sample_rate + self.features = mode + self.low_freq = low_freq + self.high_freq = high_freq + self.num_bins = num_bins + self.return_img = return_img + if mode in ['melbanks', 'mfcc']: + fft_mode = 'abs' + self.fft_mode = fft_mode + self.log_mels = log_mels + self.build() + + def build(self): + if self.window: + if self.window == 'hamming': + self.window = windows.hamming(self.length) + elif self.window in ['hann', 'hanning']: + self.window = np.array([ + 0.5 - 0.5 * (np.cos((2 * np.pi * l) / (self.length - 1))) + for l in range(self.length)]) + elif self.window == 'sqrt_hann': + self.window = np.array([ + 0.5 - 0.5 * (np.cos((2 * np.pi * l) / (self.length - 1))) + for l in range(self.length)]) ** 0.5 + elif self.window == 'kaiser': + self.window = windows.kaiser(self.length) + else: + self.window = np.ones(self.length) + self.window = self.window.astype("float32") + real_kernel = np.asarray([ + np.cos(2 * np.pi * np.arange(0, self.nfft) * n / self.nfft) + for n in range(self.nfft)]).astype("float32").T + self.real_kernel = real_kernel[:self.length, :self.nfft // 2] + if self.window is not None: + self.real_kernel *= self.window[:, None] + self.real_kernel = self.real_kernel[:, None, :] + image_kernel = np.asarray([ + np.sin(2 * np.pi * np.arange(0, self.nfft) * n / self.nfft) + for n in range(self.nfft)]).astype("float32").T + self.image_kernel = image_kernel[:self.length, :self.nfft // 2] + if self.window is not None: + self.image_kernel *= self.window[:, None] + self.image_kernel = self.image_kernel[:, None, :] + self.register_buffer( + 'real_kernel_pt', + torch.from_numpy(self.real_kernel).permute(2, 1, 0).float()) + self.register_buffer( + 'image_kernel_pt', + torch.from_numpy(self.image_kernel).permute(2, 1, 0).float()) + if self.features in ['melbanks']: + linear_to_mel_weight_matrix = get_filterbanks( + nfilt=self.num_bins, + nfft=self.nfft // 2, + samplerate=self.samplerate, + low_freq=self.low_freq, + high_freq=self.high_freq) + linear_to_mel_weight_matrix = linear_to_mel_weight_matrix[:, :, None] + self.register_buffer( + 'melbanks_pt', + torch.from_numpy(linear_to_mel_weight_matrix).permute( + 1, 0, 2).float()) + + def forward(self, inputs): + dtype = inputs.dtype + inputs = inputs.float() + if inputs.ndim == 2: + inputs = inputs.unsqueeze(1) + if self.normalize_signal: + inputs = (inputs - inputs.mean(dim=2, keepdims=True)) / \ + (inputs.std(dim=2, keepdims=True, unbiased=False) + self.eps) + real_part = F.conv1d( + inputs, self.real_kernel_pt, stride=self.shift, + padding=self.shift // 2) + imag_part = F.conv1d( + inputs, self.image_kernel_pt, stride=self.shift, + padding=self.shift // 2) + if self.features == 'complex': + return [real_part, imag_part] + fft = torch.square(real_part) + torch.square(imag_part) + if self.sqrt_real_imag: + fft = torch.sqrt(fft) + feat = fft.clip(self.eps, 1 / self.eps) + if self.fft_mode == 'log': + feat = torch.log(feat) + if self.features in ['melbanks']: + mel_spectrograms = F.conv1d(feat, self.melbanks_pt, stride=1, padding=0) + mel_spectrograms = mel_spectrograms.clip(self.eps, 1 / self.eps) + if self.log_mels: + feat = torch.log(mel_spectrograms) + else: + feat = mel_spectrograms + if self.normalize_spectrogram: + feat = (feat - feat.mean(dim=(1, 2), keepdims=True)) / \ + (feat.std(dim=(1, 2), keepdims=True, unbiased=False) + self.eps) + if self.return_img: + feat = feat[:, None, :, :] + return feat.to(dtype) + + +class TFMelBanks(nn.Module): + def __init__( + self, + sample_rate=16000, + n_fft=512, + win_length=400, + hop_length=160, + f_min=20, + f_max=7600, + n_mels=80, + do_spec_aug=False, + norm_signal=False, + do_preemph=True, + freq_start_bin=0, + freq_mask_width=( + 0, + 8), + time_mask_width=(0, 10), + eps=1e-8): + super(TFMelBanks, self).__init__() + self.torchfbank = torch.nn.Sequential( + NormalizeAudio(eps) if norm_signal else nn.Identity(), + PreEmphasis() if do_preemph else nn.Identity(), + SpectralFeaturesTF( + frame_length=win_length, + frame_step=hop_length, + fft_length=n_fft, + sample_rate=sample_rate, + window='hamming', + normalize_spectrogram=False, + normalize_signal=False, + eps=eps, + mode='melbanks', + low_freq=f_min, + high_freq=f_max, + num_bins=n_mels, + log_mels=False, + fft_mode='abs', + sqrt_real_imag=False, + return_img=False)) + self.eps = eps + if do_spec_aug: + self.specaug = FbankAug( + freq_start_bin=freq_start_bin, + freq_mask_width=freq_mask_width, + time_mask_width=time_mask_width) + else: + self.specaug = nn.Identity() + + def forward(self, x): + xdtype = x.dtype + x = x.float() + with torch.no_grad(): + with torch.cuda.amp.autocast(enabled=False): + x = self.torchfbank(x) + self.eps + x = x.log() + x = x - torch.mean(x, dim=-1, keepdim=True) + if self.training: + x = self.specaug(x) + return x.to(xdtype) + + +class TFSpectrogram(nn.Module): + def __init__( + self, + sample_rate=16000, + n_fft=512, + win_length=400, + hop_length=160, + f_min=20, + f_max=7600, + n_mels=80, + window='hanning', + normalize_spectrogram=False, + normalize_signal=False, + mode='fft', + fft_mode='abs', + pool_freqs=( + 2, + 1), + do_spec_aug=False, + norm_signal=False, + do_preemph=True, + freq_start_bin=0, + num_apply_spec_aug=1, + freq_mask_width=(0, 8), + time_mask_width=(0, 10), + eps=1e-8): + super(TFSpectrogram, self).__init__() + self.num_apply_spec_aug = num_apply_spec_aug + self.spectrogram = torch.nn.Sequential( + NormalizeAudio() if norm_signal else nn.Identity(), + PreEmphasis() if do_preemph else nn.Identity(), + SpectralFeaturesTF( + frame_length=win_length, + frame_step=hop_length, + fft_length=n_fft, + sample_rate=sample_rate, + window=window, + eps=eps, + mode=mode, + low_freq=f_min, + high_freq=f_max, + num_bins=n_mels, + normalize_spectrogram=False, + normalize_signal=False, + fft_mode='abs', + log_mels=False, + sqrt_real_imag=False, + return_img=False)) + if pool_freqs is not None: + self.pool_freq = nn.AvgPool2d(pool_freqs, stride=pool_freqs) + else: + self.pool_freq = nn.Identity() + self.eps = eps + if do_spec_aug: + self.specaug = FbankAug( + freq_start_bin=freq_start_bin, + freq_mask_width=freq_mask_width, + time_mask_width=time_mask_width) + else: + self.specaug = nn.Identity() + + def forward(self, x): + xdtype = x.dtype + x = x.float() + with torch.no_grad(): + with torch.cuda.amp.autocast(enabled=False): + x = self.spectrogram(x) + self.eps + x = x.log() + x = x - torch.mean(x, dim=-1, keepdim=True) + if self.training: + for _ in range(self.num_apply_spec_aug): + x = self.specaug(x) + x = self.pool_freq(x.unsqueeze(1)) + return x.to(xdtype) + + +class TFMelFrontend(nn.Module): + def __init__(self, + sample_rate=16000, + n_fft=512, + win_length=400, + hop_length=160, + f_min=20, + f_max=7600, + n_mels=80, + do_spec_aug=False, + norm_signal=False, + do_preemph=True, + freq_mask_width=(0, 8), + time_mask_width=(0, 10), + eps=1e-8, + **kwargs): + super().__init__() + self.n_mels = n_mels + self.spec = TFMelBanks( + sample_rate=sample_rate, + n_fft=n_fft, + win_length=win_length, + hop_length=hop_length, + f_min=f_min, + f_max=f_max, + n_mels=n_mels, + do_spec_aug=do_spec_aug, + norm_signal=norm_signal, + do_preemph=do_preemph, + freq_mask_width=freq_mask_width, + time_mask_width=time_mask_width, + eps=eps, + ) + + def output_size(self): + return self.n_mels + + def forward(self, wavs, wavs_len=None): + features = self.spec(wavs) + return features, None diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/w2vbert.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/w2vbert.py new file mode 100644 index 0000000000000000000000000000000000000000..262e4a82fe1e3191d987e7be0c8b61263fd1c3e5 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/w2vbert.py @@ -0,0 +1,386 @@ +# Copyright (c) 2025 Qituan Shangguan (2369144677@qq.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import json +import torch +import torch.nn as nn +from transformers import ( + Wav2Vec2BertModel, + Wav2Vec2BertConfig, + BitsAndBytesConfig, + AutoFeatureExtractor, + AutoModel, +) +from peft import LoraConfig, get_peft_model + + +def create_bnb_config( + load_in_8bit=True, + bnb_8bit_use_double_quant=True, + bnb_8bit_quant_type="llm_int8", + bnb_8bit_compute_dtype="bfloat16", +): + # Note: llm_int8 might not be the optimal quant_type for audio models. + # Consider "int8" or potentially "nf4" if available and suitable. + # Also, compute_dtype might need adjustment based on hardware capability. + return BitsAndBytesConfig( + load_in_8bit=load_in_8bit, + bnb_8bit_use_double_quant=bnb_8bit_use_double_quant, + bnb_8bit_quant_type=bnb_8bit_quant_type, + bnb_8bit_compute_dtype=getattr(torch, bnb_8bit_compute_dtype), + ) + + +def create_lora_config( + model_type="w2v-bert", + r=16, + lora_alpha=32, + target_modules=None, + lora_dropout=0.1, + bias="none", +): + """LoRA config specifically for w2v-bert.""" + if model_type != "w2v-bert": + raise ValueError( + "This function is specifically for w2v-bert, got " + f"{model_type}" + ) + + if target_modules is None: + # Common targets for Wav2Vec2Bert. + target_modules = ["linear_q", "linear_v"] + + # Task type for feature extraction is suitable here. + task_type = "FEATURE_EXTRACTION" + + return LoraConfig( + r=r, + lora_alpha=lora_alpha, + target_modules=target_modules, + lora_dropout=lora_dropout, + bias=bias, + task_type=task_type, + ) + + +class W2VBertFrontend(nn.Module): + """ + Wav2Vec2-BERT Frontend for wespeaker, potentially with LoRA. + """ + + PEFT_INDICATORS = ["lora_", "adapter", "prefix", "prompt"] + + def __init__( + self, + model_name="facebook/w2v-bert-2.0", + download_dir="./w2vbert_hub", + frozen=True, + use_bnb=False, + bnb_config_args=None, + use_lora=False, + lora_config_args=None, + model_config_file=None, + sample_rate=16000, + ): + super().__init__() + self.model_type = "w2v-bert" + self.model_config_file = model_config_file + self.frozen_encoder = frozen + self.use_lora = use_lora + self.sr = sample_rate + + if not frozen and use_bnb: + raise ValueError( + "Full fine-tuning (frozen=False) and quantization " + "(use_bnb=True) are not supported simultaneously." + ) + + if os.path.isdir(model_name): + local_model_path = model_name + else: + local_model_path = model_name + + try: + self.processor = AutoFeatureExtractor.from_pretrained( + local_model_path, + local_files_only=os.path.isdir(local_model_path), + ) + except Exception as e: + raise IOError( + "Failed to load feature extractor from " + f"{local_model_path}: {e}" + ) from e + + bnb_config = ( + create_bnb_config(**(bnb_config_args or {})) if use_bnb else None + ) + self._setup_model(local_model_path, bnb_config) + + if use_lora: + lora_config = create_lora_config( + model_type=self.model_type, + **(lora_config_args or {}), + ) + self.encoder = get_peft_model(self.encoder, lora_config) + print("LoRA applied to the model.") + if frozen: + print( + "Note: Encoder was initially frozen, but LoRA layers are " + "now trainable." + ) + self.frozen_encoder = True + + if frozen: + self.freeze_encoder() + else: + self.unfreeze_encoder() + + def forward(self, wavs: torch.Tensor, wavs_len: torch.Tensor): + wavs_cpu = wavs.cpu() + wavs_list = list(wavs_cpu) + + features = self.processor( + wavs_list, + return_tensors="pt", + sampling_rate=self.sr, + padding="longest", + ) + features = features.to(wavs.device) + + # Extract the required tensors from the features dictionary. + input_tensor = None + if "input_values" in features: + input_tensor = features["input_values"] + elif "input_features" in features: + input_tensor = features["input_features"] + elif "input_ids" in features: + input_tensor = features["input_ids"] + else: + raise ValueError( + "Could not find audio data in the processor output." + ) + + attention_mask_tensor = features.get("attention_mask") + + if self.use_lora: + # For Stage 1 (LoRA training), the model is a PeftModel wrapper. + # We need to access the underlying model. + outputs = self.encoder.base_model.model( + input_tensor, + attention_mask=attention_mask_tensor, + output_hidden_states=True, + return_dict=True, + ) + else: + # For Stage 2/3 (full fine-tuning) we call it directly. + outputs = self.encoder( + input_tensor, + attention_mask=attention_mask_tensor, + output_hidden_states=True, + return_dict=True, + ) + + last_hidden_state = outputs.last_hidden_state + all_hidden_states = outputs.hidden_states + + return all_hidden_states, last_hidden_state + + def _setup_model(self, model_path, bnb_config): + """Loads the Wav2Vec2BertModel.""" + try: + config_file_path = None + if ( + self.model_config_file is not None + and os.path.isfile( + os.path.join(model_path, self.model_config_file) + ) + ): + config_file_path = os.path.join( + model_path, + self.model_config_file, + ) + + if config_file_path is not None: + print( + "Loading model with custom config: " + f"{self.model_config_file}" + ) + with open(config_file_path, "r") as f: + config_dict = json.load(f) + config = Wav2Vec2BertConfig(**config_dict) + full_model = Wav2Vec2BertModel(config) + + # Load weights, potentially handling prune config naming. + ckpt_file = os.path.join( + model_path, + "model.safetensors", + ) + if not os.path.exists(ckpt_file): + ckpt_file = os.path.join( + model_path, + "pytorch_model.bin", + ) + + if ckpt_file.endswith(".safetensors"): + from safetensors.torch import load_file + + ckpt_state_dict = load_file(ckpt_file, device="cpu") + else: + ckpt_state_dict = torch.load( + ckpt_file, + map_location="cpu", + ) + + cur_state_dict = full_model.state_dict() + missing_keys, unexpected_keys = [], [] + for k in cur_state_dict.keys(): + # Skip potential pruning params. + if "hard_concrete" in k: + continue + if k in ckpt_state_dict: + if ( + cur_state_dict[k].shape + == ckpt_state_dict[k].shape + ): + cur_state_dict[k] = ckpt_state_dict[k] + else: + print( + "Shape mismatch for key " + f"{k}: model needs " + f"{cur_state_dict[k].shape}, checkpoint " + f"has {ckpt_state_dict[k].shape}" + ) + # Treat shape mismatch as missing for + # load_state_dict. + missing_keys.append(k) + else: + missing_keys.append(k) + + # Collect unexpected keys from checkpoint. + for k in ckpt_state_dict.keys(): + if k not in cur_state_dict: + unexpected_keys.append(k) + + load_result = full_model.load_state_dict( + cur_state_dict, + strict=False, + ) + print( + "Custom config model loaded. Missing: " + f"{load_result.missing_keys}, unexpected: " + f"{load_result.unexpected_keys}" + ) + else: + print(f"Loading model using AutoModel from: {model_path}") + full_model = AutoModel.from_pretrained( + model_path, + local_files_only=os.path.isdir(model_path), + quantization_config=bnb_config, + ) + + self.encoder = full_model + self.d_model = self.encoder.config.hidden_size + # W2VBERT does not have a direct equivalent to whisper's + # encoder_layers+1 for hidden states count in config. + # It depends on output_hidden_states=True during forward pass. + # We can store the config value for reference. + self.n_config_layers = self.encoder.config.num_hidden_layers + + # Remove the masked_spec_embed parameter that is not needed + # during fine-tuning. + if hasattr(self.encoder, "masked_spec_embed"): + delattr(self.encoder, "masked_spec_embed") + print("'masked_spec_embed' attribute removed.") + except Exception as e: + raise IOError(f"Failed to load model from {model_path}:{e}") from e + + def _is_peft_parameter(self, param_name): + return any( + indicator in param_name for indicator in self.PEFT_INDICATORS + ) + + def _module_has_peft_parameter(self, module): + if hasattr(module, "named_parameters"): + for param_name, _ in module.named_parameters(): + if self._is_peft_parameter(param_name): + return True + return False + + def freeze_encoder(self): + self.frozen_encoder = True + if hasattr(self, "encoder"): + print("Freezing base model parameters...") + for name, param in self.encoder.named_parameters(): + if not self._is_peft_parameter(name): + param.requires_grad = False + else: + print("Encoder not initialized yet, cannot freeze.") + + def unfreeze_encoder(self): + # If LoRA is used, unfreezing the whole encoder might not be intended. + # This method will make all non-LoRA params trainable. + self.frozen_encoder = False + if hasattr(self, "encoder"): + print("Unfreezing base model parameters...") + for name, param in self.encoder.named_parameters(): + if not self._is_peft_parameter(name): + param.requires_grad = True + else: + print("Encoder not initialized yet, cannot unfreeze.") + + def train(self, mode=True): + # Set the top-level module training mode. + super().train(mode) + if hasattr(self, "encoder"): + if self.frozen_encoder and self.use_lora: + # In frozen mode with LoRA: base model in eval, + # LoRA layers in train. + print( + "Setting train mode: base model in eval, " + "LoRA modules in train." + ) + self.encoder.eval() + for name, module in self.encoder.named_modules(): + # Check if the module itself is a LoRA layer or + # contains LoRA params. + if ( + "lora_" in name.lower() + or self._module_has_peft_parameter(module) + ): + module.train(mode) + elif self.frozen_encoder: + # Frozen mode without LoRA: everything in eval. + print("Setting train mode: entire encoder in eval (frozen).") + self.encoder.eval() + else: + # Not frozen: normal training for the entire encoder. + print( + "Setting train mode: entire encoder in " + f"{'train' if mode else 'eval'}." + ) + self.encoder.train(mode) + return self + + def output_size(self) -> int: + """Return the dimension of the output embedding.""" + # For adapter MFA, the input dim is d_model, but the final output dim + # depends on the adapter and pooling layers defined outside this + # frontend. This frontend should return the hidden dimension of the + # base model. + if not hasattr(self, "d_model"): + # Should be set during _setup_model. + raise AttributeError("Model dimension 'd_model' not set.") + # Output dim of each hidden state from W2V-BERT. + return self.d_model diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/whisper_encoder.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/whisper_encoder.py new file mode 100644 index 0000000000000000000000000000000000000000..949bafd54bdf0fa0e2880fc9470d978e4b031f33 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/frontend/whisper_encoder.py @@ -0,0 +1,321 @@ +# Copyright (c) 2024 Yiyang Zhao (zhaoyy22@mails.tsinghua.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import numpy as np +import torch +import torch.nn.functional as F +import torch.distributed as dist +from torch import Tensor +from torch import nn + +from typing import Iterable, Optional + +import os +import hashlib +import whisper +import logging +import urllib.request + + +class Linear(nn.Linear): + def forward(self, x: Tensor) -> Tensor: + return F.linear( + x, self.weight.to( + x.dtype), None if self.bias is None else self.bias.to( + x.dtype)) + + +class Conv1d(nn.Conv1d): + def _conv_forward(self, x: Tensor, weight: Tensor, + bias: Optional[Tensor]) -> Tensor: + return super()._conv_forward( + x, weight.to(x.dtype), None if bias is None else bias.to(x.dtype) + ) + + +class LayerNorm(nn.LayerNorm): + def forward(self, x: Tensor) -> Tensor: + return super().forward(x.float()).type(x.dtype) + + +def sinusoids(length, channels, max_timescale=10000): + """Returns sinusoids for positional embedding""" + assert channels % 2 == 0 + log_timescale_increment = np.log(max_timescale) / (channels // 2 - 1) + inv_timescales = torch.exp(-log_timescale_increment * + torch.arange(channels // 2)) + scaled_time = torch.arange( + length)[:, np.newaxis] * inv_timescales[np.newaxis, :] + return torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], dim=1) + + +class MultiHeadAttention(nn.Module): + def __init__(self, n_state: int, n_head: int): + super().__init__() + self.n_head = n_head + self.query = Linear(n_state, n_state) + self.key = Linear(n_state, n_state, bias=False) + self.value = Linear(n_state, n_state) + self.out = Linear(n_state, n_state) + + def forward( + self, + x: Tensor, + xa: Optional[Tensor] = None, + mask: Optional[Tensor] = None, + kv_cache: Optional[dict] = None, + ): + q = self.query(x) + + if kv_cache is None or xa is None or self.key not in kv_cache: + # hooks, if installed (i.e. kv_cache is not None), + # will prepend the cached kv tensors; otherwise, + # perform key/value projections for self- or + # cross-attention as usual. + k = self.key(x if xa is None else xa) + v = self.value(x if xa is None else xa) + else: + # for cross-attention, calculate keys and values once + # and reuse in subsequent calls. + k = kv_cache[self.key] + v = kv_cache[self.value] + + wv, qk = self.qkv_attention(q, k, v, mask) + return self.out(wv), qk + + def qkv_attention( + self, + q: Tensor, + k: Tensor, + v: Tensor, + mask: Optional[Tensor] = None): + n_batch, n_ctx, n_state = q.shape + scale = (n_state // self.n_head) ** -0.25 + q = q.view(*q.shape[:2], self.n_head, -1).permute(0, 2, 1, 3) * scale + k = k.view(*k.shape[:2], self.n_head, -1).permute(0, 2, 3, 1) * scale + v = v.view(*v.shape[:2], self.n_head, -1).permute(0, 2, 1, 3) + + qk = q @ k + if mask is not None: + qk = qk + mask[:n_ctx, :n_ctx] + qk = qk.float() + + w = F.softmax(qk, dim=-1).to(q.dtype) + return (w @ v).permute(0, 2, 1, 3).flatten(start_dim=2), qk.detach() + + +class ResidualAttentionBlock(nn.Module): + def __init__(self, n_state: int, n_head: int, + cross_attention: bool = False): + super().__init__() + + self.attn = MultiHeadAttention(n_state, n_head) + self.attn_ln = LayerNorm(n_state) + + self.cross_attn = MultiHeadAttention( + n_state, n_head) if cross_attention else None + self.cross_attn_ln = LayerNorm(n_state) if cross_attention else None + + n_mlp = n_state * 4 + self.mlp = nn.Sequential( + Linear( + n_state, n_mlp), nn.GELU(), Linear( + n_mlp, n_state)) + self.mlp_ln = LayerNorm(n_state) + + def forward( + self, + x: Tensor, + xa: Optional[Tensor] = None, + mask: Optional[Tensor] = None, + kv_cache: Optional[dict] = None, + ): + x = x + self.attn(self.attn_ln(x), mask=mask, kv_cache=kv_cache)[0] + if self.cross_attn: + x = x + self.cross_attn(self.cross_attn_ln(x), + xa, kv_cache=kv_cache)[0] + x = x + self.mlp(self.mlp_ln(x)) + return x + + +class AudioEncoder(nn.Module): + def __init__( + self, + n_mels: int, + n_ctx: int, + n_state: int, + n_head: int, + n_layer: int, + layer_st: int, + layer_ed: int): + super().__init__() + self.conv1 = Conv1d(n_mels, n_state, kernel_size=3, padding=1) + self.conv2 = Conv1d( + n_state, + n_state, + kernel_size=3, + stride=2, + padding=1) + self.register_buffer("positional_embedding", sinusoids(n_ctx, n_state)) + + self.blocks: Iterable[ResidualAttentionBlock] = nn.ModuleList( + [ResidualAttentionBlock(n_state, n_head) for _ in range(n_layer)] + ) + # self.ln_post = LayerNorm(n_state) + # ------------------------ADD:add new layer norm------------------------ + self.ln_post2 = LayerNorm(n_state * (layer_ed - layer_st + 1)) + + self.layer_st = layer_st + self.layer_ed = layer_ed + + def forward(self, x: Tensor): + """ + x : torch.Tensor, shape = (batch_size, n_mels, n_ctx) + the mel spectrogram of the audio + """ + # ---------------------------ADD------------------------ + x = x.permute(0, 2, 1) + + x = x.squeeze(1) + x = F.gelu(self.conv1(x)) + x = F.gelu(self.conv2(x)) + x = x.permute(0, 2, 1) + + # ------------Change:Tailor the positional_embedding---------- + assert x.shape[2:] == self.positional_embedding.shape[1:], \ + "incorrect audio shape" + if self.positional_embedding.shape[0] > x.shape[1]: + temp_positional_embedding = self.positional_embedding[:x.shape[1], :] + elif self.positional_embedding.shape[0] < x.shape[1]: + x = x[:, :self.positional_embedding.shape[0], :] + temp_positional_embedding = self.positional_embedding + else: + temp_positional_embedding = self.positional_embedding + + x = (x + temp_positional_embedding).to(x.dtype) + + # ----------Change: Concat block outputs------ + out = [] + for i, block in enumerate(self.blocks): + x = block(x) + if self.layer_st <= i <= self.layer_ed: + out.append(x) + + xs = torch.cat(out, dim=-1) + + xs = self.ln_post2(xs) + return xs + + +class whisper_encoder(torch.nn.Module): + def __init__(self, + frozen=False, + n_mels=80, + num_blocks=24, + output_size=1280, + n_head=20, + layer_st=16, + layer_ed=23, + model_path=None, + sample_rate=16000 + ): + super(whisper_encoder, self).__init__() + self.encoder = AudioEncoder( + n_mels=n_mels, + n_layer=num_blocks, + n_state=output_size, + n_ctx=1500, + n_head=n_head, + layer_st=layer_st, + layer_ed=layer_ed) + # 0 for freeze finetune, 1 for all parameters finetune + self.frozen = frozen + self.single_output_size = output_size + self.concat_layer = layer_ed - layer_st + 1 + self.n_mels = n_mels + + # load model + if model_path: + if dist.is_initialized(): + if dist.get_rank() == 0: + self._download_whisper_model(model_path) + dist.barrier() # Wait for rank 0 to finish downloading + self._load_pretrained_weights(model_path) + else: + self._download_whisper_model(model_path) + self._load_pretrained_weights(model_path) + + if self.frozen: + for param in self.encoder.parameters(): + param.requires_grad_(False) + + def _download_whisper_model(self, model_path='whisper_hub/large-v2.pt'): + download_dir = os.path.dirname(model_path) + if not os.path.exists(download_dir): + os.makedirs(download_dir) + if not os.path.isfile(model_path): + print("Downloading large-v2.pt ...") + url = 'https://openaipublic.azureedge.net/main/whisper/models/' \ + '81f7c96c852ee8fc832187b0132e569d6c3065a3252ed18e56effd0b6a73e524/' \ + 'large-v2.pt' + + urllib.request.urlretrieve(url, model_path) + + md5 = hashlib.md5(open(model_path, 'rb').read()).hexdigest() + + if md5 != "668764447eeda98eeba5ef7bfcb4cc3d": + print("Wrong md5sum of large-v2.pt") + os.remove(model_path) + raise ValueError("MD5 checksum does not match!") + else: + print("Model already downloaded.") + + def _load_pretrained_weights(self, model_path): + print(f"Loading pretrained weights from {model_path}...") + + state_dict = torch.load(model_path, map_location=torch.device('cpu')) + state_dict = state_dict['model_state_dict'] + + new_state_dict = {} + for k, v in state_dict.items(): + new_key = k.replace('encoder.', '', 1) + new_state_dict[new_key] = v + + missing_keys, unexpected_keys = self.encoder.load_state_dict( + new_state_dict, strict=False) + print("Pretrained weights loaded successfully.") + for key in missing_keys: + logging.warning('missing tensor: {}'.format(key)) + for key in unexpected_keys: + logging.warning('unexpected tensor: {}'.format(key)) + + def output_size(self): + return self.single_output_size * self.concat_layer + + def forward(self, wavs, wavs_len): + with torch.no_grad(): + processed_feats = [] + for i in range(wavs.size(0)): + tf_tensor = wavs[i].unsqueeze(0).to(wavs.device) + mat = whisper.log_mel_spectrogram( + tf_tensor.squeeze(), n_mels=self.n_mels) + processed_feats.append(mat) + + feat = torch.stack(processed_feats, dim=0).to(wavs.device) + + feat = feat.transpose(1, 2) + # (B,T,F) + x = self.encoder(feat) + return x, None diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/__init__.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/campplus.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/campplus.py new file mode 100644 index 0000000000000000000000000000000000000000..8effcadcbe226a079dc6cd006929ec67447bf67a --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/campplus.py @@ -0,0 +1,429 @@ +# Copyright (c) 2023 Hongji Wang (jijijiang77@gmail.com) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +''' +This implementation is adapted from github repo: +https://github.com/alibaba-damo-academy/3D-Speaker + +Some modifications: +1. Reuse the pooling layers in wespeaker +2. Remove the memory_efficient mechanism to meet the torch.jit.script + export requirements + +Reference: +[1] Hui Wang, Siqi Zheng, Yafeng Chen, Luyao Cheng and Qian Chen. + "CAM++: A Fast and Efficient Network for Speaker Verification + Using Context-Aware Masking". arXiv preprint arXiv:2303.00332 +''' + +from collections import OrderedDict + +import torch +import torch.nn as nn +import torch.nn.functional as F +import wespeaker.models.pooling_layers as pooling_layers + + +def get_nonlinear(config_str, channels): + nonlinear = nn.Sequential() + for name in config_str.split('-'): + if name == 'relu': + nonlinear.add_module('relu', nn.ReLU(inplace=True)) + elif name == 'prelu': + nonlinear.add_module('prelu', nn.PReLU(channels)) + elif name == 'batchnorm': + nonlinear.add_module('batchnorm', nn.BatchNorm1d(channels)) + elif name == 'batchnorm_': + nonlinear.add_module('batchnorm', + nn.BatchNorm1d(channels, affine=False)) + else: + raise ValueError('Unexpected module ({}).'.format(name)) + return nonlinear + + +class TDNNLayer(nn.Module): + + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + padding=0, + dilation=1, + bias=False, + config_str='batchnorm-relu'): + super(TDNNLayer, self).__init__() + if padding < 0: + assert kernel_size % 2 == 1, 'Expect equal paddings, \ + but got even kernel size ({})'.format(kernel_size) + padding = (kernel_size - 1) // 2 * dilation + self.linear = nn.Conv1d(in_channels, + out_channels, + kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + bias=bias) + self.nonlinear = get_nonlinear(config_str, out_channels) + + def forward(self, x): + x = self.linear(x) + x = self.nonlinear(x) + return x + + +class CAMLayer(nn.Module): + + def __init__(self, + bn_channels, + out_channels, + kernel_size, + stride, + padding, + dilation, + bias, + reduction=2): + super(CAMLayer, self).__init__() + self.linear_local = nn.Conv1d(bn_channels, + out_channels, + kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + bias=bias) + self.linear1 = nn.Conv1d(bn_channels, bn_channels // reduction, 1) + self.relu = nn.ReLU(inplace=True) + self.linear2 = nn.Conv1d(bn_channels // reduction, out_channels, 1) + self.sigmoid = nn.Sigmoid() + + def forward(self, x): + y = self.linear_local(x) + context = x.mean(-1, keepdim=True) + self.seg_pooling(x) + context = self.relu(self.linear1(context)) + m = self.sigmoid(self.linear2(context)) + return y * m + + def seg_pooling(self, x, seg_len: int = 100, stype: str = 'avg'): + if stype == 'avg': + seg = F.avg_pool1d(x, + kernel_size=seg_len, + stride=seg_len, + ceil_mode=True) + elif stype == 'max': + seg = F.max_pool1d(x, + kernel_size=seg_len, + stride=seg_len, + ceil_mode=True) + else: + raise ValueError('Wrong segment pooling type.') + shape = seg.shape + seg = seg.unsqueeze(-1).expand(shape[0], shape[1], shape[2], + seg_len).reshape( + shape[0], shape[1], -1) + seg = seg[..., :x.shape[-1]] + return seg + + +class CAMDenseTDNNLayer(nn.Module): + + def __init__(self, + in_channels, + out_channels, + bn_channels, + kernel_size, + stride=1, + dilation=1, + bias=False, + config_str='batchnorm-relu'): + super(CAMDenseTDNNLayer, self).__init__() + assert kernel_size % 2 == 1, 'Expect equal paddings, \ + but got even kernel size ({})'.format(kernel_size) + padding = (kernel_size - 1) // 2 * dilation + self.nonlinear1 = get_nonlinear(config_str, in_channels) + self.linear1 = nn.Conv1d(in_channels, bn_channels, 1, bias=False) + self.nonlinear2 = get_nonlinear(config_str, bn_channels) + self.cam_layer = CAMLayer(bn_channels, + out_channels, + kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + bias=bias) + + def bn_function(self, x): + return self.linear1(self.nonlinear1(x)) + + def forward(self, x): + x = self.bn_function(x) + x = self.cam_layer(self.nonlinear2(x)) + return x + + +class CAMDenseTDNNBlock(nn.ModuleList): + + def __init__(self, + num_layers, + in_channels, + out_channels, + bn_channels, + kernel_size, + stride=1, + dilation=1, + bias=False, + config_str='batchnorm-relu'): + super(CAMDenseTDNNBlock, self).__init__() + for i in range(num_layers): + layer = CAMDenseTDNNLayer(in_channels=in_channels + + i * out_channels, + out_channels=out_channels, + bn_channels=bn_channels, + kernel_size=kernel_size, + stride=stride, + dilation=dilation, + bias=bias, + config_str=config_str) + self.add_module('tdnnd%d' % (i + 1), layer) + + def forward(self, x): + for layer in self: + x = torch.cat([x, layer(x)], dim=1) + return x + + +class TransitLayer(nn.Module): + + def __init__(self, + in_channels, + out_channels, + bias=True, + config_str='batchnorm-relu'): + super(TransitLayer, self).__init__() + self.nonlinear = get_nonlinear(config_str, in_channels) + self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias) + + def forward(self, x): + x = self.nonlinear(x) + x = self.linear(x) + return x + + +class DenseLayer(nn.Module): + + def __init__(self, + in_channels, + out_channels, + bias=False, + config_str='batchnorm-relu'): + super(DenseLayer, self).__init__() + self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias) + self.nonlinear = get_nonlinear(config_str, out_channels) + + def forward(self, x): + if len(x.shape) == 2: + x = self.linear(x.unsqueeze(dim=-1)).squeeze(dim=-1) + else: + x = self.linear(x) + x = self.nonlinear(x) + return x + + +'''Note: The stride used here is different from that in Resnet +''' + + +class BasicResBlock(nn.Module): + expansion = 1 + + def __init__(self, in_planes, planes, stride=1): + super(BasicResBlock, self).__init__() + self.conv1 = nn.Conv2d(in_planes, + planes, + kernel_size=3, + stride=(stride, 1), + padding=1, + bias=False) + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d(planes, + planes, + kernel_size=3, + stride=1, + padding=1, + bias=False) + self.bn2 = nn.BatchNorm2d(planes) + + self.shortcut = nn.Sequential() + if stride != 1 or in_planes != self.expansion * planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, + self.expansion * planes, + kernel_size=1, + stride=(stride, 1), + bias=False), nn.BatchNorm2d(self.expansion * planes)) + + def forward(self, x): + out = F.relu(self.bn1(self.conv1(x))) + out = self.bn2(self.conv2(out)) + out += self.shortcut(x) + out = F.relu(out) + return out + + +class FCM(nn.Module): + + def __init__(self, block, num_blocks, m_channels=32, feat_dim=80): + super(FCM, self).__init__() + self.in_planes = m_channels + self.conv1 = nn.Conv2d(1, + m_channels, + kernel_size=3, + stride=1, + padding=1, + bias=False) + self.bn1 = nn.BatchNorm2d(m_channels) + + self.layer1 = self._make_layer(block, + m_channels, + num_blocks[0], + stride=2) + self.layer2 = self._make_layer(block, + m_channels, + num_blocks[1], + stride=2) + + self.conv2 = nn.Conv2d(m_channels, + m_channels, + kernel_size=3, + stride=(2, 1), + padding=1, + bias=False) + self.bn2 = nn.BatchNorm2d(m_channels) + self.out_channels = m_channels * (feat_dim // 8) + + def _make_layer(self, block, planes, num_blocks, stride): + strides = [stride] + [1] * (num_blocks - 1) + layers = [] + for stride in strides: + layers.append(block(self.in_planes, planes, stride)) + self.in_planes = planes * block.expansion + return nn.Sequential(*layers) + + def forward(self, x): + x = x.unsqueeze(1) + out = F.relu(self.bn1(self.conv1(x))) + out = self.layer1(out) + out = self.layer2(out) + out = F.relu(self.bn2(self.conv2(out))) + + shape = out.shape + out = out.reshape(shape[0], shape[1] * shape[2], shape[3]) + return out + + +class CAMPPlus(nn.Module): + + def __init__(self, + feat_dim=80, + embed_dim=512, + pooling_func='TSTP', + growth_rate=32, + bn_size=4, + init_channels=128, + config_str='batchnorm-relu'): + super(CAMPPlus, self).__init__() + + self.head = FCM(block=BasicResBlock, + num_blocks=[2, 2], + feat_dim=feat_dim) + channels = self.head.out_channels + + self.xvector = nn.Sequential( + OrderedDict([ + ('tdnn', + TDNNLayer(channels, + init_channels, + 5, + stride=2, + dilation=1, + padding=-1, + config_str=config_str)), + ])) + channels = init_channels + for i, (num_layers, kernel_size, + dilation) in enumerate(zip((12, 24, 16), (3, 3, 3), + (1, 2, 2))): + block = CAMDenseTDNNBlock(num_layers=num_layers, + in_channels=channels, + out_channels=growth_rate, + bn_channels=bn_size * growth_rate, + kernel_size=kernel_size, + dilation=dilation, + config_str=config_str) + self.xvector.add_module('block%d' % (i + 1), block) + channels = channels + num_layers * growth_rate + self.xvector.add_module( + 'transit%d' % (i + 1), + TransitLayer(channels, + channels // 2, + bias=False, + config_str=config_str)) + channels //= 2 + + self.xvector.add_module('out_nonlinear', + get_nonlinear(config_str, channels)) + + self.pool = getattr(pooling_layers, pooling_func)(in_dim=channels) + self.pool_out_dim = self.pool.get_out_dim() + self.xvector.add_module('stats', self.pool) + self.xvector.add_module( + 'dense', + DenseLayer(self.pool_out_dim, embed_dim, config_str='batchnorm_')) + + for m in self.modules(): + if isinstance(m, (nn.Conv1d, nn.Linear)): + nn.init.kaiming_normal_(m.weight.data) + if m.bias is not None: + nn.init.zeros_(m.bias) + + def get_frame_level_feat(self, x): + # for outer interface + x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T) + x = self.head(x) + for layer in self.xvector[:-2]: + x = layer(x) + + out = x.permute(0, 2, 1) + + return out # (B, T, D) + + def forward(self, x): + x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T) + x = self.head(x) + x = self.xvector(x) + return x + + +if __name__ == '__main__': + x = torch.zeros(1, 200, 80) + model = CAMPPlus(feat_dim=80, embed_dim=512, pooling_func='TSTP') + model.eval() + out = model(x) + print(out.shape) + + num_params = sum(param.numel() for param in model.parameters()) + print("{} M".format(num_params / 1e6)) + + # from thop import profile + # x_np = torch.randn(1, 200, 80) + # flops, params = profile(model, inputs=(x_np, )) + # print("FLOPs: {} G, Params: {} M".format(flops / 1e9, params / 1e6)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/convert_repvgg.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/convert_repvgg.py new file mode 100644 index 0000000000000000000000000000000000000000..1ee0c7e38cac7f3c0f17fdf384e1d596e7233d75 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/convert_repvgg.py @@ -0,0 +1,53 @@ +# Copyright (c) 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import torch +import fire +import yaml + +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.models.repvgg import repvgg_model_convert +from wespeaker.utils.utils import parse_config_or_kwargs + + +def convert(config='conf/config.yaml', **kwargs): + configs = parse_config_or_kwargs(config, **kwargs) + speaker_model = get_speaker_model( + configs['model'])(**configs['model_args']) + configs['model_args']['deploy'] = True + # save new configs for testing and deploying + # NOTE: 'deploy': true + saved_config_path = os.path.join(configs['exp_dir'], 'config.yaml') + with open(saved_config_path, 'w') as fout: + data = yaml.dump(configs) + fout.write(data) + + if os.path.isfile(configs['load']): + print("==> Loading checkpoint '{}'".format(configs['load'])) + checkpoint = torch.load(configs['load']) + if 'state_dict' in checkpoint: + checkpoint = checkpoint['state_dict'] + elif 'model' in checkpoint: + checkpoint = checkpoint['model'] + ckpt = {k.replace('module.', ''): v for k, v in checkpoint.items()} + speaker_model.load_state_dict(ckpt, strict=False) + else: + print('no checkpoint') + repvgg_model_convert(speaker_model, save_path=configs['save']) + print("==> Saving convert model to '{}'".format(configs['save'])) + + +if __name__ == '__main__': + fire.Fire(convert) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/ecapa_tdnn.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/ecapa_tdnn.py new file mode 100644 index 0000000000000000000000000000000000000000..9037e51899c737b34e4643cc45424816d0102978 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/ecapa_tdnn.py @@ -0,0 +1,292 @@ +# Copyright (c) 2021 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# 2023 Bing Han (hanbing97@sjtu.edu.cn) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +''' This implementation is adapted from github repo: + https://github.com/lawlict/ECAPA-TDNN. +''' + +import torch +import torch.nn as nn +import torch.nn.functional as F +import wespeaker.models.pooling_layers as pooling_layers +''' Res2Conv1d + BatchNorm1d + ReLU +''' + + +class Res2Conv1dReluBn(nn.Module): + """ + in_channels == out_channels == channels + """ + + def __init__(self, + channels, + kernel_size=1, + stride=1, + padding=0, + dilation=1, + bias=True, + scale=4): + super().__init__() + assert channels % scale == 0, "{} % {} != 0".format(channels, scale) + self.scale = scale + self.width = channels // scale + self.nums = scale if scale == 1 else scale - 1 + + self.convs = [] + self.bns = [] + for i in range(self.nums): + self.convs.append( + nn.Conv1d(self.width, + self.width, + kernel_size, + stride, + padding, + dilation, + bias=bias)) + self.bns.append(nn.BatchNorm1d(self.width)) + self.convs = nn.ModuleList(self.convs) + self.bns = nn.ModuleList(self.bns) + + def forward(self, x): + out = [] + spx = torch.split(x, self.width, 1) + sp = spx[0] + for i, (conv, bn) in enumerate(zip(self.convs, self.bns)): + # Order: conv -> relu -> bn + if i >= 1: + sp = sp + spx[i] + sp = conv(sp) + sp = bn(F.relu(sp)) + out.append(sp) + if self.scale != 1: + out.append(spx[self.nums]) + out = torch.cat(out, dim=1) + + return out + + +''' Conv1d + BatchNorm1d + ReLU +''' + + +class Conv1dReluBn(nn.Module): + + def __init__(self, + in_channels, + out_channels, + kernel_size=1, + stride=1, + padding=0, + dilation=1, + bias=True): + super().__init__() + self.conv = nn.Conv1d(in_channels, + out_channels, + kernel_size, + stride, + padding, + dilation, + bias=bias) + self.bn = nn.BatchNorm1d(out_channels) + + def forward(self, x): + return self.bn(F.relu(self.conv(x))) + + +''' The SE connection of 1D case. +''' + + +class SE_Connect(nn.Module): + + def __init__(self, channels, se_bottleneck_dim=128): + super().__init__() + self.linear1 = nn.Linear(channels, se_bottleneck_dim) + self.linear2 = nn.Linear(se_bottleneck_dim, channels) + + def forward(self, x): + out = x.mean(dim=2) + out = F.relu(self.linear1(out)) + out = torch.sigmoid(self.linear2(out)) + out = x * out.unsqueeze(2) + + return out + + +''' SE-Res2Block of the ECAPA-TDNN architecture. +''' + + +class SE_Res2Block(nn.Module): + + def __init__(self, channels, kernel_size, stride, padding, dilation, + scale): + super().__init__() + self.se_res2block = nn.Sequential( + Conv1dReluBn(channels, + channels, + kernel_size=1, + stride=1, + padding=0), + Res2Conv1dReluBn(channels, + kernel_size, + stride, + padding, + dilation, + scale=scale), + Conv1dReluBn(channels, + channels, + kernel_size=1, + stride=1, + padding=0), SE_Connect(channels)) + + def forward(self, x): + return x + self.se_res2block(x) + + +class ECAPA_TDNN(nn.Module): + + def __init__(self, + channels=512, + feat_dim=80, + embed_dim=192, + pooling_func='ASTP', + global_context_att=False, + emb_bn=False): + super().__init__() + + self.layer1 = Conv1dReluBn(feat_dim, + channels, + kernel_size=5, + padding=2) + self.layer2 = SE_Res2Block(channels, + kernel_size=3, + stride=1, + padding=2, + dilation=2, + scale=8) + self.layer3 = SE_Res2Block(channels, + kernel_size=3, + stride=1, + padding=3, + dilation=3, + scale=8) + self.layer4 = SE_Res2Block(channels, + kernel_size=3, + stride=1, + padding=4, + dilation=4, + scale=8) + + cat_channels = channels * 3 + out_channels = 512 * 3 + self.conv = nn.Conv1d(cat_channels, out_channels, kernel_size=1) + self.pool = getattr(pooling_layers, pooling_func)( + in_dim=out_channels, global_context_att=global_context_att) + self.pool_out_dim = self.pool.get_out_dim() + self.bn = nn.BatchNorm1d(self.pool_out_dim) + self.linear = nn.Linear(self.pool_out_dim, embed_dim) + self.emb_bn = emb_bn + if emb_bn: # better in SSL for SV + self.bn2 = nn.BatchNorm1d(embed_dim) + else: + self.bn2 = nn.Identity() + + def _get_frame_level_feat(self, x): + # for inner class usage + x = x.permute(0, 2, 1) # (B,T,F) -> (B,F,T) + + out1 = self.layer1(x) + out2 = self.layer2(out1) + out3 = self.layer3(out2) + out4 = self.layer4(out3) + + out = torch.cat([out2, out3, out4], dim=1) + out = self.conv(out) + + return out, out4 + + def get_frame_level_feat(self, x): + # for outer interface + out = self._get_frame_level_feat(x)[0].permute(0, 2, 1) + return out # (B, T, D) + + def forward(self, x): + out, out4 = self._get_frame_level_feat(x) + out = F.relu(out) + out = self.bn(self.pool(out)) + out = self.linear(out) + if self.emb_bn: + out = self.bn2(out) + return out4, out + + +def ECAPA_TDNN_c1024(feat_dim, embed_dim, pooling_func='ASTP', emb_bn=False): + return ECAPA_TDNN(channels=1024, + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + emb_bn=emb_bn) + + +def ECAPA_TDNN_GLOB_c1024(feat_dim, + embed_dim, + pooling_func='ASTP', + emb_bn=False): + return ECAPA_TDNN(channels=1024, + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + global_context_att=True, + emb_bn=emb_bn) + + +def ECAPA_TDNN_c512(feat_dim, embed_dim, pooling_func='ASTP', emb_bn=False): + return ECAPA_TDNN(channels=512, + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + emb_bn=emb_bn) + + +def ECAPA_TDNN_GLOB_c512(feat_dim, + embed_dim, + pooling_func='ASTP', + emb_bn=False): + return ECAPA_TDNN(channels=512, + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + global_context_att=True, + emb_bn=emb_bn) + + +if __name__ == '__main__': + x = torch.zeros(1, 200, 80) + model = ECAPA_TDNN_GLOB_c512(feat_dim=80, + embed_dim=256, + pooling_func='ASTP') + model.eval() + out = model(x) + print(out[-1].shape) + + num_params = sum(param.numel() for param in model.parameters()) + print("{} M".format(num_params / 1e6)) + + # from thop import profile + # x_np = torch.randn(1, 200, 80) + # flops, params = profile(model, inputs=(x_np, )) + # print("FLOPs: {} G, Params: {} M".format(flops / 1e9, params / 1e6)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/eres2net.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/eres2net.py new file mode 100644 index 0000000000000000000000000000000000000000..ea02a0c6bdfb2a9031e9cf3c7987dddb73ecc124 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/eres2net.py @@ -0,0 +1,446 @@ +# Copyright (c) 2024 Hongji Wang (jijijiang77@gmail.com) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +''' +This implementation is adapted from github repo: +https://github.com/alibaba-damo-academy/3D-Speaker + +Some modifications: +1. Reuse the pooling layers (small update) in wespeaker +2. Refine BasicBlockERes2Net and BasicBlockERes2Net_diff_AFF to meet + the torch.jit.script export requirements + +ERes2Net incorporates both local and global feature fusion techniques +to improve the performance. The local feature fusion (LFF) fuses the +features within one single residual block to extract the local signal. +The global feature fusion (GFF) takes acoustic features of different +scales as input to aggregate global signal. Parameters expansion, +baseWidth, and scale can be modified to obtain optimal performance. + +Reference: +[1] Yafeng Chen, Siqi Zheng, Hui Wang, Luyao Cheng, Qian Chen, Jiajun Qi. + "An Enhanced Res2Net with Local and Global Feature Fusion for Speaker + Verification". arXiv preprint arXiv:2305.12838 (2023). +''' + +import torch +import math +import torch.nn as nn +import torch.nn.functional as F +import wespeaker.models.pooling_layers as pooling_layers + + +class ReLU(nn.Hardtanh): + + def __init__(self, inplace=False): + super(ReLU, self).__init__(0.0, 20.0, inplace) + + def __repr__(self): + inplace_str = 'inplace' if self.inplace else '' + return self.__class__.__name__ + ' (' \ + + inplace_str + ')' + + +def conv1x1(in_planes, out_planes, stride=1): + "1x1 convolution without padding" + return nn.Conv2d(in_planes, + out_planes, + kernel_size=1, + stride=stride, + padding=0, + bias=False) + + +def conv3x3(in_planes, out_planes, stride=1): + "3x3 convolution with padding" + return nn.Conv2d(in_planes, + out_planes, + kernel_size=3, + stride=stride, + padding=1, + bias=False) + + +class AFF(nn.Module): + + def __init__(self, channels=64, r=4): + super(AFF, self).__init__() + inter_channels = int(channels // r) + + self.local_att = nn.Sequential( + nn.Conv2d(channels * 2, + inter_channels, + kernel_size=1, + stride=1, + padding=0), + nn.BatchNorm2d(inter_channels), + nn.SiLU(inplace=True), + nn.Conv2d(inter_channels, + channels, + kernel_size=1, + stride=1, + padding=0), + nn.BatchNorm2d(channels), + ) + + def forward(self, x, ds_y): + xa = torch.cat((x, ds_y), dim=1) + x_att = self.local_att(xa) + x_att = 1.0 + torch.tanh(x_att) + xo = torch.mul(x, x_att) + torch.mul(ds_y, 2.0 - x_att) + + return xo + + +class BasicBlockERes2Net(nn.Module): + + def __init__(self, + in_planes, + planes, + stride=1, + baseWidth=32, + scale=2, + expansion=2): + super(BasicBlockERes2Net, self).__init__() + width = int(math.floor(planes * (baseWidth / 64.0))) + self.conv1 = conv1x1(in_planes, width * scale, stride) + self.bn1 = nn.BatchNorm2d(width * scale) + self.nums = scale + self.expansion = expansion + + convs = [] + bns = [] + for i in range(self.nums): + convs.append(conv3x3(width, width)) + bns.append(nn.BatchNorm2d(width)) + self.convs = nn.ModuleList(convs) + self.bns = nn.ModuleList(bns) + self.relu = ReLU(inplace=True) + + self.conv3 = conv1x1(width * scale, planes * self.expansion) + self.bn3 = nn.BatchNorm2d(planes * self.expansion) + self.shortcut = nn.Sequential() + if stride != 1 or in_planes != self.expansion * planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, + self.expansion * planes, + kernel_size=1, + stride=stride, + bias=False), nn.BatchNorm2d(self.expansion * planes)) + self.stride = stride + self.width = width + self.scale = scale + + def forward(self, x): + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + spx = torch.split(out, self.width, 1) + sp = spx[0] + for i, (conv, bn) in enumerate(zip(self.convs, self.bns)): + if i >= 1: + sp = sp + spx[i] + sp = conv(sp) + sp = self.relu(bn(sp)) + if i == 0: + out = sp + else: + out = torch.cat((out, sp), 1) + + out = self.conv3(out) + out = self.bn3(out) + + residual = self.shortcut(x) + out += residual + out = self.relu(out) + + return out + + +class BasicBlockERes2Net_diff_AFF(nn.Module): + + def __init__(self, + in_planes, + planes, + stride=1, + baseWidth=32, + scale=2, + expansion=2): + super(BasicBlockERes2Net_diff_AFF, self).__init__() + width = int(math.floor(planes * (baseWidth / 64.0))) + self.conv1 = conv1x1(in_planes, width * scale, stride) + self.bn1 = nn.BatchNorm2d(width * scale) + self.nums = scale + self.expansion = expansion + + # to meet the torch.jit.script export requirements + self.conv2_1 = conv3x3(width, width) + self.bn2_1 = nn.BatchNorm2d(width) + convs = [] + fuse_models = [] + bns = [] + for i in range(self.nums - 1): + convs.append(conv3x3(width, width)) + bns.append(nn.BatchNorm2d(width)) + fuse_models.append(AFF(channels=width)) + self.convs = nn.ModuleList(convs) + self.bns = nn.ModuleList(bns) + self.fuse_models = nn.ModuleList(fuse_models) + self.relu = ReLU(inplace=True) + + self.conv3 = conv1x1(width * scale, planes * self.expansion) + self.bn3 = nn.BatchNorm2d(planes * self.expansion) + self.shortcut = nn.Sequential() + if stride != 1 or in_planes != self.expansion * planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, + self.expansion * planes, + kernel_size=1, + stride=stride, + bias=False), nn.BatchNorm2d(self.expansion * planes)) + self.stride = stride + self.width = width + self.scale = scale + + def forward(self, x): + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + spx = torch.split(out, self.width, 1) + sp = spx[0] + sp = self.conv2_1(sp) + sp = self.relu(self.bn2_1(sp)) + out = sp + for i, (conv, bn, fuse_model) in enumerate( + zip(self.convs, self.bns, self.fuse_models), 1): + sp = fuse_model(sp, spx[i]) + sp = conv(sp) + sp = self.relu(bn(sp)) + out = torch.cat((out, sp), 1) + + out = self.conv3(out) + out = self.bn3(out) + + residual = self.shortcut(x) + out += residual + out = self.relu(out) + + return out + + +class ERes2Net(nn.Module): + + def __init__(self, + m_channels, + num_blocks, + baseWidth=32, + scale=2, + expansion=2, + block=BasicBlockERes2Net, + block_fuse=BasicBlockERes2Net_diff_AFF, + feat_dim=80, + embed_dim=192, + pooling_func='TSTP', + two_emb_layer=False): + super(ERes2Net, self).__init__() + self.in_planes = m_channels + self.feat_dim = feat_dim + self.embed_dim = embed_dim + self.stats_dim = int(feat_dim / 8) * m_channels * 8 + self.two_emb_layer = two_emb_layer + self.expansion = expansion + + self.conv1 = nn.Conv2d(1, + m_channels, + kernel_size=3, + stride=1, + padding=1, + bias=False) + self.bn1 = nn.BatchNorm2d(m_channels) + self.layer1 = self._make_layer(block, + m_channels, + num_blocks[0], + stride=1, + baseWidth=baseWidth, + scale=scale, + expansion=expansion) + self.layer2 = self._make_layer(block, + m_channels * 2, + num_blocks[1], + stride=2, + baseWidth=baseWidth, + scale=scale, + expansion=expansion) + self.layer3 = self._make_layer(block_fuse, + m_channels * 4, + num_blocks[2], + stride=2, + baseWidth=baseWidth, + scale=scale, + expansion=expansion) + self.layer4 = self._make_layer(block_fuse, + m_channels * 8, + num_blocks[3], + stride=2, + baseWidth=baseWidth, + scale=scale, + expansion=expansion) + + # Downsampling module for each layer + self.layer1_downsample = nn.Conv2d(m_channels * expansion, + m_channels * expansion * 2, + kernel_size=3, + stride=2, + padding=1, + bias=False) + self.layer2_downsample = nn.Conv2d(m_channels * expansion * 2, + m_channels * expansion * 4, + kernel_size=3, + padding=1, + stride=2, + bias=False) + self.layer3_downsample = nn.Conv2d(m_channels * expansion * 4, + m_channels * expansion * 8, + kernel_size=3, + padding=1, + stride=2, + bias=False) + + # Bottom-up fusion module + self.fuse_mode12 = AFF(channels=m_channels * expansion * 2) + self.fuse_mode123 = AFF(channels=m_channels * expansion * 4) + self.fuse_mode1234 = AFF(channels=m_channels * expansion * 8) + + self.pool = getattr(pooling_layers, + pooling_func)(in_dim=self.stats_dim * expansion) + self.pool_out_dim = self.pool.get_out_dim() + self.seg_1 = nn.Linear(self.pool_out_dim, embed_dim) + if self.two_emb_layer: + self.seg_bn_1 = nn.BatchNorm1d(embed_dim, affine=False) + self.seg_2 = nn.Linear(embed_dim, embed_dim) + else: + self.seg_bn_1 = nn.Identity() + self.seg_2 = nn.Identity() + + def _make_layer(self, + block, + planes, + num_blocks, + stride, + baseWidth=32, + scale=2, + expansion=2): + strides = [stride] + [1] * (num_blocks - 1) + layers = [] + for stride in strides: + layers.append( + block(self.in_planes, planes, stride, baseWidth, scale, + expansion)) + self.in_planes = planes * self.expansion + return nn.Sequential(*layers) + + def _get_frame_level_feat(self, x): + # for inner class usage + x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T) + x = x.unsqueeze_(1) + out = F.relu(self.bn1(self.conv1(x))) + out1 = self.layer1(out) + out2 = self.layer2(out1) + out1_downsample = self.layer1_downsample(out1) + fuse_out12 = self.fuse_mode12(out2, out1_downsample) + out3 = self.layer3(out2) + fuse_out12_downsample = self.layer2_downsample(fuse_out12) + fuse_out123 = self.fuse_mode123(out3, fuse_out12_downsample) + out4 = self.layer4(out3) + fuse_out123_downsample = self.layer3_downsample(fuse_out123) + fuse_out1234 = self.fuse_mode1234(out4, fuse_out123_downsample) + + return fuse_out1234 + + def get_frame_level_feat(self, x): + # for outer interface + out = self._get_frame_level_feat(x) + out = out.transpose(1, 3) + out = torch.flatten(out, 2, -1) + + return out # (B, T, D) + + def forward(self, x): + fuse_out1234 = self._get_frame_level_feat(x) + stats = self.pool(fuse_out1234) + + embed_a = self.seg_1(stats) + if self.two_emb_layer: + out = F.relu(embed_a) + out = self.seg_bn_1(out) + embed_b = self.seg_2(out) + return embed_b + else: + return embed_a + + +def ERes2Net34_Base(feat_dim, + embed_dim, + pooling_func='TSTP', + two_emb_layer=False): + return ERes2Net(32, [3, 4, 6, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def ERes2Net34_Large(feat_dim, + embed_dim, + pooling_func='TSTP', + two_emb_layer=False): + return ERes2Net(64, [3, 4, 6, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def ERes2Net34_aug(feat_dim, + embed_dim, + pooling_func='TSTP', + two_emb_layer=False, + expansion=4, + baseWidth=24, + scale=3): + return ERes2Net(64, [3, 4, 6, 3], + expansion=expansion, + baseWidth=baseWidth, + scale=scale, + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +if __name__ == '__main__': + x = torch.zeros(1, 200, 80) + model = ERes2Net34_Base(feat_dim=80, embed_dim=512, two_emb_layer=False) + model.eval() + out = model(x) + print(out.size()) + + num_params = sum(p.numel() for p in model.parameters()) + print("{} M".format(num_params / 1e6)) + + # from thop import profile + # x_np = torch.randn(1, 200, 80) + # flops, params = profile(model, inputs=(x_np, )) + # print("FLOPs: {} G, Params: {} M".format(flops / 1e9, params / 1e6)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/gemini_dfresnet.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/gemini_dfresnet.py new file mode 100644 index 0000000000000000000000000000000000000000..52f366305b5caf71ac6a412b805102bb76ee5ae9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/gemini_dfresnet.py @@ -0,0 +1,189 @@ +# Copyright (c) 2024 Shuai Wang (wsstriving@gmail.com) +# 2024 Tianchi Liu (tianchi_liu@u.nus.edu) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''The implementation of Gemini-DF-ResNet. + +Reference: +[1] Liu, Tianchi, et al. "Golden Gemini is All You Need: Finding the + Sweet Spots for Speaker Verification." arXiv:2312.03620 (2023). +[2] Liu, Bei, et al. "DF-ResNet: Boosting Speaker Verification Performance + with Depth-First Design." INTERSPEECH. 2022. +''' +import torch +import torch.nn as nn +import torch.nn.functional as F +import wespeaker.models.pooling_layers as pooling_layers + + +class Inverted_Bottleneck(nn.Module): + def __init__(self, dim): + super(Inverted_Bottleneck, self).__init__() + self.conv1 = nn.Conv2d(dim, 4 * dim, kernel_size=1, bias=False) + self.bn1 = nn.BatchNorm2d(4 * dim) + self.conv2 = nn.Conv2d(4 * dim, 4 * dim, + kernel_size=3, padding=1, groups=4 * dim, + bias=False) + self.bn2 = nn.BatchNorm2d(4 * dim) + self.conv3 = nn.Conv2d(4 * dim, dim, kernel_size=1, bias=False) + self.bn3 = nn.BatchNorm2d(dim) + + def forward(self, x): + out = F.relu(self.bn1(self.conv1(x))) + out = F.relu(self.bn2(self.conv2(out))) + out = self.bn3(self.conv3(out)) + out += x + out = F.relu(out) + return out + + +class Gemini_DF_ResNet(nn.Module): + # DF_ResNet with T14c stride strategy of Golden Gemini + def __init__(self, + depths, + dims, + feat_dim=40, + embed_dim=128, + pooling_func='TSTP', + two_emb_layer=False): + super(Gemini_DF_ResNet, self).__init__() + self.feat_dim = feat_dim + self.embed_dim = embed_dim + self.stats_dim = int(feat_dim / 8 / 2) * dims[-1] + self.two_emb_layer = two_emb_layer + + self.downsample_layers = nn.ModuleList() + stem = nn.Sequential( + nn.Conv2d(1, dims[0], kernel_size=3, stride=1, padding=1, bias=False), + nn.BatchNorm2d(dims[0]), + nn.ReLU() + ) + self.downsample_layers.append(stem) + + stride_f = [2, 2, 2, 2] + stride_t = [1, 2, 1, 1] + + for i in range(4): + downsample_layer = nn.Sequential( + nn.Conv2d( + dims[i], dims[i + 1], kernel_size=3, + stride=(stride_f[i], stride_t[i]), + padding=1, bias=False), + nn.BatchNorm2d(dims[i + 1]) + ) + self.downsample_layers.append(downsample_layer) + + self.stages = nn.ModuleList() + for i in range(4): + stage = nn.Sequential( + *[Inverted_Bottleneck(dim=dims[i + 1]) for _ in range(depths[i])] + ) + self.stages.append(stage) + + self.pool = getattr(pooling_layers, + pooling_func)(in_dim=self.stats_dim) + self.pool_out_dim = self.pool.get_out_dim() + self.seg_1 = nn.Linear(self.pool_out_dim, embed_dim) + if self.two_emb_layer: + self.seg_bn_1 = nn.BatchNorm1d(embed_dim, affine=False) + self.seg_2 = nn.Linear(embed_dim, embed_dim) + else: + self.seg_bn_1 = nn.Identity() + self.seg_2 = nn.Identity() + + def _get_frame_level_feat(self, x): + # for inner class usage + x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T) + x = x.unsqueeze_(1) + out = self.downsample_layers[0](x) + out = self.downsample_layers[1](out) + out = self.stages[0](out) + out = self.downsample_layers[2](out) + out = self.stages[1](out) + out = self.downsample_layers[3](out) + out = self.stages[2](out) + out = self.downsample_layers[4](out) + out = self.stages[3](out) + + return out + + def get_frame_level_feat(self, x): + # for outer interface + out = self._get_frame_level_feat(x) + out = out.transpose(1, 3) + out = torch.flatten(out, 2, -1) + + return out # (B, T, D) + + def forward(self, x): + + out = self._get_frame_level_feat(x) + stats = self.pool(out) + + embed_a = self.seg_1(stats) + if self.two_emb_layer: + out = F.relu(embed_a) + out = self.seg_bn_1(out) + embed_b = self.seg_2(out) + return embed_a, embed_b + else: + return torch.tensor(0.0), embed_a + + +# following models do include separate downsmapling layers into layer counting +def Gemini_DF_ResNet60(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return Gemini_DF_ResNet(depths=[3, 3, 9, 3], + dims=[32, 32, 64, 128, 256], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def Gemini_DF_ResNet114(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return Gemini_DF_ResNet(depths=[3, 3, 27, 3], + dims=[32, 32, 64, 128, 256], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def Gemini_DF_ResNet183(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return Gemini_DF_ResNet(depths=[3, 8, 45, 3], + dims=[32, 32, 64, 128, 256], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def Gemini_DF_ResNet237(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return Gemini_DF_ResNet(depths=[3, 8, 63, 3], + dims=[32, 32, 64, 128, 256], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +if __name__ == '__main__': + x = torch.zeros(1, 200, 80) + model = Gemini_DF_ResNet114(80, 256, 'TSTP') + model.eval() + out = model(x) + print(out[-1].size()) + + num_params = sum(p.numel() for p in model.parameters()) + print("{} M".format(num_params / 1e6)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/pooling_layers.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/pooling_layers.py new file mode 100644 index 0000000000000000000000000000000000000000..5af413981f4f3c49c6e81be1ae6c3fd1d8c2c8a9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/pooling_layers.py @@ -0,0 +1,428 @@ +# Copyright (c) 2021 Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Pooling functions to aggregate frame-level deep features +into segment-level speaker embeddings + +High-order statistics are surprisingly effective, TSDP acts similarly as TSTP, +even though we remove the mean statistic, on Voxceleb. +""" + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class TAP(nn.Module): + """ + Temporal average pooling, only first-order mean is considered + """ + + def __init__(self, in_dim=0, **kwargs): + super(TAP, self).__init__() + self.in_dim = in_dim + + def forward(self, x): + pooling_mean = x.mean(dim=-1) + # To be compatable with 2D input + pooling_mean = pooling_mean.flatten(start_dim=1) + return pooling_mean + + def get_out_dim(self): + self.out_dim = self.in_dim + return self.out_dim + + +class TSDP(nn.Module): + """ + Temporal standard deviation pooling, only second-order std is considered + """ + + def __init__(self, in_dim=0, **kwargs): + super(TSDP, self).__init__() + self.in_dim = in_dim + + def forward(self, x): + # The last dimension is the temporal axis + pooling_std = torch.sqrt(torch.var(x, dim=-1) + 1e-7) + pooling_std = pooling_std.flatten(start_dim=1) + return pooling_std + + def get_out_dim(self): + self.out_dim = self.in_dim + return self.out_dim + + +class TSTP(nn.Module): + """ + Temporal statistics pooling, concatenate mean and std, which is used in + x-vector + Comment: simple concatenation can not make full use of both statistics + """ + + def __init__(self, in_dim=0, **kwargs): + super(TSTP, self).__init__() + self.in_dim = in_dim + + def forward(self, x): + # The last dimension is the temporal axis + pooling_mean = x.mean(dim=-1) + pooling_std = torch.sqrt(torch.var(x, dim=-1) + 1e-7) + pooling_mean = pooling_mean.flatten(start_dim=1) + pooling_std = pooling_std.flatten(start_dim=1) + stats = torch.cat((pooling_mean, pooling_std), 1) + return stats + + def get_out_dim(self): + self.out_dim = self.in_dim * 2 + return self.out_dim + + +class ASTP(nn.Module): + """ Attentive statistics pooling: Channel- and context-dependent + statistics pooling, first used in ECAPA_TDNN. + """ + + def __init__(self, + in_dim, + bottleneck_dim=128, + global_context_att=False, + **kwargs): + super(ASTP, self).__init__() + self.in_dim = in_dim + self.global_context_att = global_context_att + + # Use Conv1d with stride == 1 rather than Linear, then we don't + # need to transpose inputs. + if global_context_att: + self.linear1 = nn.Conv1d( + in_dim * 3, bottleneck_dim, + kernel_size=1) # equals W and b in the paper + else: + self.linear1 = nn.Conv1d( + in_dim, bottleneck_dim, + kernel_size=1) # equals W and b in the paper + self.linear2 = nn.Conv1d(bottleneck_dim, in_dim, + kernel_size=1) # equals V and k in the paper + + def forward(self, x): + """ + x: a 3-dimensional tensor in tdnn-based architecture (B,F,T) + or a 4-dimensional tensor in resnet architecture (B,C,F,T) + 0-dim: batch-dimension, last-dim: time-dimension (frame-dimension) + """ + if len(x.shape) == 4: + x = x.reshape(x.shape[0], x.shape[1] * x.shape[2], x.shape[3]) + assert len(x.shape) == 3 + + if self.global_context_att: + context_mean = torch.mean(x, dim=-1, keepdim=True).expand_as(x) + context_std = torch.sqrt( + torch.var(x, dim=-1, keepdim=True) + 1e-7).expand_as(x) + x_in = torch.cat((x, context_mean, context_std), dim=1) + else: + x_in = x + + # DON'T use ReLU here! ReLU may be hard to converge. + alpha = torch.tanh( + self.linear1(x_in)) # alpha = F.relu(self.linear1(x_in)) + alpha = torch.softmax(self.linear2(alpha), dim=2) + mean = torch.sum(alpha * x, dim=2) + var = torch.sum(alpha * (x**2), dim=2) - mean**2 + std = torch.sqrt(var.clamp(min=1e-7)) + return torch.cat([mean, std], dim=1) + + def get_out_dim(self): + self.out_dim = 2 * self.in_dim + return self.out_dim + + +class ASP(nn.Module): + """Attentive Statistics Pooling (compatible with WeSpeaker and + W2V-BERT/WavLM).""" + + def __init__( + self, + in_planes=None, + acoustic_dim=None, + input_dim=None, + hidden_dim=None, + ): + super().__init__() + + if input_dim is not None: + # W2V / Transformer-style input + self.feature_dim = input_dim + elif in_planes is not None and acoustic_dim is not None: + # WeSpeaker-style input + outmap_size = int(acoustic_dim / 8) + self.feature_dim = in_planes * 8 * outmap_size + else: + raise ValueError( + "Specify either (in_planes, acoustic_dim) or " + "(input_dim, hidden_dim)." + ) + + self.out_dim = self.feature_dim * 2 + hidden_dim = hidden_dim or 128 + + self.attention = nn.Sequential( + nn.Conv1d(self.feature_dim, hidden_dim, kernel_size=1), + nn.ReLU(inplace=True), + nn.BatchNorm1d(hidden_dim), + nn.Conv1d(hidden_dim, self.feature_dim, kernel_size=1), + nn.Softmax(dim=2), + ) + + def forward(self, x: torch.Tensor): + # W2V: [B, T, D] + # WeSpeaker: [B, C, F, T] + if x.dim() == 4: + x = x.reshape(x.size(0), -1, x.size(-1)) + elif x.dim() == 3 and x.shape[1] != self.feature_dim: + x = x.transpose(1, 2) + + w = self.attention(x) + mu = torch.sum(x * w, dim=2) + sg = torch.sqrt( + ( + torch.sum((x**2) * w, dim=2) + - mu**2 + ).clamp(min=1e-5) + ) + return torch.cat([mu, sg], dim=1) + + +class MHASTP(torch.nn.Module): + """ Multi head attentive statistics pooling + Reference: + Self Multi-Head Attention for Speaker Recognition + https://arxiv.org/pdf/1906.09890.pdf + """ + + def __init__(self, + in_dim, + layer_num=2, + head_num=2, + d_s=1, + bottleneck_dim=64, + **kwargs): + super(MHASTP, self).__init__() + assert (in_dim % head_num + ) == 0 # make sure that head num can be divided by input_dim + self.in_dim = in_dim + self.head_num = head_num + d_model = int(in_dim / head_num) + channel_dims = [bottleneck_dim for i in range(layer_num + 1)] + if d_s > 1: + d_s = d_model + else: + d_s = 1 + self.d_s = d_s + channel_dims[0], channel_dims[-1] = d_model, d_s + heads_att_trans = [] + for i in range(self.head_num): + att_trans = nn.Sequential() + for i in range(layer_num - 1): + att_trans.add_module( + 'att_' + str(i), + nn.Conv1d(channel_dims[i], channel_dims[i + 1], 1, 1)) + att_trans.add_module('tanh' + str(i), nn.Tanh()) + att_trans.add_module( + 'att_' + str(layer_num - 1), + nn.Conv1d(channel_dims[layer_num - 1], channel_dims[layer_num], + 1, 1)) + heads_att_trans.append(att_trans) + self.heads_att_trans = nn.ModuleList(heads_att_trans) + + def forward(self, input): + """ + input: a 3-dimensional tensor in xvector architecture + or a 4-dimensional tensor in resnet architecture + 0-dim: batch-dimension, last-dim: time-dimension (frame-dimension) + """ + if len(input.shape) == 4: # B x F x T + input = input.reshape(input.shape[0], + input.shape[1] * input.shape[2], + input.shape[3]) + assert len(input.shape) == 3 + bs, f_dim, t_dim = input.shape + chunks = torch.chunk(input, self.head_num, 1) + # split + chunks_out = [] + # for i in range(self.head_num): + # att_score = self.heads_att_trans[i](chunks[i]) + for i, layer in enumerate(self.heads_att_trans): + att_score = layer(chunks[i]) + alpha = F.softmax(att_score, dim=-1) + mean = torch.sum(alpha * chunks[i], dim=2) + var = torch.sum(alpha * chunks[i]**2, dim=2) - mean**2 + std = torch.sqrt(var.clamp(min=1e-7)) + chunks_out.append(torch.cat((mean, std), dim=1)) + out = torch.cat(chunks_out, dim=1) + return out + + def get_out_dim(self): + self.out_dim = 2 * self.in_dim + return self.out_dim + + +class MQMHASTP(torch.nn.Module): + """ An attentive pooling + Reference: + multi query multi head attentive statistics pooling + https://arxiv.org/pdf/2110.05042.pdf + Args: + in_dim: the feature dimension of input + layer_num: the number of layer in the pooling layer + query_num: the number of querys + head_num: the number of heads + bottleneck_dim: the bottleneck dimension + + SA (H = 1, Q = 1, n = 2, d_s = 1) ref: + https://www.danielpovey.com/files/2018_interspeech_xvector_attention.pdf + MHA (H > 1, Q = 1, n = 1, d_s = 1) ref: + https://arxiv.org/pdf/1906.09890.pdf + AS (H = 1, Q > 1, n = 2, d_s = 1) ref: + https://arxiv.org/pdf/1803.10963.pdf + VSA (H = 1, Q > 1, n = 2, d_s = d_h) ref: + http://www.interspeech2020.org/uploadfile/pdf/Mon-2-10-5.pdf + """ + + def __init__(self, + in_dim, + layer_num=2, + query_num=2, + head_num=8, + d_s=2, + bottleneck_dim=64, + **kwargs): + super(MQMHASTP, self).__init__() + self.n_query = nn.ModuleList([ + MHASTP(in_dim, + layer_num=layer_num, + head_num=head_num, + d_s=d_s, + bottleneck_dim=bottleneck_dim) for i in range(query_num) + ]) + self.query_num = query_num + self.in_dim = in_dim + + def forward(self, input): + """ + input: a 3-dimensional tensor in xvector architecture + or a 4-dimensional tensor in resnet architecture + 0-dim: batch-dimension, last-dim: time-dimension (frame-dimension) + """ + if len(input.shape) == 4: # B x F x T + input = input.reshape(input.shape[0], + input.shape[1] * input.shape[2], + input.shape[3]) + assert len(input.shape) == 3 + res = [] + for i, layer in enumerate(self.n_query): + res.append(layer(input)) + out = torch.cat(res, dim=-1) + return out + + def get_out_dim(self): + self.out_dim = self.in_dim * 2 * self.query_num + return self.out_dim + + +class XI(torch.nn.Module): + def __init__(self, in_dim, hidden_size=256, stddev=False, + train_mean=True, train_prec=True, **kwargs): + super(XI, self).__init__() + self.input_dim = in_dim + self.stddev = stddev + if self.stddev: + self.output_dim = 2 * self.input_dim + else: + self.output_dim = self.input_dim + self.prior_mean = torch.nn.Parameter(torch.zeros(1, self.input_dim), + requires_grad=train_mean) + self.prior_logprec = torch.nn.Parameter(torch.zeros(1, self.input_dim), + requires_grad=train_prec) + self.softmax = torch.nn.Softmax(dim=2) + + # Log-precision estimator + self.lin1_relu_bn = nn.Sequential( + nn.Conv1d(self.input_dim, hidden_size, + kernel_size=1, stride=1, bias=True), + nn.ReLU(inplace=True), + nn.BatchNorm1d(hidden_size)) + self.lin2 = nn.Conv1d(hidden_size, self.input_dim, kernel_size=1, + stride=1, bias=True) + self.softplus2 = torch.nn.Softplus(beta=1, threshold=20) + + def forward(self, inputs): + """ + @inputs: a 3-dimensional tensor (a batch), + including [samples-index, frames-dim-index, frames-index] + """ + assert len(inputs.shape) == 3 + assert inputs.shape[1] == self.input_dim + feat = inputs + # Log-precision estimator + # frame precision estimate + logprec = self.softplus2(self.lin2(self.lin1_relu_bn(feat))) + + # Square and take log before softmax + logprec = 2.0 * torch.log(logprec) + CLAMP_MIN = -15.0 + CLAMP_MAX = 15.0 + logprec = logprec.clamp(min=CLAMP_MIN, max=CLAMP_MAX) + # Gaussian Posterior Inference + # Option 1: a_o (prior_mean-phi) included in variance + weight_attn = self.softmax( + torch.cat( + (logprec, + self.prior_logprec.repeat( + logprec.shape[0], 1).unsqueeze(dim=2)), 2)) + # Posterior precision + Ls = torch.sum(torch.exp(torch.cat( + (logprec, self.prior_logprec.repeat( + logprec.shape[0], 1).unsqueeze(dim=2)), 2)), dim=2) + # Posterior mean + phi = torch.sum(torch.cat( + (feat, self.prior_mean.repeat( + feat.shape[0], 1).unsqueeze(dim=2)), 2) * weight_attn, dim=2) + + if self.stddev: + sigma2 = torch.sum(torch.cat(( + feat, self.prior_mean.repeat( + feat.shape[0], 1).unsqueeze(dim=2)), 2).pow(2) * weight_attn, dim=2) + sigma = torch.sqrt(torch.clamp(sigma2 - phi ** 2, min=1.0e-12)) + return torch.cat((phi, sigma), dim=1).unsqueeze(dim=2) + else: + return phi + + def get_out_dim(self): + return self.output_dim + + def get_prior(self): + return self.prior_mean, self.prior_logprec + +if __name__ == '__main__': + data = torch.randn(16, 512, 10, 35) + # model = StatisticsPooling() + model = MQMHASTP(512 * 10) + model = MHASTP(512 * 10) + model = MQMHASTP(512 * 10, context=False) + print(model) + + out = model(data) + print(out.shape) + print(model.get_out_dim()) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/projections.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/projections.py new file mode 100644 index 0000000000000000000000000000000000000000..18ba10fa0321af7ec6f8e07c7d6ca402f2fd4b0d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/projections.py @@ -0,0 +1,602 @@ +# Copyright (c) 2021 Shuai Wang (wsstriving@gmail.com) +# 2021 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# 2023 Bing Han (hanbing97@sjtu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +def get_projection(conf): + if conf['project_type'] == 'add_margin': + projection = AddMarginProduct(conf['embed_dim'], + conf['num_class'], + scale=conf['scale'], + margin=0.0) + elif conf['project_type'] == 'arc_margin': + projection = ArcMarginProduct(conf['embed_dim'], + conf['num_class'], + scale=conf['scale'], + margin=0.0, + easy_margin=conf['easy_margin']) + elif conf['project_type'] == 'arc_margin_intertopk_subcenter': + projection = ArcMarginProduct_intertopk_subcenter( + conf['embed_dim'], + conf['num_class'], + scale=conf['scale'], + margin=0.0, + easy_margin=conf['easy_margin'], + K=conf.get('K', 3), + mp=conf.get('mp', 0.06), + k_top=conf.get('k_top', 5), + do_lm=conf.get('do_lm', False)) + elif conf['project_type'] == 'sphere': + projection = SphereProduct(conf['embed_dim'], + conf['num_class'], + margin=4) + elif conf['project_type'] == 'ham_margin': + projection = HyperbolicAMSoftmax(conf['embed_dim'], + conf['num_class'], + scale=conf['scale'], + margin=0.0, + curvature=conf.get('curvature', 1.0)) + elif conf['project_type'] == 'sphereface2': + projection = SphereFace2(conf['embed_dim'], + conf['num_class'], + scale=conf['scale'], + margin=0.0, + t=conf.get('t', 3), + lanbuda=conf.get('lanbuda', 0.7), + margin_type=conf.get('margin_type', 'C')) + else: + projection = Linear(conf['embed_dim'], conf['num_class']) + + return projection + + +class SphereFace2(nn.Module): + r"""Implement of sphereface2 for speaker verification: + Reference: + [1] Exploring Binary Classification Loss for Speaker Verification + https://ieeexplore.ieee.org/abstract/document/10094954 + [2] Sphereface2: Binary classification is all you need + for deep face recognition + https://arxiv.org/pdf/2108.01513 + Args: + in_features: size of each input sample + out_features: size of each output sample + scale: norm of input feature + margin: margin + lanbuda: weight of positive and negative pairs + t: parameter for adjust score distribution + margin_type: A:cos(theta+margin) or C:cos(theta)-margin + Recommend margin: + training: 0.2 for C and 0.15 for A + LMF: 0.3 for C and 0.25 for A + """ + + def __init__(self, + in_features, + out_features, + scale=32.0, + margin=0.2, + lanbuda=0.7, + t=3, + margin_type='C'): + super(SphereFace2, self).__init__() + self.in_features = in_features + self.out_features = out_features + self.scale = scale + self.weight = nn.Parameter(torch.FloatTensor(out_features, + in_features)) + nn.init.xavier_uniform_(self.weight) + self.bias = nn.Parameter(torch.zeros(1, 1)) + self.t = t + self.lanbuda = lanbuda + self.margin_type = margin_type + + ######## + self.margin = margin + self.cos_m = math.cos(margin) + self.sin_m = math.sin(margin) + self.th = math.cos(math.pi - margin) + self.mm = math.sin(math.pi - margin) + self.mmm = 1.0 + math.cos(math.pi - margin) + ######## + + def update(self, margin=0.2): + self.margin = margin + self.cos_m = math.cos(margin) + self.sin_m = math.sin(margin) + self.th = math.cos(math.pi - margin) + self.mm = math.sin(math.pi - margin) + self.mmm = 1.0 + math.cos(math.pi - margin) + + def fun_g(self, z, t: int): + gz = 2 * torch.pow((z + 1) / 2, t) - 1 + return gz + + def forward(self, input, label): + # compute similarity + cos = F.linear(F.normalize(input), F.normalize(self.weight)) + + if self.margin_type == 'A': # arcface type + sin = torch.sqrt(1.0 - torch.pow(cos, 2)) + cos_m_theta_p = self.scale * self.fun_g( + torch.where(cos > self.th, cos * self.cos_m - sin * self.sin_m, + cos - self.mmm), self.t) + self.bias[0][0] + cos_m_theta_n = self.scale * self.fun_g( + cos * self.cos_m + sin * self.sin_m, self.t) + self.bias[0][0] + cos_p_theta = self.lanbuda * torch.log( + 1 + torch.exp(-1.0 * cos_m_theta_p)) + cos_n_theta = ( + 1 - self.lanbuda) * torch.log(1 + torch.exp(cos_m_theta_n)) + else: # cosface type + cos_m_theta_p = self.scale * (self.fun_g(cos, self.t) - + self.margin) + self.bias[0][0] + cos_m_theta_n = self.scale * (self.fun_g(cos, self.t) + + self.margin) + self.bias[0][0] + cos_p_theta = self.lanbuda * torch.log( + 1 + torch.exp(-1.0 * cos_m_theta_p)) + cos_n_theta = ( + 1 - self.lanbuda) * torch.log(1 + torch.exp(cos_m_theta_n)) + + target_mask = input.new_zeros(cos.size()) + target_mask.scatter_(1, label.view(-1, 1).long(), 1.0) + nontarget_mask = 1 - target_mask + cos1 = (cos - self.margin) * target_mask + cos * nontarget_mask + output = self.scale * cos1 # for computing the accuracy + loss = (target_mask * cos_p_theta + + nontarget_mask * cos_n_theta).sum(1).mean() + return output, loss + + def extra_repr(self): + return '''in_features={}, out_features={}, scale={}, lanbuda={}, + margin={}, t={}, margin_type={}'''.format( + self.in_features, self.out_features, self.scale, self.lanbuda, + self.margin, self.t, self.margin_type) + + +class ArcMarginProduct(nn.Module): + r"""Implement of large margin arc distance: : + Args: + in_features: size of each input sample + out_features: size of each output sample + scale: norm of input feature + margin: margin + cos(theta + margin) + """ + + def __init__(self, + in_features, + out_features, + scale=32.0, + margin=0.2, + easy_margin=False): + super(ArcMarginProduct, self).__init__() + self.in_features = in_features + self.out_features = out_features + self.scale = scale + self.margin = margin + self.weight = nn.Parameter(torch.FloatTensor(out_features, + in_features)) + nn.init.xavier_uniform_(self.weight) + + self.easy_margin = easy_margin + self.cos_m = math.cos(margin) + self.sin_m = math.sin(margin) + self.th = math.cos(math.pi - margin) + self.mm = math.sin(math.pi - margin) * margin + self.mmm = 1.0 + math.cos( + math.pi - margin) # this can make the output more continuous + ######## + self.m = self.margin + ######## + + def update(self, margin=0.2): + self.margin = margin + self.cos_m = math.cos(margin) + self.sin_m = math.sin(margin) + self.th = math.cos(math.pi - margin) + self.mm = math.sin(math.pi - margin) * margin + self.m = self.margin + self.mmm = 1.0 + math.cos(math.pi - margin) + # self.weight = self.weight + # self.scale = self.scale + + def forward(self, input, label): + cosine = F.linear(F.normalize(input), F.normalize(self.weight)) + sine = torch.sqrt(1.0 - torch.pow(cosine, 2)) + phi = cosine * self.cos_m - sine * self.sin_m + if self.easy_margin: + phi = torch.where(cosine > 0, phi, cosine) + else: + ######## + # phi = torch.where(cosine > self.th, phi, cosine - self.mm) + phi = torch.where(cosine > self.th, phi, cosine - self.mmm) + ######## + + one_hot = input.new_zeros(cosine.size()) + one_hot.scatter_(1, label.view(-1, 1).long(), 1) + output = (one_hot * phi) + ((1.0 - one_hot) * cosine) + output *= self.scale + + return output + + def extra_repr(self): + return '''in_features={}, out_features={}, scale={}, + margin={}, easy_margin={}'''.format(self.in_features, + self.out_features, + self.scale, self.margin, + self.easy_margin) + + +class ArcMarginProduct_intertopk_subcenter(nn.Module): + r"""Implement of large margin arc distance with intertopk and subcenter: + Reference: + MULTI-QUERY MULTI-HEAD ATTENTION POOLING AND INTER-TOPK PENALTY + FOR SPEAKER VERIFICATION. + https://arxiv.org/pdf/2110.05042.pdf + Sub-center ArcFace: Boosting Face Recognition by + Large-Scale Noisy Web Faces. + https://ibug.doc.ic.ac.uk/media/uploads/documents/eccv_1445.pdf + Args: + in_features: size of each input sample + out_features: size of each output sample + scale: norm of input feature + margin: margin + cos(theta + margin) + K: number of sub-centers + k_top: number of hard samples + mp: margin penalty of hard samples + do_lm: whether do large margin finetune + """ + + def __init__(self, + in_features, + out_features, + scale=32.0, + margin=0.2, + easy_margin=False, + K=3, + mp=0.06, + k_top=5, + do_lm=False): + super(ArcMarginProduct_intertopk_subcenter, self).__init__() + self.in_features = in_features + self.out_features = out_features + self.scale = scale + self.margin = margin + self.do_lm = do_lm + + # intertopk + subcenter + self.K = K + if do_lm: # if do LMF, remove hard sample penalty + self.mp = 0.0 + self.k_top = 0 + else: + self.mp = mp + self.k_top = k_top + + # initial classifier + self.weight = nn.Parameter( + torch.FloatTensor(self.K * out_features, in_features)) + nn.init.xavier_uniform_(self.weight) + + self.easy_margin = easy_margin + self.cos_m = math.cos(margin) + self.sin_m = math.sin(margin) + self.th = math.cos(math.pi - margin) + self.mm = math.sin(math.pi - margin) * margin + self.mmm = 1.0 + math.cos( + math.pi - margin) # this can make the output more continuous + ######## + self.m = self.margin + ######## + self.cos_mp = math.cos(0.0) + self.sin_mp = math.sin(0.0) + + def update(self, margin=0.2): + self.margin = margin + self.cos_m = math.cos(margin) + self.sin_m = math.sin(margin) + self.th = math.cos(math.pi - margin) + self.mm = math.sin(math.pi - margin) * margin + self.m = self.margin + self.mmm = 1.0 + math.cos(math.pi - margin) + + # hard sample margin is increasing as margin + if margin > 0.001: + mp = self.mp * (margin / 0.2) + else: + mp = 0.0 + self.cos_mp = math.cos(mp) + self.sin_mp = math.sin(mp) + + def forward(self, input, label): + cosine = F.linear(F.normalize(input), + F.normalize(self.weight)) # (batch, out_dim * k) + cosine = torch.reshape( + cosine, (-1, self.out_features, self.K)) # (batch, out_dim, k) + cosine, _ = torch.max(cosine, 2) # (batch, out_dim) + + sine = torch.sqrt(1.0 - torch.pow(cosine, 2)) + phi = cosine * self.cos_m - sine * self.sin_m + phi_mp = cosine * self.cos_mp + sine * self.sin_mp + + if self.easy_margin: + phi = torch.where(cosine > 0, phi, cosine) + else: + ######## + # phi = torch.where(cosine > self.th, phi, cosine - self.mm) + phi = torch.where(cosine > self.th, phi, cosine - self.mmm) + ######## + + one_hot = input.new_zeros(cosine.size()) + one_hot.scatter_(1, label.view(-1, 1).long(), 1) + + if self.k_top > 0: + # topk (j != y_i) + _, top_k_index = torch.topk(cosine - 2 * one_hot, + self.k_top) # exclude j = y_i + top_k_one_hot = input.new_zeros(cosine.size()).scatter_( + 1, top_k_index, 1) + + # sum + output = (one_hot * phi) + (top_k_one_hot * phi_mp) + ( + (1.0 - one_hot - top_k_one_hot) * cosine) + else: + output = (one_hot * phi) + ((1.0 - one_hot) * cosine) + output *= self.scale + return output + + def extra_repr(self): + return 'in_features={}, out_features={}, scale={}, margin={}, easy_margin={},' \ + 'K={}, mp={}, k_top={}, do_lm={}'.format( + self.in_features, self.out_features, self.scale, self.margin, + self.easy_margin, self.K, self.mp, self.k_top, self.do_lm) + + +class AddMarginProduct(nn.Module): + r"""Implement of large margin cosine distance: : + Args: + in_features: size of each input sample + out_features: size of each output sample + scale: norm of input feature + margin: margin + cos(theta) - margin + """ + + def __init__(self, in_features, out_features, scale=32.0, margin=0.20): + super(AddMarginProduct, self).__init__() + self.in_features = in_features + self.out_features = out_features + self.scale = scale + self.margin = margin + self.weight = nn.Parameter(torch.FloatTensor(out_features, + in_features)) + nn.init.xavier_uniform_(self.weight) + + def update(self, margin): + self.margin = margin + + def forward(self, input, label): + # ---------------- cos(theta) & phi(theta) --------------- + cosine = F.linear(F.normalize(input), F.normalize(self.weight)) + phi = cosine - self.margin + # ---------------- convert label to one-hot --------------- + one_hot = input.new_zeros(cosine.size()) + one_hot.scatter_(1, label.view(-1, 1).long(), 1) + output = (one_hot * phi) + ((1.0 - one_hot) * cosine) + output *= self.scale + return output + + def __repr__(self): + return self.__class__.__name__ + '(' \ + + 'in_features=' + str(self.in_features) \ + + ', out_features=' + str(self.out_features) \ + + ', scale=' + str(self.scale) \ + + ', margin=' + str(self.margin) + ')' + + +class SphereProduct(nn.Module): + r"""Implement of large margin cosine distance: : + Args: + in_features: size of each input sample + out_features: size of each output sample + margin: margin + cos(margin * theta) + """ + + def __init__(self, in_features, out_features, margin=2): + super(SphereProduct, self).__init__() + self.in_features = in_features + self.out_features = out_features + self.margin = margin + self.base = 1000.0 + self.gamma = 0.12 + self.power = 1 + self.LambdaMin = 5.0 + self.iter = 0 + self.weight = nn.Parameter(torch.FloatTensor(out_features, + in_features)) + nn.init.xavier_uniform(self.weight) + + # duplication formula + self.mlambda = [ + lambda x: x**0, lambda x: x**1, lambda x: 2 * x**2 - 1, + lambda x: 4 * x**3 - 3 * x, lambda x: 8 * x**4 - 8 * x**2 + 1, + lambda x: 16 * x**5 - 20 * x**3 + 5 * x + ] + assert self.margin < 6 + + def forward(self, input, label): + # lambda = max(lambda_min,base*(1+gamma*iteration)^(-power)) + self.iter += 1 + self.lamb = max( + self.LambdaMin, + self.base * (1 + self.gamma * self.iter)**(-1 * self.power)) + + cos_theta = F.linear(F.normalize(input), F.normalize(self.weight)) + cos_theta = cos_theta.clamp(-1, 1) + cos_m_theta = self.mlambda[self.margin](cos_theta) + theta = cos_theta.data.acos() + k = (self.margin * theta / 3.14159265).floor() + phi_theta = ((-1.0)**k) * cos_m_theta - 2 * k + NormOfFeature = torch.norm(input, 2, 1) + one_hot = input.new_zeros(cos_theta.size()) + one_hot.scatter_(1, label.view(-1, 1), 1) + output = (one_hot * (phi_theta - cos_theta) / + (1 + self.lamb)) + cos_theta + output *= NormOfFeature.view(-1, 1) + + return output + + def __repr__(self): + return self.__class__.__name__ + '(' \ + + 'in_features=' + str(self.in_features) \ + + ', out_features=' + str(self.out_features) \ + + ', margin=' + str(self.margin) + ')' + + +class HyperbolicAMSoftmax(nn.Module): + r"""Implement of Hyperbolic Additive Margin Softmax + for speaker verification: + Reference: + Hyperbolic Additive Margin Softmax with Hierarchical Information + for Speaker Verification + https://arxiv.org/abs/2601.19709 + Args: + in_features: size of each input sample (embedding dim) + out_features: size of each output sample (num speakers) + scale: scale factor for logits (s) + margin: additive margin on hyperbolic distance (m) + curvature: curvature of the Poincare ball (c) + """ + + def __init__(self, + in_features, + out_features, + scale=30.0, + margin=0.2, + curvature=1.0): + super(HyperbolicAMSoftmax, self).__init__() + self.in_features = in_features + self.out_features = out_features + self.scale = scale + self.margin = margin + self.curvature = curvature + + # Class centers in hyperbolic space, small init to stay near origin + self.weight = nn.Parameter(torch.randn(out_features, in_features) * 1e-3) + + def update(self, margin=0.2): + self.margin = margin + + def proj_to_ball(self, x: torch.Tensor, eps: float = 1e-5) -> torch.Tensor: + """Project embeddings onto the Poincare ball with curvature c.""" + norm = torch.norm(x, dim=-1, keepdim=True).clamp(min=eps) + max_norm = (1.0 - eps) / (self.curvature ** 0.5) + scale = torch.clamp(max_norm / norm, max=1.0) + return x * scale + + def poincare_distance(self, x: torch.Tensor, y: torch.Tensor, + eps: float = 1e-5) -> torch.Tensor: + """Compute Poincare distance between points on the ball.""" + x_norm = torch.norm(x, dim=-1, keepdim=True).clamp(max=1 - eps) + y_norm = torch.norm(y, dim=-1, keepdim=True).clamp(max=1 - eps) + diff = x - y + diff_norm = torch.norm(diff, dim=-1, keepdim=True) + + num = 2 * diff_norm.pow(2) + denom = (1 - x_norm.pow(2)) * (1 - y_norm.pow(2)) + # Clamp to ensure argument >= 1 for acosh stability + return torch.acosh((1 + num / denom.clamp_min(eps)).clamp(min=1.0 + eps)) + + def forward(self, input, label): + # Project embeddings and class centers to Poincare ball + x_hyp = self.proj_to_ball(input) + w_hyp = self.proj_to_ball(self.weight) + + # Compute pairwise Poincare distances: (B, out_features) + B, D = x_hyp.shape + C = w_hyp.shape[0] + x_exp = x_hyp.unsqueeze(1).expand(B, C, D) + w_exp = w_hyp.unsqueeze(0).expand(B, C, D) + dist = self.poincare_distance(x_exp, w_exp).squeeze(-1) + + # Add margin to correct class distances + margin = torch.zeros_like(dist) + margin.scatter_(1, label.view(-1, 1), self.margin) + dist_m = dist + margin + + # Convert distance to logits (smaller distance = higher logit) + output = -self.scale * dist_m + return output + + def extra_repr(self): + return ('in_features={}, out_features={}, scale={}, ' + 'margin={}, curvature={}'.format( + self.in_features, self.out_features, self.scale, + self.margin, self.curvature)) + + +class Linear(nn.Module): + """ + The linear transform for simple softmax loss + """ + + def __init__(self, emb_dim=512, class_num=1000): + super(Linear, self).__init__() + + self.trans = nn.Sequential(nn.BatchNorm1d(emb_dim), + nn.ReLU(inplace=True), + nn.Linear(emb_dim, class_num)) + + def forward(self, input, label): + out = self.trans(input) + return out + + +if __name__ == '__main__': + # projection = ArcMarginProduct(100, + # 200, + # scale=32.0, + # margin=0.2, + # easy_margin=False) + # + # print(hasattr(projection, 'update_mar')) + projection = ArcMarginProduct_intertopk_subcenter(100, + 200, + scale=32.0, + margin=0.0, + easy_margin=False, + K=3, + mp=0.06, + k_top=5) + print(hasattr(projection, 'update')) + projection.update(0.2) + print(projection) + embed = torch.randn(16, 100) + label = torch.randint(200, (16, )) + out = projection(embed, label) + print(out.size()) + + # for name, param in projection.named_parameters(): + # print(name) + # print(param.shape) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/redimnet.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/redimnet.py new file mode 100644 index 0000000000000000000000000000000000000000..bc5434aa99c53626721e91636533bdf1473afff6 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/redimnet.py @@ -0,0 +1,1078 @@ +# Copyright (c) 2024 https://github.com/IDRnD/ReDimNet +# 2024 Shuai Wang (wsstriving@gmail.com) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Redimnet in pytorch. + +Reference: +Paper: "Reshape Dimensions Network for Speaker Recognition" +Repo: https://github.com/IDRnD/ReDimNet + +Cite: +@misc{yakovlev2024reshapedimensionsnetworkspeaker, + title={Reshape Dimensions Network for Speaker Recognition}, + author={Ivan Yakovlev and Rostislav Makarov and Andrei Balykin + and Pavel Malov and Anton Okhotnikov and Nikita Torgashov}, + year={2024}, + eprint={2407.18223}, + archivePrefix={arXiv}, + primaryClass={eess.AS}, + url={https://arxiv.org/abs/2407.18223}, +} +""" +import math + +import functools +import torch +import torch.nn as nn +import torch.nn.functional as F +import wespeaker.models.pooling_layers as pooling_layers + +MaxPoolNd = {1: nn.MaxPool1d, 2: nn.MaxPool2d} +ConvNd = {1: nn.Conv1d, 2: nn.Conv2d} +BatchNormNd = {1: nn.BatchNorm1d, 2: nn.BatchNorm2d} + + +class to1d(nn.Module): + + def forward(self, x): + size = x.size() + bs, c, f, t = tuple(size) + return x.permute((0, 2, 1, 3)).reshape((bs, c * f, t)) + + +class NewGELUActivation(nn.Module): + + def forward(self, input): + return (0.5 * input * (1.0 + torch.tanh( + math.sqrt(2.0 / math.pi) * + (input + 0.044715 * torch.pow(input, 3.0))))) + + +class LayerNorm(nn.Module): + """ + LayerNorm that supports two data formats: channels_last or channels_first. + The ordering of the dimensions in the inputs. + channels_last corresponds to inputs with shape (batch_size, T, channels) + while channels_first corresponds to shape (batch_size, channels, T). + """ + + def __init__(self, C, eps=1e-6, data_format="channels_last"): + super().__init__() + self.weight = nn.Parameter(torch.ones(C)) + self.bias = nn.Parameter(torch.zeros(C)) + self.eps = eps + self.data_format = data_format + if self.data_format not in ["channels_last", "channels_first"]: + raise NotImplementedError + self.C = (C, ) + + def forward(self, x): + if self.data_format == "channels_last": + return F.layer_norm(x, self.C, self.weight, self.bias, self.eps) + elif self.data_format == "channels_first": + u = x.mean(1, keepdim=True) + s = (x - u).pow(2).mean(1, keepdim=True) + x = (x - u) / torch.sqrt(s + self.eps) + + w = self.weight + b = self.bias + for _ in range(x.ndim - 2): + w = w.unsqueeze(-1) + b = b.unsqueeze(-1) + x = w * x + b + return x + + def extra_repr(self) -> str: + return ", ".join([ + f"{k}={v}" for k, v in { + "C": self.C, + "data_format": self.data_format, + "eps": self.eps, + }.items() + ]) + + +class GRU(nn.Module): + + def __init__(self, *args, **kwargs): + super(GRU, self).__init__() + self.gru = nn.GRU(*args, **kwargs) + + def forward(self, x): + # x : (bs,C,T) + return self.gru(x.permute((0, 2, 1)))[0].permute((0, 2, 1)) + + +class PosEncConv(nn.Module): + + def __init__(self, C, ks, groups=None): + super().__init__() + assert ks % 2 == 1 + self.conv = nn.Conv1d(C, + C, + ks, + padding=ks // 2, + groups=C if groups is None else groups) + self.norm = LayerNorm(C, eps=1e-6, data_format="channels_first") + + def forward(self, x): + return x + self.norm(self.conv(x)) + + +class ConvNeXtLikeBlock(nn.Module): + + def __init__( + self, + C, + dim=2, + kernel_sizes=((3, 3), ), + group_divisor=1, + padding="same", + ): + super().__init__() + self.dwconvs = nn.ModuleList(modules=[ + ConvNd[dim]( + C, + C, + kernel_size=ks, + padding=padding, + groups=C // group_divisor if group_divisor is not None else 1, + ) for ks in kernel_sizes + ]) + self.norm = BatchNormNd[dim](C * len(kernel_sizes)) + self.gelu = nn.GELU() + self.pwconv1 = ConvNd[dim](C * len(kernel_sizes), C, 1) + + def forward(self, x): + skip = x + x = torch.cat([dwconv(x) for dwconv in self.dwconvs], dim=1) + x = self.gelu(self.norm(x)) + x = self.pwconv1(x) + x = skip + x + return x + + +class ConvBlock2d(nn.Module): + + def __init__(self, c, f, block_type="convnext_like", group_divisor=1): + super().__init__() + if block_type == "convnext_like": + self.conv_block = ConvNeXtLikeBlock( + c, + dim=2, + kernel_sizes=[(3, 3)], + group_divisor=group_divisor, + padding="same", + ) + elif block_type == "basic_resnet": + self.conv_block = ResBasicBlock( + c, + c, + f, + stride=1, + se_channels=min(64, max(c, 32)), + group_divisor=group_divisor, + use_fwSE=False, + ) + elif block_type == "basic_resnet_fwse": + self.conv_block = ResBasicBlock( + c, + c, + f, + stride=1, + se_channels=min(64, max(c, 32)), + group_divisor=group_divisor, + use_fwSE=True, + ) + else: + raise NotImplementedError() + + def forward(self, x): + return self.conv_block(x) + + +class MultiHeadAttention(nn.Module): + """Multi-headed attention from 'Attention Is All You Need' paper""" + + def __init__( + self, + embed_dim, + num_heads, + dropout=0.0, + bias=True, + ): + super().__init__() + self.embed_dim = embed_dim + self.num_heads = num_heads + self.dropout = dropout + self.head_dim = embed_dim // num_heads + + if (self.head_dim * num_heads) != self.embed_dim: + raise ValueError( + f"embed_dim must be divisible by num_heads (got " + f"`embed_dim`: {self.embed_dim} and `num_heads`: {num_heads})." + ) + self.scaling = self.head_dim**-0.5 + + self.k_proj = nn.Linear(embed_dim, embed_dim, bias=bias) + self.v_proj = nn.Linear(embed_dim, embed_dim, bias=bias) + self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias) + self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias) + + def _shape(self, tensor: torch.Tensor, seq_len, bsz): + return (tensor.view(bsz, seq_len, self.num_heads, + self.head_dim).transpose(1, 2).contiguous()) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + """Input shape: Batch x Time x Channel""" + bsz, tgt_len, _ = hidden_states.size() + + # get query proj + query_states = self.q_proj(hidden_states) * self.scaling + # self_attention + key_states = self._shape(self.k_proj(hidden_states), -1, bsz) + value_states = self._shape(self.v_proj(hidden_states), -1, bsz) + + proj_shape = (bsz * self.num_heads, -1, self.head_dim) + query_states = self._shape(query_states, tgt_len, + bsz).view(*proj_shape) + key_states = key_states.view(*proj_shape) + value_states = value_states.view(*proj_shape) + + attn_weights = torch.bmm(query_states, key_states.transpose(1, 2)) + attn_weights = F.softmax(attn_weights, dim=-1) + + attn_probs = F.dropout(attn_weights, + p=self.dropout, + training=self.training) + + attn_output = torch.bmm(attn_probs, value_states) + + attn_output = attn_output.view(bsz, self.num_heads, tgt_len, + self.head_dim) + attn_output = attn_output.transpose(1, 2) + + # Use the `embed_dim` from the config (stored in the class) + # rather than `hidden_state` because `attn_output` can be + # partitioned aross GPUs when using tensor-parallelism. + attn_output = attn_output.reshape(bsz, tgt_len, self.embed_dim) + + attn_output = self.out_proj(attn_output) + return attn_output + + +class TransformerEncoderLayer(nn.Module): + + def __init__( + self, + n_state, + n_mlp, + n_head, + channel_last=False, + act_do=0.0, + att_do=0.0, + hid_do=0.0, + ln_eps=1e-6, + ): + + hidden_size = n_state + num_attention_heads = n_head + intermediate_size = n_mlp + activation_dropout = act_do + attention_dropout = att_do + hidden_dropout = hid_do + layer_norm_eps = ln_eps + + super().__init__() + self.channel_last = channel_last + self.attention = MultiHeadAttention( + embed_dim=hidden_size, + num_heads=num_attention_heads, + dropout=attention_dropout, + ) + self.layer_norm = nn.LayerNorm(hidden_size, eps=layer_norm_eps) + self.feed_forward = FeedForward( + hidden_size=hidden_size, + intermediate_size=intermediate_size, + activation_dropout=activation_dropout, + hidden_dropout=hidden_dropout, + ) + self.final_layer_norm = nn.LayerNorm(hidden_size, eps=layer_norm_eps) + + def forward(self, hidden_states): + if not self.channel_last: + hidden_states = hidden_states.permute(0, 2, 1) + attn_residual = hidden_states + hidden_states = self.attention(hidden_states) + hidden_states = attn_residual + hidden_states + + hidden_states = self.layer_norm(hidden_states) + hidden_states = hidden_states + self.feed_forward(hidden_states) + hidden_states = self.final_layer_norm(hidden_states) + + outputs = hidden_states + if not self.channel_last: + outputs = outputs.permute(0, 2, 1) + return outputs + + +class FeedForward(nn.Module): + + def __init__( + self, + hidden_size, + intermediate_size, + activation_dropout=0.0, + hidden_dropout=0.0, + ): + super().__init__() + self.intermediate_dropout = nn.Dropout(activation_dropout) + self.intermediate_dense = nn.Linear(hidden_size, intermediate_size) + self.intermediate_act_fn = NewGELUActivation() + self.output_dense = nn.Linear(intermediate_size, hidden_size) + self.output_dropout = nn.Dropout(hidden_dropout) + + def forward(self, hidden_states): + hidden_states = self.intermediate_dense(hidden_states) + hidden_states = self.intermediate_act_fn(hidden_states) + hidden_states = self.intermediate_dropout(hidden_states) + hidden_states = self.output_dense(hidden_states) + hidden_states = self.output_dropout(hidden_states) + return hidden_states + + +class BasicBlock(nn.Module): + """ + Key difference with the BasicBlock in resnet.py: + 1. If use group convolution, conv1 have same number of input/output channels + 2. No stride to downsample + """ + + def __init__( + self, + in_planes, + planes, + stride=1, + group_divisor=4, + ): + super().__init__() + self.conv1 = nn.Conv2d( + in_planes, + in_planes if group_divisor is not None else planes, + kernel_size=3, + stride=stride, + padding=1, + bias=False, + groups=in_planes // + group_divisor if group_divisor is not None else 1, + ) + + # If using group convolution, add point-wise conv to reshape + if group_divisor is not None: + self.conv1pw = nn.Conv2d(in_planes, planes, 1) + else: + self.conv1pw = nn.Identity() + + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d( + planes, + planes, + kernel_size=3, + padding=1, + bias=False, + groups=planes // group_divisor if group_divisor is not None else 1, + ) + + # If using group convolution, add point-wise conv to reshape + if group_divisor is not None: + self.conv2pw = nn.Conv2d(planes, planes, 1) + else: + self.conv2pw = nn.Identity() + + self.bn2 = nn.BatchNorm2d(planes) + self.relu = nn.ReLU(inplace=True) + + if planes != in_planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, + planes, + kernel_size=1, + stride=stride, + bias=False), + nn.BatchNorm2d(planes), + ) + else: + self.shortcut = nn.Identity() + + def forward(self, x): + residual = x + + out = self.conv1pw(self.conv1(x)) + out = self.relu(out) + out = self.bn1(out) + + out = self.conv2pw(self.conv2(out)) + out = self.bn2(out) + + out += self.shortcut(residual) + out = self.relu(out) + return out + + +class fwSEBlock(nn.Module): + """ + Squeeze-and-Excitation block + link: https://arxiv.org/pdf/1709.01507.pdf + PyTorch implementation + """ + + def __init__(self, num_freq, num_feats=64): + super(fwSEBlock, self).__init__() + self.squeeze = nn.Linear(num_freq, num_feats) + self.exitation = nn.Linear(num_feats, num_freq) + + self.activation = nn.ReLU() # Assuming ReLU, modify as needed + + def forward(self, inputs): + # [bs, C, F, T] + x = torch.mean(inputs, dim=[1, 3]) + x = self.squeeze(x) + x = self.activation(x) + x = self.exitation(x) + x = torch.sigmoid(x) + # Reshape and apply excitation + x = x[:, None, :, None] + x = inputs * x + return x + + +class ResBasicBlock(nn.Module): + + def __init__( + self, + in_planes, + planes, + num_freq, + stride=1, + se_channels=64, + group_divisor=4, + use_fwSE=False, + ): + super().__init__() + self.conv1 = nn.Conv2d( + in_planes, + in_planes if group_divisor is not None else planes, + kernel_size=3, + stride=stride, + padding=1, + bias=False, + groups=in_planes // + group_divisor if group_divisor is not None else 1, + ) + if group_divisor is not None: + self.conv1pw = nn.Conv2d(in_planes, planes, 1) + else: + self.conv1pw = nn.Identity() + + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d( + planes, + planes, + kernel_size=3, + padding=1, + bias=False, + groups=planes // group_divisor if group_divisor is not None else 1, + ) + + if group_divisor is not None: + self.conv2pw = nn.Conv2d(planes, planes, 1) + else: + self.conv2pw = nn.Identity() + + self.bn2 = nn.BatchNorm2d(planes) + self.relu = nn.ReLU(inplace=True) + + if use_fwSE: + self.se = fwSEBlock(num_freq, se_channels) + else: + self.se = nn.Identity() + + if planes != in_planes: + self.downsample = nn.Sequential( + nn.Conv2d(in_planes, + planes, + kernel_size=1, + stride=stride, + bias=False), + nn.BatchNorm2d(planes), + ) + else: + self.downsample = nn.Identity() + + def forward(self, x): + residual = x + + out = self.conv1pw(self.conv1(x)) + out = self.relu(out) + out = self.bn1(out) + + out = self.conv2pw(self.conv2(out)) + out = self.bn2(out) + out = self.se(out) + + out += self.downsample(residual) + out = self.relu(out) + return out + + +class TimeContextBlock1d(nn.Module): + """ """ + + def __init__( + self, + C, + hC, + pos_ker_sz=59, + block_type="att", + ): + super().__init__() + assert pos_ker_sz + + self.red_dim_conv = nn.Sequential( + nn.Conv1d(C, hC, 1), + LayerNorm(hC, eps=1e-6, data_format="channels_first")) + + if block_type == "fc": + self.tcm = nn.Sequential( + nn.Conv1d(hC, hC * 2, 1), + LayerNorm(hC * 2, eps=1e-6, data_format="channels_first"), + nn.GELU(), + nn.Conv1d(hC * 2, hC, 1), + ) + elif block_type == "gru": + # Just GRU + self.tcm = nn.Sequential( + GRU( + input_size=hC, + hidden_size=hC, + num_layers=1, + bias=True, + batch_first=False, + dropout=0.0, + bidirectional=True, + ), + nn.Conv1d(2 * hC, hC, 1), + ) + elif block_type == "att": + # Basic Transformer self-attention encoder block + self.tcm = nn.Sequential( + PosEncConv(hC, ks=pos_ker_sz, groups=hC), + TransformerEncoderLayer(n_state=hC, n_mlp=hC * 2, n_head=4), + ) + elif block_type == "conv+att": + # Basic Transformer self-attention encoder block + self.tcm = nn.Sequential( + ConvNeXtLikeBlock(hC, + dim=1, + kernel_sizes=[7], + group_divisor=1, + padding="same"), + ConvNeXtLikeBlock(hC, + dim=1, + kernel_sizes=[19], + group_divisor=1, + padding="same"), + ConvNeXtLikeBlock(hC, + dim=1, + kernel_sizes=[31], + group_divisor=1, + padding="same"), + ConvNeXtLikeBlock(hC, + dim=1, + kernel_sizes=[59], + group_divisor=1, + padding="same"), + TransformerEncoderLayer(n_state=hC, n_mlp=hC, n_head=4), + ) + else: + raise NotImplementedError() + + self.exp_dim_conv = nn.Conv1d(hC, C, 1) + + def forward(self, x): + skip = x + x = self.red_dim_conv(x) + x = self.tcm(x) + x = self.exp_dim_conv(x) + return skip + x + + +class ReDimNetBone(nn.Module): + + def __init__( + self, + F=72, + C=16, + block_1d_type="conv+att", + block_2d_type="basic_resnet", + stages_setup=( + # stride, num_blocks, conv_exp, kernel_size, att_block_red + (1, 2, 1, [(3, 3)], None), # 16 + (2, 3, 1, [(3, 3)], None), # 32 + # 64, (72*12 // 8) = 108 - channels in attention block + (3, 4, 1, [(3, 3)], 8), + (2, 5, 1, [(3, 3)], 8), # 128 + (1, 5, 1, [(7, 1)], 8), # 128 # TDNN - time context + (2, 3, 1, [(3, 3)], 8), # 256 + ), + group_divisor=1, + out_channels=512, + ): + super().__init__() + self.F = F + self.C = C + + self.block_1d_type = block_1d_type + self.block_2d_type = block_2d_type + + self.stages_setup = stages_setup + self.build(stages_setup, group_divisor, out_channels) + + def build(self, stages_setup, group_divisor, out_channels): + self.num_stages = len(stages_setup) + + cur_c = self.C + cur_f = self.F + # Weighting the inputs + # TODO: ask authors about the impact of this pre-weighting + self.inputs_weights = torch.nn.ParameterList( + [nn.Parameter(torch.ones(1, 1, 1, 1), requires_grad=False)] + [ + nn.Parameter( + torch.zeros(1, num_inputs + 1, self.C * self.F, 1), + requires_grad=True, + ) for num_inputs in range(1, + len(stages_setup) + 1) + ]) + + self.stem = nn.Sequential( + nn.Conv2d(1, int(cur_c), kernel_size=3, stride=1, padding="same"), + LayerNorm(int(cur_c), eps=1e-6, data_format="channels_first"), + ) + + Block1d = functools.partial(TimeContextBlock1d, + block_type=self.block_1d_type) + Block2d = functools.partial(ConvBlock2d, block_type=self.block_2d_type) + + self.stages_cfs = [] + for stage_ind, ( + stride, + num_blocks, + conv_exp, + kernel_sizes, # TODO: Why the kernel_sizes are not used? + att_block_red, + ) in enumerate(stages_setup): + assert stride in [1, 2, 3] + # Pool frequencies & expand channels if needed + layers = [ + nn.Conv2d( + int(cur_c), + int(stride * cur_c * conv_exp), + kernel_size=(stride, 1), + stride=(stride, 1), + padding=0, + groups=1, + ), + ] + + self.stages_cfs.append((cur_c, cur_f)) + + cur_c = stride * cur_c + assert cur_f % stride == 0 + cur_f = cur_f // stride + + for _ in range(num_blocks): + # ConvBlock2d(f, c, block_type="convnext_like", group_divisor=1) + layers.append( + Block2d(c=int(cur_c * conv_exp), + f=cur_f, + group_divisor=group_divisor)) + + if conv_exp != 1: + # Squeeze back channels to align with ReDimNet c+f reshaping: + _group_divisor = group_divisor + # if c // group_divisor == 0: + # _group_divisor = c + layers.append( + nn.Sequential( + nn.Conv2d( + int(cur_c * conv_exp), + cur_c, + kernel_size=(3, 3), + stride=1, + padding="same", + groups=(cur_c // _group_divisor + if _group_divisor is not None else 1), + ), + nn.BatchNorm2d( + cur_c, + eps=1e-6, + ), + nn.GELU(), + nn.Conv2d(cur_c, cur_c, 1), + )) + + layers.append(to1d()) + + # reduce block? + if att_block_red is not None: + layers.append( + Block1d(self.C * self.F, + hC=(self.C * self.F) // att_block_red)) + + setattr(self, f"stage{stage_ind}", nn.Sequential(*layers)) + + if out_channels is not None: + self.mfa = nn.Sequential( + nn.Conv1d(self.F * self.C, + out_channels, + kernel_size=1, + padding="same"), + nn.BatchNorm1d(out_channels, affine=True), + ) + else: + self.mfa = nn.Identity() + + def to1d(self, x): + size = x.size() + bs, c, f, t = tuple(size) + return x.permute((0, 2, 1, 3)).reshape((bs, c * f, t)) + + def to2d(self, x, c, f): + size = x.size() + bs, cf, t = tuple(size) + return x.reshape((bs, f, c, t)).permute((0, 2, 1, 3)) + + def weigth1d(self, outs_1d, i): + xs = torch.cat([t.unsqueeze(1) for t in outs_1d], dim=1) + w = F.softmax(self.inputs_weights[i], dim=1) + x = (w * xs).sum(dim=1) + return x + + def run_stage(self, prev_outs_1d, stage_ind): + stage = getattr(self, f"stage{stage_ind}") + c, f = self.stages_cfs[stage_ind] + + x = self.weigth1d(prev_outs_1d, stage_ind) + x = self.to2d(x, c, f) + x = stage(x) + return x + + def forward(self, inp): + x = self.stem(inp) + outputs_1d = [self.to1d(x)] + for stage_ind in range(self.num_stages): + outputs_1d.append(self.run_stage(outputs_1d, stage_ind)) + x = self.weigth1d(outputs_1d, -1) + x = self.mfa(x) + return x + + +class ReDimNet(nn.Module): + + def __init__( + self, + feat_dim=72, + C=16, + block_1d_type="conv+att", + block_2d_type="basic_resnet", + # Default setup: M version: + stages_setup=( + # stride, num_blocks, kernel_sizes, layer_ext, att_block_red + (1, 2, 1, [(3, 3)], 12), + (2, 2, 1, [(3, 3)], 12), + (1, 3, 1, [(3, 3)], 12), + (2, 4, 1, [(3, 3)], 8), + (1, 4, 1, [(3, 3)], 8), + (2, 4, 1, [(3, 3)], 4), + ), + group_divisor=4, + out_channels=None, + # ------------------------- + embed_dim=192, + pooling_func="ASTP", + global_context_att=True, + two_emb_layer=False, + ): + + super().__init__() + self.two_emb_layer = two_emb_layer + self.backbone = ReDimNetBone( + feat_dim, + C, + block_1d_type, + block_2d_type, + stages_setup, + group_divisor, + out_channels, + ) + + if out_channels is None: + out_channels = C * feat_dim + + self.pool = getattr(pooling_layers, pooling_func)( + in_dim=out_channels, global_context_att=global_context_att) + + self.pool_out_dim = self.pool.get_out_dim() + self.seg_1 = nn.Linear(self.pool_out_dim, embed_dim) + if self.two_emb_layer: + self.seg_bn_1 = nn.BatchNorm1d(embed_dim, affine=False) + self.seg_2 = nn.Linear(embed_dim, embed_dim) + else: + self.seg_bn_1 = nn.Identity() + self.seg_2 = nn.Identity() + + def _get_frame_level_feat(self, x): + # for inner class usage + x = x.permute(0, 2, 1) # (B,F,T) => (B,T,F) + x = x.unsqueeze_(1) + out = self.backbone(x) + + return out + + def get_frame_level_feat(self, x): + # for outer interface + out = self._get_frame_level_feat(x).permute(0, 2, 1) + + return out # (B, T, D) + + def forward(self, x): + out = self._get_frame_level_feat(x) + + stats = self.pool(out) + embed_a = self.seg_1(stats) + if self.two_emb_layer: + out = F.relu(embed_a) + out = self.seg_bn_1(out) + embed_b = self.seg_2(out) + return embed_a, embed_b + else: + return torch.tensor(0.0), embed_a + + +def ReDimNetB0(feat_dim=60, + embed_dim=192, + pooling_func="ASTP", + two_emb_layer=False): + return ReDimNet( + feat_dim=feat_dim, + C=10, + block_1d_type="conv+att", + block_2d_type="basic_resnet", + stages_setup=[ + (1, 2, 1, [(3, 3)], 30), + (2, 3, 2, [(3, 3)], 30), + (1, 3, 3, [(3, 3)], 30), + (2, 4, 2, [(3, 3)], 10), + (1, 3, 1, [(3, 3)], 10), + ], + group_divisor=1, + out_channels=None, + embed_dim=embed_dim, + pooling_func=pooling_func, + global_context_att=True, + two_emb_layer=two_emb_layer, + ) + + +def ReDimNetB1(feat_dim=72, + embed_dim=192, + pooling_func="ASTP", + two_emb_layer=False): + return ReDimNet( + feat_dim=feat_dim, + C=12, + block_1d_type="conv+att", + block_2d_type="convnext_like", + stages_setup=[ + (1, 2, 1, [(3, 3)], None), + (2, 3, 1, [(3, 3)], None), + (3, 4, 1, [(3, 3)], 12), + (2, 5, 1, [(3, 3)], 12), + (2, 3, 1, [(3, 3)], 8), + ], + group_divisor=8, + out_channels=None, + embed_dim=embed_dim, + pooling_func=pooling_func, + global_context_att=True, + two_emb_layer=two_emb_layer, + ) + + +def ReDimNetB2(feat_dim=72, + embed_dim=192, + pooling_func="ASTP", + two_emb_layer=False): + return ReDimNet( + feat_dim=feat_dim, + C=16, + block_1d_type="conv+att", + block_2d_type="convnext_like", + stages_setup=[ + (1, 2, 1, [(3, 3)], 12), + (2, 2, 1, [(3, 3)], 12), + (1, 3, 1, [(3, 3)], 12), + (2, 4, 1, [(3, 3)], 8), + (1, 4, 1, [(3, 3)], 8), + (2, 4, 1, [(3, 3)], 4), + ], + group_divisor=4, + out_channels=None, + embed_dim=embed_dim, + pooling_func=pooling_func, + global_context_att=True, + two_emb_layer=two_emb_layer, + ) + + +def ReDimNetB3(feat_dim=72, + embed_dim=192, + pooling_func="ASTP", + two_emb_layer=False): + return ReDimNet( + feat_dim=feat_dim, + C=16, + block_1d_type="conv+att", + block_2d_type="basic_resnet_fwse", + stages_setup=[ + (1, 6, 4, [(3, 3)], 32), + (2, 6, 2, [(3, 3)], 32), + (1, 8, 2, [(3, 3)], 32), + (2, 10, 2, [(3, 3)], 16), + (1, 10, 1, [(3, 3)], 16), + (2, 8, 1, [(3, 3)], 16), + ], + group_divisor=1, + out_channels=None, + embed_dim=embed_dim, + pooling_func=pooling_func, + global_context_att=True, + two_emb_layer=two_emb_layer, + ) + + +def ReDimNetB4(feat_dim=72, + embed_dim=192, + pooling_func="ASTP", + two_emb_layer=False): + return ReDimNet( + feat_dim=feat_dim, + C=32, + block_1d_type="conv+att", + block_2d_type="basic_resnet_fwse", + stages_setup=[ + (1, 4, 2, [(3, 3)], 48), + (2, 4, 2, [(3, 3)], 48), + (1, 6, 2, [(3, 3)], 48), + (2, 6, 1, [(3, 3)], 32), + (1, 8, 1, [(3, 3)], 24), + (2, 4, 1, [(3, 3)], 16), + ], + group_divisor=1, + out_channels=None, + embed_dim=embed_dim, + pooling_func=pooling_func, + global_context_att=True, + two_emb_layer=two_emb_layer, + ) + + +def ReDimNetB5(feat_dim=72, + embed_dim=192, + pooling_func="ASTP", + two_emb_layer=False): + return ReDimNet( + feat_dim=feat_dim, + C=32, + block_1d_type="conv+att", + block_2d_type="basic_resnet_fwse", + stages_setup=[ + (1, 4, 2, [(3, 3)], 48), + (2, 4, 2, [(3, 3)], 48), + (1, 6, 2, [(3, 3)], 48), + (2, 6, 1, [(3, 3)], 32), + (1, 8, 1, [(3, 3)], 24), + (2, 4, 1, [(3, 3)], 16), + ], + group_divisor=16, + out_channels=None, + embed_dim=embed_dim, + pooling_func=pooling_func, + global_context_att=True, + two_emb_layer=two_emb_layer, + ) + + +def ReDimNetB6(feat_dim=72, + embed_dim=192, + pooling_func="ASTP", + two_emb_layer=False): + return ReDimNet( + feat_dim=feat_dim, + C=32, + block_1d_type="conv+att", + block_2d_type="basic_resnet", + stages_setup=[ + (1, 4, 4, [(3, 3)], 32), + (2, 6, 2, [(3, 3)], 32), + (1, 6, 2, [(3, 3)], 24), + (3, 8, 1, [(3, 3)], 24), + (1, 8, 1, [(3, 3)], 16), + (2, 8, 1, [(3, 3)], 16), + ], + group_divisor=32, + out_channels=None, + embed_dim=embed_dim, + pooling_func=pooling_func, + global_context_att=True, + two_emb_layer=two_emb_layer, + ) + + +if __name__ == "__main__": + x = torch.zeros(1, 200, 72) + model = ReDimNet(feat_dim=72, embed_dim=192, two_emb_layer=False) + model.eval() + out = model(x) + print(out[-1].size()) + + num_params = sum(p.numel() for p in model.parameters()) + print("{} M".format(num_params / 1e6)) + + # Currently, the model sizes differ from the ones in the paper + model_classes = [ + ReDimNetB0, # 1.0M v.s. 1.0M + ReDimNetB1, # 2.1M v.s. 2.2M + ReDimNetB2, # 4.9M v.s. 4.7M + ReDimNetB3, # 3.2M v.s. 3.0M + ReDimNetB4, # 6.4M v.s. 6.3M + ReDimNetB5, # 7.65M v.s. 9.2M + ReDimNetB6, # 15.0M v.s. 15.0M + ] + + for i, model_class in enumerate(model_classes): + model = model_class() + num_params = sum(p.numel() for p in model.parameters()) + print("{} M of Model B{}".format(num_params / 1e6, i)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/redimnet2.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/redimnet2.py new file mode 100644 index 0000000000000000000000000000000000000000..49b63237f0bfbab93233bcf76c7b94d3f6f8ac58 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/redimnet2.py @@ -0,0 +1,1080 @@ +# Copyright (c) 2026 Bosen Xu (2332974001@qq.com) +# Based on original code from PalabraAI/redimnet2 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import torch +import torch.nn as nn +import torch.nn.functional as F +import functools +import numpy as np +import wespeaker.models.pooling_layers as pooling_layers +from wespeaker.frontend.tfmel import (TFMelBanks, TFSpectrogram, + NormalizeAudio, PreEmphasis, FbankAug) + + +def ShapeLogger(x): + return x + + +class FreqEncoder(nn.Module): + def __init__(self, c, bins): + super().__init__() + self.freq_embedder = nn.Embedding( + num_embeddings=bins, + embedding_dim=c) + + def forward(self, x): + b, c, f, t = x.size() + freqs = torch.range(start=0, end=f - 1, step=1, dtype=torch.long) + freqs = freqs.unsqueeze(0).repeat(b, 1).to(x.device) # [bs,f] + fe = self.freq_embedder(freqs).permute( + 0, 2, 1).unsqueeze(-1) # [bs, freq_emb_dim, f, 1] + fe = fe.repeat(1, 1, 1, t) + x = x + fe + return x + + +class LayerNorm(nn.Module): + def __init__(self, C, eps=1e-6, data_format="channels_last"): + super().__init__() + self.weight = nn.Parameter(torch.ones(C)) + self.bias = nn.Parameter(torch.zeros(C)) + self.eps = eps + self.data_format = data_format + if self.data_format not in ["channels_last", "channels_first"]: + raise NotImplementedError + self.C = (C, ) + + def forward(self, x): + if self.data_format == "channels_last": + return F.layer_norm(x, self.C, self.weight, self.bias, self.eps) + elif self.data_format == "channels_first": + u = x.mean(1, keepdim=True) + s = (x - u).pow(2).mean(1, keepdim=True) + x = (x - u) / torch.sqrt(s + self.eps) + w = self.weight + b = self.bias + for _ in range(x.ndim - 2): + w = w.unsqueeze(-1) + b = b.unsqueeze(-1) + x = w * x + b + return x + + +class fwSEBlock(nn.Module): + def __init__(self, num_freq, num_feats=64): + super(fwSEBlock, self).__init__() + self.squeeze = nn.Linear(num_freq, num_feats) + self.exitation = nn.Linear(num_feats, num_freq) + self.activation = nn.ReLU() + + def forward(self, inputs): + x = torch.mean(inputs, dim=[1, 3]) + x = self.squeeze(x) + x = self.activation(x) + x = self.exitation(x) + x = torch.sigmoid(x) + x = x[:, None, :, None] + x = inputs * x + return x + + +class ResBasicBlock(nn.Module): + def __init__(self, inc, outc, num_freq, stride=1, se_channels=64, + Gdiv=4, use_fwSE=False): + super().__init__() + self.conv1 = nn.Conv2d( + inc, + inc if Gdiv is not None else outc, + kernel_size=3, + stride=stride, + padding=1, + bias=False, + groups=inc // + Gdiv if Gdiv is not None else 1) + if Gdiv is not None: + self.conv1pw = nn.Conv2d(inc, outc, 1) + else: + self.conv1pw = nn.Identity() + self.bn1 = nn.BatchNorm2d(outc) + self.conv2 = nn.Conv2d(outc, outc, kernel_size=3, padding=1, bias=False, + groups=outc // Gdiv if Gdiv is not None else 1) + if Gdiv is not None: + self.conv2pw = nn.Conv2d(outc, outc, 1) + else: + self.conv2pw = nn.Identity() + self.bn2 = nn.BatchNorm2d(outc) + self.relu = nn.ReLU(inplace=True) + if use_fwSE: + self.se = fwSEBlock(num_freq, se_channels) + else: + self.se = nn.Identity() + if outc != inc: + self.downsample = nn.Sequential( + nn.Conv2d(inc, outc, kernel_size=1, stride=stride, bias=False), + nn.BatchNorm2d(outc), + ) + else: + self.downsample = nn.Identity() + + def forward(self, x): + residual = x + out = self.conv1pw(self.conv1(x)) + out = self.relu(out) + out = self.bn1(out) + out = self.conv2pw(self.conv2(out)) + out = self.bn2(out) + out = self.se(out) + out += self.downsample(residual) + out = self.relu(out) + return out + + +ConvNd = {1: nn.Conv1d, 2: nn.Conv2d} +BatchNormNd = {1: nn.BatchNorm1d, 2: nn.BatchNorm2d} + + +class ConvNeXtLikeBlock(nn.Module): + def __init__(self, C, dim=2, kernel_sizes=None, Gdiv=1, padding='same', + activation='gelu'): + super().__init__() + if kernel_sizes is None: + kernel_sizes = [(3, 3)] + self.dwconvs = nn.ModuleList( + modules=[ + ConvNd[dim]( + C, + C, + kernel_size=ks, + padding=padding, + groups=C // + Gdiv if Gdiv is not None else 1) for ks in kernel_sizes]) + self.norm = BatchNormNd[dim](C * len(kernel_sizes)) + if activation == 'gelu': + self.act = nn.GELU() + elif activation == 'relu': + self.act = nn.ReLU() + self.pwconv1 = ConvNd[dim](C * len(kernel_sizes), C, 1) + + def forward(self, x): + skip = x + x = torch.cat([dwconv(x) for dwconv in self.dwconvs], dim=1) + x = self.act(self.norm(x)) + x = self.pwconv1(x) + x = skip + x + return x + + +class MultiHeadAttention(nn.Module): + def __init__(self, embed_dim: int, num_heads: int, dropout: float = 0.0, + bias: bool = True): + super().__init__() + self.embed_dim = embed_dim + self.num_heads = num_heads + self.dropout = dropout + self.head_dim = embed_dim // num_heads + if (self.head_dim * num_heads) != self.embed_dim: + raise ValueError("embed_dim must be divisible by num_heads") + self.scaling = self.head_dim**-0.5 + self.k_proj = nn.Linear(embed_dim, embed_dim, bias=bias) + self.v_proj = nn.Linear(embed_dim, embed_dim, bias=bias) + self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias) + self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias) + + def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int): + return tensor.view( + bsz, seq_len, self.num_heads, self.head_dim).transpose( + 1, 2).contiguous() + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + bsz, tgt_len, _ = hidden_states.size() + query_states = self.q_proj(hidden_states) * self.scaling + key_states = self._shape(self.k_proj(hidden_states), -1, bsz) + value_states = self._shape(self.v_proj(hidden_states), -1, bsz) + proj_shape = (bsz * self.num_heads, -1, self.head_dim) + query_states = self._shape(query_states, tgt_len, bsz).view(*proj_shape) + key_states = key_states.view(*proj_shape) + value_states = value_states.view(*proj_shape) + src_len = key_states.size(1) + attn_weights = torch.bmm(query_states, key_states.transpose(1, 2)) + attn_weights = F.softmax(attn_weights, dim=-1) + attn_probs = F.dropout(attn_weights, p=self.dropout, training=self.training) + attn_output = torch.bmm(attn_probs, value_states) + attn_output = attn_output.view(bsz, self.num_heads, tgt_len, self.head_dim) + attn_output = attn_output.transpose(1, 2) + attn_output = attn_output.reshape(bsz, tgt_len, self.embed_dim) + attn_output = self.out_proj(attn_output) + return attn_output + + +class NewGELUActivation(nn.Module): + def forward(self, input: torch.Tensor) -> torch.Tensor: + return 0.5 * input * (1.0 + torch.tanh( + math.sqrt(2.0 / math.pi) * (input + 0.044715 * torch.pow(input, 3.0)))) + + +class GELUActivation(nn.Module): + def __init__(self, use_gelu_python: bool = False): + super().__init__() + self.act = F.gelu if not use_gelu_python else self._gelu_python + + def _gelu_python(self, input: torch.Tensor) -> torch.Tensor: + return input * 0.5 * (1.0 + torch.erf(input / math.sqrt(2.0))) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + return self.act(input) + + +class FastGELUActivation(nn.Module): + def forward(self, input: torch.Tensor) -> torch.Tensor: + return 0.5 * input * (1.0 + torch.tanh( + input * 0.7978845608 * (1.0 + 0.044715 * input * input))) + + +class QuickGELUActivation(nn.Module): + def forward(self, input: torch.Tensor) -> torch.Tensor: + return input * torch.sigmoid(1.702 * input) + + +ACT2CLS = { + "gelu": GELUActivation, + "gelu_new": NewGELUActivation, + "gelu_fast": FastGELUActivation, + "linear": nn.Identity, + "relu": nn.ReLU, + "silu": nn.SiLU, +} +ACT2FN = {k: v() if isinstance(v, type) else v for k, v in ACT2CLS.items()} + + +class FeedForward(nn.Module): + def __init__( + self, + hidden_size: int, + intermediate_size: int, + hidden_act: str = 'gelu_new', + activation_dropout: float = 0.0, + hidden_dropout: float = 0.0): + super().__init__() + self.intermediate_dropout = nn.Dropout(activation_dropout) + self.intermediate_dense = nn.Linear(hidden_size, intermediate_size) + self.intermediate_act_fn = ACT2FN.get(hidden_act, GELUActivation()) + self.output_dense = nn.Linear(intermediate_size, hidden_size) + self.output_dropout = nn.Dropout(hidden_dropout) + + def forward(self, hidden_states): + hidden_states = self.intermediate_dense(hidden_states) + hidden_states = self.intermediate_act_fn(hidden_states) + hidden_states = self.intermediate_dropout(hidden_states) + hidden_states = self.output_dense(hidden_states) + hidden_states = self.output_dropout(hidden_states) + return hidden_states + + +class TransformerEncoderLayer(nn.Module): + def __init__( + self, + n_state: int, + n_mlp: int, + n_head: int, + channel_last: bool = False, + act: str = 'gelu_new', + act_do: float = 0.0, + att_do: float = 0.0, + hid_do: float = 0.0, + ln_eps: float = 1e-6): + super().__init__() + self.channel_last = channel_last + self.attention = MultiHeadAttention( + embed_dim=n_state, num_heads=n_head, dropout=att_do) + self.layer_norm = nn.LayerNorm(n_state, eps=ln_eps) + self.feed_forward = FeedForward( + hidden_size=n_state, + hidden_act=act, + intermediate_size=n_mlp, + activation_dropout=act_do, + hidden_dropout=hid_do) + self.final_layer_norm = nn.LayerNorm(n_state, eps=ln_eps) + + def forward(self, hidden_states): + if not self.channel_last: + hidden_states = hidden_states.permute(0, 2, 1) + attn_residual = hidden_states + hidden_states = self.attention(hidden_states) + hidden_states = attn_residual + hidden_states + hidden_states = self.layer_norm(hidden_states) + hidden_states = hidden_states + self.feed_forward(hidden_states) + hidden_states = self.final_layer_norm(hidden_states) + outputs = hidden_states + if not self.channel_last: + outputs = outputs.permute(0, 2, 1) + return outputs + + +class MelBanks(nn.Module): + def __init__( + self, + sample_rate=16000, + n_fft=512, + win_length=400, + hop_length=160, + f_min=20, + f_max=7600, + n_mels=80, + do_spec_aug=False, + norm_signal=False, + do_preemph=True, + spec_norm='mn', + freq_start_bin=0, + num_apply_spec_aug=1, + freq_mask_width=( + 0, + 8), + time_mask_width=( + 0, + 10)): + super(MelBanks, self).__init__() + self.num_apply_spec_aug = num_apply_spec_aug + import torchaudio + self.torchfbank = torch.nn.Sequential( + NormalizeAudio() if norm_signal else nn.Identity(), + PreEmphasis() if do_preemph else nn.Identity(), + torchaudio.transforms.MelSpectrogram( + sample_rate=sample_rate, + n_fft=n_fft, + win_length=win_length, + hop_length=hop_length, + f_min=f_min, + f_max=f_max, + n_mels=n_mels, + window_fn=torch.hamming_window), + ) + self.spec_norm = spec_norm + if spec_norm == 'mn': + self.spec_norm = lambda x: x - torch.mean(x, dim=-1, keepdim=True) + elif spec_norm == 'mvn': + self.spec_norm = lambda x: (x - torch.mean(x, dim=-1, keepdims=True)) / \ + (torch.std(x, dim=-1, keepdim=True) + 1e-8) + elif spec_norm == 'bn': + self.spec_norm = nn.BatchNorm1d(n_mels) + else: + self.spec_norm = lambda x: x + if do_spec_aug: + self.specaug = FbankAug( + freq_start_bin=freq_start_bin, + freq_mask_width=freq_mask_width, + time_mask_width=time_mask_width) + else: + self.specaug = nn.Identity() + + def forward(self, x): + xdtype = x.dtype + x = x.float() + with torch.no_grad(), torch.amp.autocast('cuda', enabled=False): + x = self.torchfbank(x) + 1e-6 + x = x.log() + x = self.spec_norm(x) + if self.training: + for _ in range(self.num_apply_spec_aug): + x = self.specaug(x) + return x.to(xdtype) + + +class to1d(nn.Module): + def forward(self, x: torch.Tensor) -> torch.Tensor: + size = x.size() + bs, c, f, t = size + return x.permute((0, 2, 1, 3)).reshape((bs, c * f, t)) + + +class to2d(nn.Module): + def __init__(self, f, c): + super().__init__() + self.f = f + self.c = c + + def forward(self, x): + bs, cf, t = x.size() + out = x.reshape((bs, self.f, self.c, t)).permute((0, 2, 1, 3)) + return out + + +class weigth1d(nn.Module): + def __init__(self, N, C, sequential=False, requires_grad=True): + super().__init__() + self.N = N + self.sequential = sequential + self.w = nn.Parameter(torch.zeros(1, N, C, 1), requires_grad=requires_grad) + + def forward(self, xs): + w = F.softmax(self.w, dim=1) + if not self.sequential: + xs = torch.cat([t.unsqueeze(1) for t in xs], dim=1) + x = (w * xs).sum(dim=1) + else: + s = torch.zeros_like(xs[0]) + for i, t in enumerate(xs): + s += t * w[:, i, :, :] + x = s + return x + + +class ConvBlock2d(nn.Module): + def __init__(self, c, f, block_type="convnext_like", Gdiv=1, kernel_sizes=None): + super().__init__() + if kernel_sizes is None: + kernel_sizes = [(3, 3)] + if block_type == "convnext_like": + self.conv_block = ConvNeXtLikeBlock( + c, dim=2, kernel_sizes=kernel_sizes, Gdiv=Gdiv, + padding='same', activation='gelu') + elif block_type == "convnext_like_relu": + self.conv_block = ConvNeXtLikeBlock( + c, dim=2, kernel_sizes=kernel_sizes, Gdiv=Gdiv, + padding='same', activation='relu') + elif block_type == "basic_resnet": + self.conv_block = ResBasicBlock( + c, c, f, stride=1, se_channels=min( + 64, max( + c, 32)), Gdiv=Gdiv, use_fwSE=False) + elif block_type == "basic_resnet_fwse": + self.conv_block = ResBasicBlock( + c, c, f, stride=1, se_channels=min( + 64, max( + c, 32)), Gdiv=Gdiv, use_fwSE=True) + else: + raise NotImplementedError() + + def forward(self, x): + return self.conv_block(x) + + +class PosEncConv(nn.Module): + def __init__(self, C, ks, groups=None): + super().__init__() + assert ks % 2 == 1 + self.conv = nn.Conv1d( + C, C, ks, padding=ks // 2, + groups=C if groups is None else groups) + self.norm = LayerNorm(C, eps=1e-6, data_format="channels_first") + + def forward(self, x): + return x + self.norm(self.conv(x)) + + +class TimeContextBlock1d(nn.Module): + def __init__(self, C, hC, pos_ker_sz=59, block_type='att', + red_dim_conv=None, exp_dim_conv=None): + super().__init__() + assert pos_ker_sz + self.red_dim_conv = nn.Sequential( + nn.Conv1d(C, hC, 1), + LayerNorm(hC, eps=1e-6, data_format="channels_first") + ) + if block_type == 'fc': + self.tcm = nn.Sequential( + nn.Conv1d(hC, hC * 2, 1), + LayerNorm(hC * 2, eps=1e-6, data_format="channels_first"), + nn.GELU(), + nn.Conv1d(hC * 2, hC, 1) + ) + elif block_type == 'conv': + self.tcm = nn.Sequential( + *[ConvNeXtLikeBlock( + hC, dim=1, kernel_sizes=[7, 15, 31], + Gdiv=1, padding='same') for i in range(4)]) + elif block_type == 'att': + self.tcm = nn.Sequential( + PosEncConv(hC, ks=pos_ker_sz, groups=hC), + TransformerEncoderLayer(n_state=hC, n_mlp=hC * 2, n_head=4) + ) + elif block_type == 'conv+att': + self.tcm = nn.Sequential( + ConvNeXtLikeBlock(hC, dim=1, kernel_sizes=[7], Gdiv=1, padding='same'), + ConvNeXtLikeBlock(hC, dim=1, kernel_sizes=[19], Gdiv=1, padding='same'), + ConvNeXtLikeBlock(hC, dim=1, kernel_sizes=[31], Gdiv=1, padding='same'), + ConvNeXtLikeBlock(hC, dim=1, kernel_sizes=[59], Gdiv=1, padding='same'), + TransformerEncoderLayer(n_state=hC, n_mlp=hC, n_head=4) + ) + else: + raise NotImplementedError() + self.exp_dim_conv = nn.Conv1d(hC, C, 1) + + def forward(self, x): + skip = x + x = self.red_dim_conv(x) + x = self.tcm(x) + x = self.exp_dim_conv(x) + return skip + x + + +class ReDimNet2(nn.Module): + # UNet-like ReDimNet + def __init__(self, + F=72, + C=24, + spec_in_channels=1, # Phase + Magnitude + causal='none', + out_channels=None, + block_1d_type='tf-att', + block_2d_type="basic_resnet", + return_2d_output=False, + fm_weigthing_type='NC', + use_freq_pos_enc=False, + compress_tconvs=True, + stages_setup=None, + group_divisor=1, + dual_agg=False, + agg_gnorm=False, + # Subnet stuff + return_all_outputs=False, + offset_fm_weights=0, + is_subnet=False, + ): + super().__init__() + if stages_setup is None: + stages_setup = [ + # Encoder part: + ((1, 1), 2, 4, [(3, 3)], None), # 16 + ((2, 1), 3, 3, [(3, 3)], None), # 32 + + ((1, 2), 4, 2, [(3, 3)], None), # 64, + ((2, 1), 5, 1, [(3, 3)], 48), # 128 + + ((1, 2), 4, 1, [(3, 3)], 64), # 128 + ((2, 1), 3, 1, [(3, 3)], 96), # 128 + ] + self.F = F + self.C = C + + if causal == 'full': + block_1d_type = block_1d_type + '-causal' + block_2d_type = block_2d_type + '-causal' + self.causal = True + elif causal == 'only_1d': + block_1d_type = block_1d_type + '-causal' + self.causal = True + elif causal == 'none': + self.causal = False + else: + raise NotImplementedError() + + self.block_1d_type = block_1d_type + self.block_2d_type = block_2d_type + + self.stages_setup = stages_setup + self.fm_weigthing_type = fm_weigthing_type + self.dual_agg = dual_agg + self.agg_gnorm = agg_gnorm + + # Subnet stuff + self.is_subnet = is_subnet + self.offset_fm_weights = offset_fm_weights + self.return_all_outputs = return_all_outputs + + self.build(F, C, spec_in_channels, out_channels, stages_setup, group_divisor, + compress_tconvs, return_2d_output, use_freq_pos_enc) + + def build(self, F, C, spec_in_channels, out_channels, stages_setup, group_divisor, + compress_tconvs, return_2d_output, use_freq_pos_enc): + self.F = F + self.C = C + + c = C + f = F + + stt = 1 + sft = 1 + + max_stt = stt + + self.num_stages = len(stages_setup) + + append_to1d_before_tcm = True + Block1d = functools.partial( + TimeContextBlock1d, block_type=self.block_1d_type) + Block2d = functools.partial( + ConvBlock2d, block_type=self.block_2d_type) + + if self.fm_weigthing_type == 'NC': + agg1d = functools.partial(weigth1d, C=F * C) + elif self.fm_weigthing_type == 'N': + agg1d = functools.partial(weigth1d, C=None) + else: + raise NotImplementedError() + + if not self.is_subnet: + self.stem = nn.Sequential( + nn.Conv2d( + spec_in_channels, int(c), kernel_size=3, + stride=1, padding='same'), + LayerNorm(int(c), eps=1e-6, data_format="channels_first"), + to1d() + ) + else: + # Subnet stem: aggregate offset_fm_weights incoming 1D feature maps, + # reshape to 2D, then apply a standard conv+norm stem before to1d(). + assert self.offset_fm_weights > 0, \ + "offset_fm_weights must be > 0 when is_subnet=True" + self.stem = nn.Sequential( + agg1d( + N=self.offset_fm_weights, + requires_grad=self.offset_fm_weights > 1), + to2d(f=F, c=C), + nn.Conv2d( + int(c), int(c), kernel_size=3, + stride=1, padding='same'), + LayerNorm(int(c), eps=1e-6, data_format="channels_first"), + to1d() + ) + + if self.agg_gnorm: + self.stem_gnorm = nn.GroupNorm(num_groups=C, num_channels=C * F) + + # Track accumulated feature-map count for the weight1d N parameter. + # Starts at offset_fm_weights+1 to account for the + # subnet offset + stem output. + feat_count = self.offset_fm_weights + 1 + self._stage_has_dual = [] + + for stage_ind, (stride, num_blocks, conv_exp, kernel_sizes, + att_block_red) in enumerate(stages_setup): + (sf, st) = stride + tot_stride = np.prod((sf, st)) + num_feats_to_weight = feat_count + # if tot_stride > 1: + layers = [] + sft = sft * sf + stt = stt * st + layers.append( + agg1d( + N=num_feats_to_weight, + requires_grad=num_feats_to_weight > 1)) + layers.append(to2d(f=f, c=c)) + if use_freq_pos_enc: + layers.append(FreqEncoder(c=c, bins=f)) + + layers.append(ShapeLogger(nn.Conv2d( + int(c), int(sf * c * conv_exp), + kernel_size=(sf, stt), + stride=(sf, stt), + padding=0, + groups=1 if not compress_tconvs else + math.gcd(int(c), int(sf * c * conv_exp))))) + + c = sf * c + assert f % sf == 0 + f = f // sf + + if stt >= max_stt: + max_stt = stt + + for block_ind in range(num_blocks): + layers.append( + Block2d(c=int(c * conv_exp), f=f, + kernel_sizes=kernel_sizes, Gdiv=group_divisor)) + + if conv_exp != 1: + _group_divisor = group_divisor + layers.append(nn.Sequential( + nn.Conv2d( + int(c * conv_exp), c, kernel_size=1, + stride=1, padding='same'), + nn.BatchNorm2d(c, eps=1e-6) + )) + + has_dual = self.dual_agg and att_block_red is not None + + if has_dual: + # Split the stage so the 1D-attention branch runs in + # parallel with a plain 2D->1D reshape branch; both are + # upsampled (+gnorm) and aggregated alongside prior + # feature maps. + if append_to1d_before_tcm: + layers.append(to1d()) + setattr(self, f'stage{stage_ind}_pre', nn.Sequential(*layers)) + + blk_1d = Block1d(C * F, hC=(C * F) // att_block_red) + setattr(self, f'stage{stage_ind}_1d', blk_1d) + + up_2d = [ShapeLogger(nn.Upsample(scale_factor=stt, mode='nearest'))] + if self.agg_gnorm: + up_2d.append(nn.GroupNorm(num_groups=C, num_channels=C * F)) + setattr(self, f'stage{stage_ind}_up_2d', nn.Sequential(*up_2d)) + + up_1d = [ShapeLogger(nn.Upsample(scale_factor=stt, mode='nearest'))] + if self.agg_gnorm: + up_1d.append(nn.GroupNorm(num_groups=C, num_channels=C * F)) + setattr(self, f'stage{stage_ind}_up_1d', nn.Sequential(*up_1d)) + + self._stage_has_dual.append(True) + feat_count += 2 + else: + if append_to1d_before_tcm: + layers.append(to1d()) + if att_block_red is not None: + if append_to1d_before_tcm: + layers.append(Block1d(C * F, hC=(C * F) // att_block_red)) + else: + layers.append(Block1d(C=c, F=f, hC=att_block_red)) + if not append_to1d_before_tcm: + layers.append(to1d()) + layers.append( + ShapeLogger(nn.Upsample(scale_factor=stt, mode='nearest'))) + if self.agg_gnorm: + layers.append(nn.GroupNorm(num_groups=C, num_channels=C * F)) + setattr(self, f'stage{stage_ind}', nn.Sequential(*layers)) + + self._stage_has_dual.append(False) + feat_count += 1 + + self.fin_wght1d = agg1d(N=feat_count, requires_grad=feat_count > 1) + + self.time_stride = max_stt + self.freq_stride = sft + self.head = nn.Identity() + print(f"out_channels : {out_channels}") + if return_2d_output: + self.fin_to2d = to2d(f=f, c=c) + if out_channels is not None: + self.head = nn.Conv2d(c, out_channels, 1) + else: + self.fin_to2d = nn.Identity() + if out_channels is not None: + self.head = nn.Conv1d(C * F, out_channels, 1) + + def run_stage(self, prev_outs_1d, stage_ind): + if self._stage_has_dual[stage_ind]: + pre = getattr(self, f'stage{stage_ind}_pre') + blk_1d = getattr(self, f'stage{stage_ind}_1d') + up_2d = getattr(self, f'stage{stage_ind}_up_2d') + up_1d = getattr(self, f'stage{stage_ind}_up_1d') + x_pre = pre(prev_outs_1d) + x_2d = up_2d(x_pre) + x_1d = up_1d(blk_1d(x_pre)) + return [x_2d, x_1d] + stage = getattr(self, f'stage{stage_ind}') + return [stage(prev_outs_1d)] + + def forward(self, inp): + if not self.is_subnet: + bs, _, _, T = inp.size() + inp = inp[:, :, :, :(T // self.time_stride) * self.time_stride] + # Needed for right reshape operations + x = self.stem(inp) + if self.agg_gnorm: + x = self.stem_gnorm(x) + outputs_1d = [x] + else: + assert isinstance(inp, list), \ + "Subnet-mode ReDimNet2 expects a list of 1D feature maps as input" + outputs_1d = list(inp) + x = self.stem(inp) + if self.agg_gnorm: + x = self.stem_gnorm(x) + outputs_1d.append(x) + + for stage_ind in range(self.num_stages): + outputs_1d.extend(self.run_stage(outputs_1d, stage_ind)) + x = self.fin_wght1d(outputs_1d) + outputs_1d.append(x) + x = self.fin_to2d(x) + x = self.head(x) + + if self.return_all_outputs: + return x, outputs_1d + return x + + +class ReDimNet2Wrap(nn.Module): + def __init__(self, + F=72, + C=24, + feat_dim=None, + embed_dim=192, + pooling_func="ASTP", + two_emb_layer=False, + causal='none', + spec='fbank', + spec_in_channels=1, # Phase + Magnitude + out_channels=None, + block_1d_type='conv+att', + block_2d_type="basic_resnet", + compress_tconvs=True, + return_2d_output=False, + use_freq_pos_enc=False, + fm_weigthing_type='NC', + stages_setup=None, + group_divisor=1, + dual_agg=False, + agg_gnorm=False, + num_classes=None, + feat_agg_dropout=0.0, + head_activation=None, + hop_length=160, + pad_right_samples=None, + before_pool_offset=None, + feat_type='pt', + global_context_att=True, + emb_bn=False, + spec_params=None, + return_all_outputs=False, + ): + super().__init__() + + if stages_setup is None: + stages_setup = [ + # Encoder part: + ((1, 1), 2, 4, [(3, 3)], 24), # 16 + ((2, 1), 3, 3, [(3, 3)], 24), # 32 + + ((1, 2), 4, 2, [(3, 3)], 24), # 64, + ((2, 1), 5, 1, [(3, 3)], 24), # 128 + + ((1, 2), 4, 1, [(3, 3)], 24), # 128 + ((2, 1), 3, 1, [(3, 3)], 24), # 128 + ] + + if spec_params is None: + spec_params = dict( + do_spec_aug=False, + freq_mask_width=(0, 6), + time_mask_width=(0, 8), + ) + + if feat_dim is not None: + F = feat_dim + + self.return_all_outputs = return_all_outputs + + self.backbone = ReDimNet2( + F=F, C=C, + causal=causal, + spec_in_channels=spec_in_channels, # Phase + Magnitude + out_channels=out_channels, + return_2d_output=return_2d_output, + block_1d_type=block_1d_type, + block_2d_type=block_2d_type, + compress_tconvs=compress_tconvs, + fm_weigthing_type=fm_weigthing_type, + use_freq_pos_enc=use_freq_pos_enc, + stages_setup=stages_setup, + group_divisor=group_divisor, + dual_agg=dual_agg, + agg_gnorm=agg_gnorm, + return_all_outputs=return_all_outputs, + ) + if spec is None or spec == 'fbank': + self.spec = None + elif spec == 'pt': + self.spec = MelBanks(n_mels=F, hop_length=hop_length, **spec_params) + elif spec == 'tf': + self.spec = TFMelBanks(n_mels=F, hop_length=hop_length, **spec_params) + elif spec == 'tf_spec': + self.spec = TFSpectrogram(**spec_params) + elif spec == 'pt_stft': + self.spec = None # STFT not implemented + + if out_channels is None: + out_channels = C * F + else: + if return_2d_output: + out_channels = (F // self.backbone.freq_stride) * out_channels + else: + out_channels = out_channels + + self.pool = getattr(pooling_layers, pooling_func)( + in_dim=out_channels, global_context_att=global_context_att) + + self.pad_right_samples = pad_right_samples + self.before_pool_offset = before_pool_offset + self.pool_out_dim = self.pool.get_out_dim() + self.bn = nn.BatchNorm1d(self.pool_out_dim) + self.linear = nn.Linear(self.pool_out_dim, embed_dim) + self.embed_dim = embed_dim + self.emb_bn = emb_bn + if emb_bn: # better in SSL for SV + self.bn2 = nn.BatchNorm1d(embed_dim) + else: + self.bn2 = None + + def forward(self, x): + if self.pad_right_samples is not None: + x = torch.nn.functional.pad( + x, (0, self.pad_right_samples), + mode='constant', value=None) + if self.spec is not None and self.spec != 'fbank': + x = self.spec(x) + + if x.ndim == 3: + x = x.unsqueeze(1) + if self.return_all_outputs: + out, all_outs_1d = self.backbone(x) + else: + out = self.backbone(x) + # print(f"pre pool : {out.size()}") + if out.ndim == 4: + bs, C, F, T = out.size() + out = out.reshape(bs, C * F, T) + if self.before_pool_offset is not None: + out = out[:, :, self.before_pool_offset:] + out = self.bn(self.pool(out)) + out = self.linear(out) + + if self.bn2 is not None: + out = self.bn2(out) + + if self.return_all_outputs: + return out, all_outs_1d + return out + + def prepare_for_frontend(self, frontend_type): + if frontend_type == 'tfmel' and self.spec is not None and \ + self.spec != 'fbank': + print( + f"ReDimNet2Wrap: Disabling internal spec ({self.spec}) " + f"for external {frontend_type} frontend") + self.spec = None + + +def ReDimNet2Custom(**kwargs): + return ReDimNet2Wrap(**kwargs) + + +def ReDimNet2B0(C=12, out_channels=64, stages_setup=None, **kwargs): + if stages_setup is None: + stages_setup = [ + [[1, 1], 2, 2, [[3, 3]], 36], + [[2, 1], 3, 1, [[3, 3]], 36], + [[1, 2], 4, 1, [[3, 3]], 36], + [[2, 1], 5, 1, [[3, 3]], 36], + [[1, 2], 4, 1, [[3, 3]], 18], + [[2, 1], 3, 1, [[3, 3]], 18], + ] + return ReDimNet2Wrap( + C=C, + out_channels=out_channels, + stages_setup=stages_setup, + **kwargs, + ) + + +def ReDimNet2B1(C=16, out_channels=64, stages_setup=None, **kwargs): + if stages_setup is None: + stages_setup = [ + [[1, 1], 2, 2, [[3, 3]], 32], + [[2, 1], 3, 1, [[3, 3]], 32], + [[1, 2], 4, 1, [[3, 3]], 32], + [[2, 1], 5, 1, [[3, 3]], 32], + [[1, 2], 4, 1, [[3, 3]], 16], + [[2, 1], 3, 1, [[3, 3]], 16], + ] + return ReDimNet2Wrap( + C=C, + out_channels=out_channels, + stages_setup=stages_setup, + **kwargs, + ) + + +def ReDimNet2B2(C=20, out_channels=64, stages_setup=None, **kwargs): + if stages_setup is None: + stages_setup = [ + [[1, 1], 2, 2, [[3, 5]], 40], + [[2, 1], 3, 1, [[3, 5]], 30], + [[1, 2], 4, 1, [[3, 5]], 30], + [[3, 1], 5, 1, [[3, 5]], 20], + [[1, 2], 4, 1, [[3, 7]], 20], + [[2, 1], 3, 1, [[3, 7]], 10], + ] + return ReDimNet2Wrap( + C=C, + out_channels=out_channels, + stages_setup=stages_setup, + **kwargs, + ) + + +def ReDimNet2B3(C=24, out_channels=64, stages_setup=None, **kwargs): + if stages_setup is None: + stages_setup = [ + [[1, 1], 2, 2, [[3, 3]], 36], + [[2, 1], 3, 1, [[3, 3]], 36], + [[1, 2], 4, 1, [[3, 3]], 36], + [[2, 1], 5, 1, [[3, 3]], 36], + [[1, 2], 4, 1, [[3, 3]], 18], + [[2, 1], 3, 1, [[3, 3]], 18], + ] + return ReDimNet2Wrap( + C=C, + out_channels=out_channels, + stages_setup=stages_setup, + **kwargs, + ) + + +def ReDimNet2B4(C=32, stages_setup=None, **kwargs): + if stages_setup is None: + stages_setup = [ + [[1, 1], 2, 4, [[3, 3]], 24], + [[2, 1], 3, 3, [[3, 3]], 24], + [[1, 2], 4, 2, [[3, 3]], 24], + [[2, 1], 5, 1, [[3, 3]], 24], + [[1, 2], 4, 1, [[3, 3]], 24], + [[2, 1], 3, 1, [[3, 3]], 24], + ] + return ReDimNet2Wrap( + C=C, + stages_setup=stages_setup, + **kwargs, + ) + + +def ReDimNet2B5(C=48, out_channels=256, stages_setup=None, **kwargs): + if stages_setup is None: + stages_setup = [ + [[1, 1], 2, 4, [[3, 3]], 48], + [[2, 1], 3, 3, [[3, 3]], 48], + [[1, 2], 4, 2, [[3, 3]], 48], + [[2, 1], 5, 1, [[3, 3]], 48], + [[1, 2], 4, 1, [[3, 3]], 32], + [[2, 1], 3, 1, [[3, 3]], 32], + ] + return ReDimNet2Wrap( + C=C, + out_channels=out_channels, + stages_setup=stages_setup, + **kwargs, + ) + + +def ReDimNet2B6(C=64, out_channels=224, return_2d_output=True, + stages_setup=None, **kwargs): + if stages_setup is None: + stages_setup = [ + [[1, 1], 3, 3, [[3, 3]], 64], + [[2, 1], 4, 2, [[3, 3]], 64], + [[1, 2], 5, 2, [[3, 3]], 48], + [[2, 1], 5, 1, [[3, 3]], 48], + [[1, 2], 4, 0.75, [[3, 3]], 32], + [[2, 1], 3, 0.5, [[3, 3]], 24], + ] + return ReDimNet2Wrap( + C=C, + out_channels=out_channels, + return_2d_output=return_2d_output, + stages_setup=stages_setup, + **kwargs, + ) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/repvgg.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/repvgg.py new file mode 100644 index 0000000000000000000000000000000000000000..b5e3298fa29bff68afcd05cbf552a782c488be94 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/repvgg.py @@ -0,0 +1,927 @@ +# Copyright (c) 2021 xmuspeech (Author: Leo) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Some modifications from the original architecture: +REPVGG_TINY_A0: Smaller hidden_dim and Deeper structure + +Ref: +1. RepVGG: Making VGG-style ConvNets Great Again + (https://arxiv.org/pdf/2101.03697) + Github: https://github.com/DingXiaoH/RepVGG +2. Rep Works in Speaker Verification (https://arxiv.org/pdf/2110.09720) +3. asv-subtools: + https://github.com/Snowdar/asv-subtools/blob/master/pytorch/libs/nnet/repvgg.py +""" + +import torch.nn as nn +import numpy as np +import torch +import copy +import wespeaker.models.pooling_layers as pooling_layers + +optional_groupwise_layers = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26] +g2_map = dict.fromkeys(optional_groupwise_layers, 2) +g4_map = dict.fromkeys(optional_groupwise_layers, 4) + + +class SEBlock_2D(torch.nn.Module): + """ A SE Block layer layer which can learn to use global information to + selectively emphasise informative features and suppress less useful ones. + This is a pytorch implementation of SE Block based on the paper: + Squeeze-and-Excitation Networks + by JFChou xmuspeech 2019-07-13 + leo 2020-12-20 [Check and update] + """ + + def __init__(self, in_planes, ratio=16, inplace=True): + ''' + @ratio: a reduction ratio which allows us to vary the capacity + and computational cost of the SE blocks + in the network. + ''' + super(SEBlock_2D, self).__init__() + + self.in_planes = in_planes + self.avg_pool = torch.nn.AdaptiveAvgPool2d(1) + self.fc_1 = torch.nn.Linear(in_planes, in_planes // ratio) + self.relu = torch.nn.ReLU(inplace=inplace) + self.fc_2 = torch.nn.Linear(in_planes // ratio, in_planes) + self.sigmoid = torch.nn.Sigmoid() + + def forward(self, inputs): + """ + @inputs: a 3-dimensional tensor (a batch), + including [samples-index, frames-dim-index, frames-index] + """ + assert len(inputs.shape) == 4 + assert inputs.shape[1] == self.in_planes + + b, c, _, _ = inputs.size() + x = self.avg_pool(inputs).view(b, c) + x = self.fc_1(x) + x = self.relu(x) + x = self.fc_2(x) + x = self.sigmoid(x) + + scale = x.view(b, c, 1, 1) + return inputs * scale + + +def conv_bn(in_channels, + out_channels, + kernel_size, + stride, + padding, + dilation=1, + groups=1): + result = nn.Sequential() + result.add_module( + 'conv', + nn.Conv2d(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + groups=groups, + bias=False)) + result.add_module('bn', nn.BatchNorm2d(num_features=out_channels)) + return result + + +class RepVGGBlock(nn.Module): + + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + padding=0, + dilation=1, + groups=1, + padding_mode='zeros', + deploy=False, + use_se=False): + super(RepVGGBlock, self).__init__() + self.deploy = deploy + self.groups = groups + self.in_channels = in_channels + + assert kernel_size == 3 + assert padding == 1 + + padding_11 = padding - kernel_size // 2 + + self.nonlinearity = nn.ReLU(inplace=True) + + if use_se: + self.se = SEBlock_2D(out_channels, 4) + + else: + self.se = nn.Identity() + self.rbr_reparam = None + self.rbr_identity = None + self.rbr_dense = None + self.rbr_1x1 = None + if deploy: + self.rbr_reparam = nn.Conv2d(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + groups=groups, + bias=True, + padding_mode=padding_mode) + + else: + self.rbr_identity = nn.BatchNorm2d( + num_features=in_channels + ) if out_channels == in_channels and stride == 1 else None + self.rbr_dense = conv_bn(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + groups=groups) + self.rbr_1x1 = conv_bn(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=stride, + padding=padding_11, + groups=groups) + + def forward(self, inputs): + if self.deploy and self.rbr_reparam is not None: + + return self.se(self.nonlinearity(self.rbr_reparam(inputs))) + + if self.rbr_identity is None: + id_out = 0 + else: + id_out = self.rbr_identity(inputs) + if self.rbr_dense is not None and self.rbr_1x1 is not None: + return self.se( + self.nonlinearity( + self.rbr_dense(inputs) + self.rbr_1x1(inputs) + id_out)) + else: + raise TypeError( + "It's a training repvgg structure but branch conv not exits.") + + # Optional. This improves the accuracy and facilitates quantization. + # 1. Cancel the original weight decay on rbr_dense.conv.weight + # and rbr_1x1.conv.weight. + # 2. Use like this. + # loss = criterion(....) + # for every RepVGGBlock blk: + # loss += weight_decay_coefficient * 0.5 * blk.get_cust_L2() + # optimizer.zero_grad() + # loss.backward() + + def get_custom_L2(self): + K3 = self.rbr_dense.conv.weight + K1 = self.rbr_1x1.conv.weight + t3 = (self.rbr_dense.bn.weight / + ((self.rbr_dense.bn.running_var + + self.rbr_dense.bn.eps).sqrt())).reshape(-1, 1, 1, 1).detach() + t1 = (self.rbr_1x1.bn.weight / ((self.rbr_1x1.bn.running_var + + self.rbr_1x1.bn.eps).sqrt())).reshape( + -1, 1, 1, 1).detach() + + # The L2 loss of the "circle" of weights in 3x3 kernel. + # Use regular L2 on them. + l2_loss_circle = (K3**2).sum() - (K3[:, :, 1:2, 1:2]**2).sum() + # The equivalent resultant central point of 3x3 kernel. + eq_kernel = K3[:, :, 1:2, 1:2] * t3 + K1 * t1 + # Normalize for an L2 coefficient comparable to regular L2. + l2_loss_eq_kernel = (eq_kernel**2 / (t3**2 + t1**2)).sum() + return l2_loss_eq_kernel + l2_loss_circle + + # This func derives the equivalent kernel and bias in a DIFFERENTIABLE way + # You can get the equivalent kernel and bias + # at any time and do whatever you want, + # for example, apply some penalties or constraints during training, + # just like you do to the other models. + # May be useful for quantization or pruning. + + def get_equivalent_kernel_bias(self): + kernel3x3, bias3x3 = self._fuse_bn_tensor(self.rbr_dense) + kernel1x1, bias1x1 = self._fuse_bn_tensor(self.rbr_1x1) + kernelid, biasid = self._fuse_bn_tensor(self.rbr_identity) + return kernel3x3 + self._pad_1x1_to_3x3_tensor( + kernel1x1) + kernelid, bias3x3 + bias1x1 + biasid + + def _pad_1x1_to_3x3_tensor(self, kernel1x1): + if kernel1x1 is None: + return 0 + else: + return torch.nn.functional.pad(kernel1x1, [1, 1, 1, 1]) + + def _fuse_bn_tensor(self, branch): + if branch is None: + return 0, 0 + if isinstance(branch, nn.Sequential): + kernel = branch.conv.weight + running_mean = branch.bn.running_mean + running_var = branch.bn.running_var + gamma = branch.bn.weight + beta = branch.bn.bias + eps = branch.bn.eps + else: + assert isinstance(branch, nn.BatchNorm2d) + if not hasattr(self, 'id_tensor'): + input_dim = self.in_channels // self.groups + kernel_value = np.zeros((self.in_channels, input_dim, 3, 3), + dtype=np.float32) + for i in range(self.in_channels): + kernel_value[i, i % input_dim, 1, 1] = 1 + self.id_tensor = torch.from_numpy(kernel_value).to( + branch.weight.device) + kernel = self.id_tensor + running_mean = branch.running_mean + running_var = branch.running_var + gamma = branch.weight + beta = branch.bias + eps = branch.eps + std = (running_var + eps).sqrt() + t = (gamma / std).reshape(-1, 1, 1, 1) + return kernel * t, beta - running_mean * gamma / std + + def switch_to_deploy(self): + if self.rbr_reparam is not None: + return + kernel, bias = self.get_equivalent_kernel_bias() + self.rbr_reparam = nn.Conv2d( + in_channels=self.rbr_dense.conv.in_channels, + out_channels=self.rbr_dense.conv.out_channels, + kernel_size=self.rbr_dense.conv.kernel_size, + stride=self.rbr_dense.conv.stride, + padding=self.rbr_dense.conv.padding, + dilation=self.rbr_dense.conv.dilation, + groups=self.rbr_dense.conv.groups, + bias=True) + self.rbr_reparam.weight.data = kernel + self.rbr_reparam.bias.data = bias + for para in self.parameters(): + para.detach_() + self.rbr_dense = None + self.rbr_1x1 = None + if hasattr(self, 'rbr_identity'): + self.rbr_identity = None + if hasattr(self, 'id_tensor'): + self.__delattr__('id_tensor') + self.deploy = True + + +class RepSPKBlock(nn.Module): + + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + padding=0, + dilation=1, + branch_dilation=2, + groups=1, + padding_mode='zeros', + deploy=False, + use_se=False): + super(RepSPKBlock, self).__init__() + self.deploy = deploy + self.groups = groups + self.in_channels = in_channels + + # NOTE: RSBB block + # ref: https://arxiv.org/pdf/2110.09720.pdf + assert kernel_size == 3 + assert padding == 1 + assert dilation == 1 + assert branch_dilation == 2 + self.branch_dilation = branch_dilation + self.depoly_kernel_size = (kernel_size - 1) * (branch_dilation - + 1) + kernel_size + + self.nonlinearity = nn.ReLU(inplace=True) + + if use_se: + self.se = SEBlock_2D(out_channels, 4) + + else: + self.se = nn.Identity() + self.rbr_reparam = None + self.rbr_identity = None + self.rbr_dense = None + self.rbr_dense_dilation = None + if deploy: + self.rbr_reparam = nn.Conv2d(in_channels=in_channels, + out_channels=out_channels, + kernel_size=self.depoly_kernel_size, + stride=stride, + padding=self.branch_dilation, + groups=groups, + bias=True, + padding_mode=padding_mode) + + else: + self.rbr_identity = nn.BatchNorm2d( + num_features=in_channels + ) if out_channels == in_channels and stride == 1 else None + self.rbr_dense = conv_bn(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + groups=groups) + self.rbr_dense_dilation = conv_bn(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=self.branch_dilation, + dilation=self.branch_dilation, + groups=groups) + + def forward(self, inputs): + if self.deploy and self.rbr_reparam is not None: + + return self.se(self.nonlinearity(self.rbr_reparam(inputs))) + + if self.rbr_identity is None: + id_out = 0 + else: + id_out = self.rbr_identity(inputs) + if self.rbr_dense is not None and self.rbr_dense_dilation is not None: + return self.se( + self.nonlinearity( + self.rbr_dense(inputs) + self.rbr_dense_dilation(inputs) + + id_out)) + else: + raise TypeError( + "It's a training repvgg structure but branch conv not exits.") + + def get_equivalent_kernel_bias(self): + kernel3x3, bias3x3 = self._fuse_bn_tensor(self.rbr_dense) + kernel_dilation_branch, bias_dilation_branch = self._fuse_bn_tensor( + self.rbr_dense_dilation) + kernelid, biasid = self._fuse_bn_tensor(self.rbr_identity) + return self._convert_3x3_dilation_to_5x5_tensor( + kernel_dilation_branch) + self._pad_3x3_to_5x5_tensor( + kernel3x3) + kernelid, bias3x3 + bias_dilation_branch + biasid + + def _pad_3x3_to_5x5_tensor(self, kernel3x3): + if kernel3x3 is None: + return 0 + else: + return torch.nn.functional.pad(kernel3x3, [1, 1, 1, 1]) + + def _convert_3x3_dilation_to_5x5_tensor(self, kernel3x3): + if kernel3x3 is None: + return 0 + else: + kernel_value = torch.zeros( + (kernel3x3.size(0), kernel3x3.size(1), 5, 5), + dtype=kernel3x3.dtype) + kernel_value[:, :, ::2, ::2] = kernel3x3 + return kernel_value + + def _fuse_bn_tensor(self, branch): + if branch is None: + return 0, 0 + if isinstance(branch, nn.Sequential): + kernel = branch.conv.weight + running_mean = branch.bn.running_mean + running_var = branch.bn.running_var + gamma = branch.bn.weight + beta = branch.bn.bias + eps = branch.bn.eps + else: + assert isinstance(branch, nn.BatchNorm2d) + if not hasattr(self, 'id_tensor'): + input_dim = self.in_channels // self.groups + kernel_value = np.zeros((self.in_channels, input_dim, 5, 5), + dtype=np.float32) + for i in range(self.in_channels): + kernel_value[i, i % input_dim, 2, 2] = 1 + self.id_tensor = torch.from_numpy(kernel_value).to( + branch.weight.device) + kernel = self.id_tensor + running_mean = branch.running_mean + running_var = branch.running_var + gamma = branch.weight + beta = branch.bias + eps = branch.eps + std = (running_var + eps).sqrt() + t = (gamma / std).reshape(-1, 1, 1, 1) + return kernel * t, beta - running_mean * gamma / std + + def switch_to_deploy(self): + if self.rbr_reparam is not None: + return + kernel, bias = self.get_equivalent_kernel_bias() + self.rbr_reparam = nn.Conv2d( + in_channels=self.rbr_dense.conv.in_channels, + out_channels=self.rbr_dense.conv.out_channels, + kernel_size=self.depoly_kernel_size, + stride=self.rbr_dense.conv.stride, + padding=self.branch_dilation, + dilation=self.rbr_dense.conv.dilation, + groups=self.rbr_dense.conv.groups, + bias=True) + self.rbr_reparam.weight.data = kernel + self.rbr_reparam.bias.data = bias + for para in self.parameters(): + para.detach_() + self.rbr_dense = None + self.rbr_dense_dilation = None + if hasattr(self, 'rbr_identity'): + self.rbr_identity = None + if hasattr(self, 'id_tensor'): + self.__delattr__('id_tensor') + self.deploy = True + + +class RepVGG(nn.Module): + + def __init__(self, + head_inplanes=1, + block="RepVGG", + num_blocks=None, + strides=None, + base_width=64, + width_multiplier=None, + override_groups_map=None, + deploy=False, + use_se=False, + pooling_func='ASTP', + feat_dim=80, + embed_dim=256): + super(RepVGG, self).__init__() + + assert len(width_multiplier) == 4 + assert len(num_blocks) == 4 + assert len(strides) == 5 + width_multiplier = [w * (base_width / 64.) for w in width_multiplier] + self.deploy = deploy + self.override_groups_map = override_groups_map or dict() + self.use_se = use_se + self.downsample_multiple = 1 + if block == "RepVGG": + used_block = RepVGGBlock + elif block == "RepSPK": + used_block = RepSPKBlock + else: + raise TypeError("Do not support {} block.".format(block)) + + for s in strides: + self.downsample_multiple *= s + + assert 0 not in self.override_groups_map + + self.in_planes = min(64, int(64 * width_multiplier[0])) + + self.stage0 = used_block(head_inplanes, + out_channels=self.in_planes, + kernel_size=3, + stride=strides[0], + padding=1, + deploy=self.deploy, + use_se=self.use_se) + self.cur_layer_idx = 1 + self.stage1 = self._make_stage(used_block, + int(64 * width_multiplier[0]), + num_blocks[0], + stride=strides[1]) + self.stage2 = self._make_stage(used_block, + int(128 * width_multiplier[1]), + num_blocks[1], + stride=strides[2]) + self.stage3 = self._make_stage(used_block, + int(256 * width_multiplier[2]), + num_blocks[2], + stride=strides[3]) + self.stage4 = self._make_stage(used_block, + int(512 * width_multiplier[3]), + num_blocks[3], + stride=strides[4]) + self.output_planes = self.in_planes + self.stats_dim = self.output_planes * int(feat_dim / 8) + + self.pool = getattr(pooling_layers, + pooling_func)(in_dim=self.stats_dim) + self.pool_out_dim = self.pool.get_out_dim() + self.seg = nn.Linear(self.pool_out_dim, embed_dim) + + # init paramters + for m in self.modules(): + if isinstance(m, nn.Conv2d): + torch.nn.init.normal_(m.weight, 0., 0.01) + nn.init.kaiming_normal_(m.weight, + mode='fan_out', + nonlinearity='relu') + elif isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.GroupNorm)): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + + def _make_stage(self, block, planes, num_blocks, stride): + strides = [stride] + [1] * (num_blocks - 1) + blocks = [] + for stride in strides: + cur_groups = self.override_groups_map.get(self.cur_layer_idx, 1) + blocks.append( + block(in_channels=self.in_planes, + out_channels=planes, + kernel_size=3, + stride=stride, + padding=1, + groups=cur_groups, + deploy=self.deploy, + use_se=self.use_se)) + self.in_planes = planes + self.cur_layer_idx += 1 + return nn.Sequential(*blocks) + + def get_downsample_multiple(self): + return self.downsample_multiple + + def get_output_planes(self): + return self.output_planes + + def _get_frame_level_feat(self, x): + # for inner class usage + x = x.permute(0, 2, 1) # (B,T,F) -> (B,F,T) + x = x.unsqueeze_(1) + x = self.stage0(x) + x = self.stage1(x) + x = self.stage2(x) + x = self.stage3(x) + x = self.stage4(x) + + return x + + def get_frame_level_feat(self, x): + # for outer interface + out = self._get_frame_level_feat(x) + out = out.transpose(1, 3) + out = torch.flatten(out, 2, -1) + + return out # (B, T, D) + + def forward(self, x): + x = self._get_frame_level_feat(x) + stats = self.pool(x) + embed = self.seg(stats) + + return embed + + +def repvgg_model_convert(model: torch.nn.Module, save_path=None, do_copy=True): + if do_copy: + model = copy.deepcopy(model) + for module in model.modules(): + if hasattr(module, 'switch_to_deploy'): + module.switch_to_deploy() + if save_path is not None: + torch.save(model.state_dict(), save_path) + return model + + +# NOTE(cdliang): REPVGG_TINY_A0: Smaller hidden_dim and Deeper structure +def REPVGG_TINY_A0(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[3, 4, 23, 3], + strides=[1, 1, 2, 2, 2], + width_multiplier=[0.5, 0.5, 0.5, 0.5], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_TINY_RSBB_A0(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[3, 4, 23, 3], + strides=[1, 1, 2, 2, 2], + width_multiplier=[0.5, 0.5, 0.5, 0.5], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim, + block='RepSPK') + + +def REPVGG_A0(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[2, 4, 14, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[0.75, 0.75, 0.75, 2.5], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_RSBB_A0(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[2, 4, 14, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[0.75, 0.75, 0.75, 2.5], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim, + block='RepSPK') + + +def REPVGG_A1(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[2, 4, 14, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[1, 1, 1, 2.5], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_A2(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[2, 4, 14, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[1.5, 1.5, 1.5, 2.75], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_RSBB_A2(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[2, 4, 14, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[1.5, 1.5, 1.5, 2.75], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim, + block='RepSPK') + + +def REPVGG_B0(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[1, 1, 1, 2.5], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_RSBB_B0(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[1, 1, 1, 2.5], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim, + block='RepSPK') + + +def REPVGG_B1(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[2, 2, 2, 4], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_B1g2(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[2, 2, 2, 4], + override_groups_map=g2_map, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_B1g4(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[2, 2, 2, 4], + override_groups_map=g4_map, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_B2(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[2.5, 2.5, 2.5, 5], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_B2g2(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[2.5, 2.5, 2.5, 5], + override_groups_map=g2_map, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_B2g4(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[2.5, 2.5, 2.5, 5], + override_groups_map=g4_map, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_B3(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[3, 3, 3, 5], + override_groups_map=None, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_B3g2(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[3, 3, 3, 5], + override_groups_map=g2_map, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_B3g4(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=False): + return RepVGG(num_blocks=[4, 6, 16, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[3, 3, 3, 5], + override_groups_map=g4_map, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +def REPVGG_D2SE(feat_dim, + embed_dim, + pooling_func='TSTP', + deploy=False, + use_se=True): + return RepVGG(num_blocks=[8, 14, 24, 1], + strides=[1, 1, 2, 2, 2], + width_multiplier=[2.5, 2.5, 2.5, 5], + override_groups_map=g4_map, + deploy=deploy, + use_se=use_se, + pooling_func=pooling_func, + embed_dim=embed_dim, + feat_dim=feat_dim) + + +if __name__ == '__main__': + x = torch.zeros(1, 200, 80) + model = REPVGG_TINY_A0(feat_dim=80, + embed_dim=256, + pooling_func='TSTP', + deploy=True, + use_se=False) + model.eval() + y = model(x) + print(y.size()) + + num_params = sum(p.numel() for p in model.parameters()) + print("{} M".format(num_params / 1e6)) + + # from thop import profile + # x_np = torch.randn(1, 200, 80) + # flops, params = profile(model, inputs=(x_np, )) + # print("FLOPs: {} G, Params: {} M".format(flops / 1e9, params / 1e6)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/res2net.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/res2net.py new file mode 100644 index 0000000000000000000000000000000000000000..c783396331b15db012b5ad0e094ac46bb26468eb --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/res2net.py @@ -0,0 +1,227 @@ +# Copyright (c) 2024 Hongji Wang (jijijiang77@gmail.com) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +''' +This implementation is adapted from github repo: +https://github.com/alibaba-damo-academy/3D-Speaker + +Some modifications: +1. Reuse the pooling layers (small update) in wespeaker +2. Refine BasicBlockRes2Net to meet the torch.jit.script + export requirements + +''' + +import torch +import math +import torch.nn as nn +import torch.nn.functional as F +import wespeaker.models.pooling_layers as pooling_layers +from wespeaker.models.eres2net import ReLU, conv1x1, conv3x3 + + +class BasicBlockRes2Net(nn.Module): + expansion = 2 + + def __init__(self, in_planes, planes, stride=1, baseWidth=32, scale=2): + super(BasicBlockRes2Net, self).__init__() + width = int(math.floor(planes * (baseWidth / 64.0))) + self.conv1 = conv1x1(in_planes, width * scale, stride) + self.bn1 = nn.BatchNorm2d(width * scale) + self.nums = scale - 1 + convs = [] + bns = [] + for i in range(self.nums): + convs.append(conv3x3(width, width)) + bns.append(nn.BatchNorm2d(width)) + self.convs = nn.ModuleList(convs) + self.bns = nn.ModuleList(bns) + self.relu = ReLU(inplace=True) + + self.conv3 = conv1x1(width * scale, planes * self.expansion) + self.bn3 = nn.BatchNorm2d(planes * self.expansion) + self.shortcut = nn.Sequential() + if stride != 1 or in_planes != self.expansion * planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, + self.expansion * planes, + kernel_size=1, + stride=stride, + bias=False), nn.BatchNorm2d(self.expansion * planes)) + self.stride = stride + self.width = width + self.scale = scale + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + spx = torch.split(out, self.width, 1) + sp = spx[0] + for i, (conv, bn) in enumerate(zip(self.convs, self.bns)): + if i >= 1: + sp = sp + spx[i] + sp = conv(sp) + sp = self.relu(bn(sp)) + if i == 0: + out = sp + else: + out = torch.cat((out, sp), 1) + + out = torch.cat((out, spx[self.nums]), 1) + + out = self.conv3(out) + out = self.bn3(out) + + residual = self.shortcut(x) + out += residual + out = self.relu(out) + + return out + + +class Res2Net(nn.Module): + + def __init__(self, + m_channels, + num_blocks, + block=BasicBlockRes2Net, + feat_dim=80, + embed_dim=192, + pooling_func='TSTP', + two_emb_layer=False): + super(Res2Net, self).__init__() + self.in_planes = m_channels + self.feat_dim = feat_dim + self.embed_dim = embed_dim + self.stats_dim = int(feat_dim / 8) * m_channels * 8 + self.two_emb_layer = two_emb_layer + + self.conv1 = nn.Conv2d(1, + m_channels, + kernel_size=3, + stride=1, + padding=1, + bias=False) + self.bn1 = nn.BatchNorm2d(m_channels) + self.layer1 = self._make_layer(block, + m_channels, + num_blocks[0], + stride=1) + self.layer2 = self._make_layer(block, + m_channels * 2, + num_blocks[1], + stride=2) + self.layer3 = self._make_layer(block, + m_channels * 4, + num_blocks[2], + stride=2) + self.layer4 = self._make_layer(block, + m_channels * 8, + num_blocks[3], + stride=2) + + self.pool = getattr(pooling_layers, + pooling_func)(in_dim=self.stats_dim * + block.expansion) + self.pool_out_dim = self.pool.get_out_dim() + self.seg_1 = nn.Linear(self.pool_out_dim, embed_dim) + if self.two_emb_layer: + self.seg_bn_1 = nn.BatchNorm1d(embed_dim, affine=False) + self.seg_2 = nn.Linear(embed_dim, embed_dim) + else: + self.seg_bn_1 = nn.Identity() + self.seg_2 = nn.Identity() + + def _make_layer(self, block, planes, num_blocks, stride): + strides = [stride] + [1] * (num_blocks - 1) + layers = [] + for stride in strides: + layers.append(block(self.in_planes, planes, stride)) + self.in_planes = planes * block.expansion + return nn.Sequential(*layers) + + def _get_frame_level_feat(self, x): + # for inner class usage + x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T) + + x = x.unsqueeze_(1) + out = F.relu(self.bn1(self.conv1(x))) + out = self.layer1(out) + out = self.layer2(out) + out = self.layer3(out) + out = self.layer4(out) + + return out + + def get_frame_level_feat(self, x): + # for outer interface + out = self._get_frame_level_feat(x) + out = out.transpose(1, 3) + out = torch.flatten(out, 2, -1) + + return out # (B, T, D) + + def forward(self, x): + out = self._get_frame_level_feat(x) + stats = self.pool(out) + + embed_a = self.seg_1(stats) + if self.two_emb_layer: + out = F.relu(embed_a) + out = self.seg_bn_1(out) + embed_b = self.seg_2(out) + return embed_b + else: + return embed_a + + +def Res2Net34_Base(feat_dim, + embed_dim, + pooling_func='TSTP', + two_emb_layer=False): + return Res2Net(32, [3, 4, 6, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def Res2Net34_Large(feat_dim, + embed_dim, + pooling_func='TSTP', + two_emb_layer=False): + return Res2Net(64, [3, 4, 6, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +if __name__ == '__main__': + x = torch.zeros(1, 200, 80) + model = Res2Net34_Base(feat_dim=80, embed_dim=256, two_emb_layer=False) + model.eval() + out = model(x) + print(out.size()) + + num_params = sum(p.numel() for p in model.parameters()) + print("{} M".format(num_params / 1e6)) + + # from thop import profile + # x_np = torch.randn(1, 200, 80) + # flops, params = profile(model, inputs=(x_np, )) + # print("FLOPs: {} G, Params: {} M".format(flops / 1e9, params / 1e6)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/resnet.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/resnet.py new file mode 100644 index 0000000000000000000000000000000000000000..13df0a239602586f571dfa25f28f418715e10166 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/resnet.py @@ -0,0 +1,279 @@ +# Copyright (c) 2021 Shuai Wang (wsstriving@gmail.com) +# 2022 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2023 Bing Han (hanbing97@sjtu.edu.cn) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''ResNet in PyTorch. + +Some modifications from the original architecture: +1. Smaller kernel size for the input layer +2. Smaller number of Channels +3. No max_pooling involved + +Reference: +[1] Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun + Deep Residual Learning for Image Recognition. arXiv:1512.03385 +''' + +import torch +import torch.nn as nn +import torch.nn.functional as F +import wespeaker.models.pooling_layers as pooling_layers + + +class BasicBlock(nn.Module): + expansion = 1 + + def __init__(self, in_planes, planes, stride=1): + super(BasicBlock, self).__init__() + self.conv1 = nn.Conv2d(in_planes, + planes, + kernel_size=3, + stride=stride, + padding=1, + bias=False) + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d(planes, + planes, + kernel_size=3, + stride=1, + padding=1, + bias=False) + self.bn2 = nn.BatchNorm2d(planes) + + self.shortcut = nn.Sequential() + if stride != 1 or in_planes != self.expansion * planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, + self.expansion * planes, + kernel_size=1, + stride=stride, + bias=False), nn.BatchNorm2d(self.expansion * planes)) + + def forward(self, x): + out = F.relu(self.bn1(self.conv1(x))) + out = self.bn2(self.conv2(out)) + out += self.shortcut(x) + out = F.relu(out) + return out + + +class Bottleneck(nn.Module): + expansion = 4 + + def __init__(self, in_planes, planes, stride=1): + super(Bottleneck, self).__init__() + self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False) + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d(planes, + planes, + kernel_size=3, + stride=stride, + padding=1, + bias=False) + self.bn2 = nn.BatchNorm2d(planes) + self.conv3 = nn.Conv2d(planes, + self.expansion * planes, + kernel_size=1, + bias=False) + self.bn3 = nn.BatchNorm2d(self.expansion * planes) + + self.shortcut = nn.Sequential() + if stride != 1 or in_planes != self.expansion * planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, + self.expansion * planes, + kernel_size=1, + stride=stride, + bias=False), nn.BatchNorm2d(self.expansion * planes)) + + def forward(self, x): + out = F.relu(self.bn1(self.conv1(x))) + out = F.relu(self.bn2(self.conv2(out))) + out = self.bn3(self.conv3(out)) + out += self.shortcut(x) + out = F.relu(out) + return out + + +class ResNet(nn.Module): + + def __init__(self, + block, + num_blocks, + m_channels=32, + feat_dim=40, + embed_dim=128, + pooling_func='TSTP', + two_emb_layer=False): + super(ResNet, self).__init__() + self.in_planes = m_channels + self.feat_dim = feat_dim + self.embed_dim = embed_dim + self.stats_dim = int(feat_dim / 8) * m_channels * 8 + self.two_emb_layer = two_emb_layer + + self.conv1 = nn.Conv2d(1, + m_channels, + kernel_size=3, + stride=1, + padding=1, + bias=False) + self.bn1 = nn.BatchNorm2d(m_channels) + self.layer1 = self._make_layer(block, + m_channels, + num_blocks[0], + stride=1) + self.layer2 = self._make_layer(block, + m_channels * 2, + num_blocks[1], + stride=2) + self.layer3 = self._make_layer(block, + m_channels * 4, + num_blocks[2], + stride=2) + self.layer4 = self._make_layer(block, + m_channels * 8, + num_blocks[3], + stride=2) + + self.pool = getattr(pooling_layers, + pooling_func)(in_dim=self.stats_dim * + block.expansion) + self.pool_out_dim = self.pool.get_out_dim() + self.seg_1 = nn.Linear(self.pool_out_dim, embed_dim) + if self.two_emb_layer: + self.seg_bn_1 = nn.BatchNorm1d(embed_dim, affine=False) + self.seg_2 = nn.Linear(embed_dim, embed_dim) + else: + self.seg_bn_1 = nn.Identity() + self.seg_2 = nn.Identity() + + def _make_layer(self, block, planes, num_blocks, stride): + strides = [stride] + [1] * (num_blocks - 1) + layers = [] + for stride in strides: + layers.append(block(self.in_planes, planes, stride)) + self.in_planes = planes * block.expansion + return nn.Sequential(*layers) + + def _get_frame_level_feat(self, x): + # for inner class usage + x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T) + + x = x.unsqueeze_(1) + out = F.relu(self.bn1(self.conv1(x))) + out = self.layer1(out) + out = self.layer2(out) + out = self.layer3(out) + out = self.layer4(out) + + return out + + def get_frame_level_feat(self, x): + # for outer interface + out = self._get_frame_level_feat(x) + out = out.transpose(1, 3) + out = torch.flatten(out, 2, -1) + + return out # (B, T, D) + + def forward(self, x): + out = self._get_frame_level_feat(x) + + stats = self.pool(out) + + embed_a = self.seg_1(stats) + if self.two_emb_layer: + out = F.relu(embed_a) + out = self.seg_bn_1(out) + embed_b = self.seg_2(out) + return embed_a, embed_b + else: + return torch.tensor(0.0), embed_a + + +def ResNet18(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return ResNet(BasicBlock, [2, 2, 2, 2], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def ResNet34(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return ResNet(BasicBlock, [3, 4, 6, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def ResNet50(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return ResNet(Bottleneck, [3, 4, 6, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def ResNet101(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return ResNet(Bottleneck, [3, 4, 23, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def ResNet152(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return ResNet(Bottleneck, [3, 8, 36, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def ResNet221(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return ResNet(Bottleneck, [6, 16, 48, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +def ResNet293(feat_dim, embed_dim, pooling_func='TSTP', two_emb_layer=False): + return ResNet(Bottleneck, [10, 20, 64, 3], + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + two_emb_layer=two_emb_layer) + + +if __name__ == '__main__': + x = torch.zeros(1, 200, 80) + model = ResNet34(feat_dim=80, embed_dim=256, two_emb_layer=False) + model.eval() + out = model(x) + print(out[-1].size()) + + num_params = sum(p.numel() for p in model.parameters()) + print("{} M".format(num_params / 1e6)) + + # from thop import profile + # x_np = torch.randn(1, 200, 80) + # flops, params = profile(model, inputs=(x_np, )) + # print("FLOPs: {} G, Params: {} M".format(flops / 1e9, params / 1e6)) + + # from torchinfo import summary + # summary(model, (16, 100, 80)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/samresnet.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/samresnet.py new file mode 100644 index 0000000000000000000000000000000000000000..bcdda521531f7e00eb80fbac7c0b3a0b4cd2246d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/samresnet.py @@ -0,0 +1,178 @@ +# Copyright (c) 2024 XiaoyiQin, Yuke Lin (linyuke0609@gmail.com) +# 2024 Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import torch.nn as nn +import wespeaker.models.pooling_layers as pooling_layers + + +class SimAMBasicBlock(nn.Module): + expansion = 1 + + def __init__( + self, ConvLayer, NormLayer, in_planes, planes, stride=1, block_id=1 + ): + super(SimAMBasicBlock, self).__init__() + self.conv1 = ConvLayer( + in_planes, + planes, + kernel_size=3, + stride=stride, + padding=1, + bias=False, + ) + self.bn1 = NormLayer(planes) + self.conv2 = ConvLayer( + planes, planes, kernel_size=3, stride=1, padding=1, bias=False + ) + self.bn2 = NormLayer(planes) + self.relu = nn.ReLU(inplace=True) + self.sigmoid = nn.Sigmoid() + + self.downsample = nn.Sequential() + if stride != 1 or in_planes != self.expansion * planes: + self.downsample = nn.Sequential( + ConvLayer( + in_planes, + self.expansion * planes, + kernel_size=1, + stride=stride, + bias=False, + ), + NormLayer(self.expansion * planes), + ) + + def forward(self, x): + out = self.relu(self.bn1(self.conv1(x))) + out = self.bn2(self.conv2(out)) + out = self.SimAM(out) + out += self.downsample(x) + out = self.relu(out) + return out + + def SimAM(self, X, lambda_p=1e-4): + n = X.shape[2] * X.shape[3] - 1 + d = (X - X.mean(dim=[2, 3], keepdim=True)).pow(2) + v = d.sum(dim=[2, 3], keepdim=True) / n + E_inv = d / (4 * (v + lambda_p)) + 0.5 + return X * self.sigmoid(E_inv) + + +class ResNet(nn.Module): + def __init__( + self, in_planes, block, num_blocks, in_ch=1, **kwargs + ): + super(ResNet, self).__init__() + self.in_planes = in_planes + self.NormLayer = nn.BatchNorm2d + self.ConvLayer = nn.Conv2d + + self.conv1 = self.ConvLayer( + in_ch, in_planes, kernel_size=3, stride=1, padding=1, bias=False + ) + self.bn1 = self.NormLayer(in_planes) + self.relu = nn.ReLU(inplace=True) + self.layer1 = self._make_layer( + block, in_planes, num_blocks[0], stride=1, block_id=1 + ) + self.layer2 = self._make_layer( + block, in_planes * 2, num_blocks[1], stride=2, block_id=2 + ) + self.layer3 = self._make_layer( + block, in_planes * 4, num_blocks[2], stride=2, block_id=3 + ) + self.layer4 = self._make_layer( + block, in_planes * 8, num_blocks[3], stride=2, block_id=4 + ) + + def _make_layer(self, block, planes, num_blocks, stride, block_id=1): + strides = [stride] + [1] * (num_blocks - 1) + layers = [] + for stride in strides: + layers.append( + block( + self.ConvLayer, + self.NormLayer, + self.in_planes, + planes, + stride, + block_id, + ) + ) + self.in_planes = planes * block.expansion + return nn.Sequential(*layers) + + def forward(self, x): + x = self.relu(self.bn1(self.conv1(x))) + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + return x + + +def SimAM_ResNet34(in_planes): + return ResNet(in_planes, SimAMBasicBlock, [3, 4, 6, 3]) + + +def SimAM_ResNet100(in_planes): + return ResNet(in_planes, SimAMBasicBlock, [6, 16, 24, 3]) + + +class SimAM_ResNet34_ASP(nn.Module): + def __init__(self, in_planes=64, embed_dim=256, acoustic_dim=80, dropout=0): + super(SimAM_ResNet34_ASP, self).__init__() + self.front = SimAM_ResNet34(in_planes) + self.pooling = pooling_layers.ASP(in_planes, acoustic_dim) + self.bottleneck = nn.Linear(self.pooling.out_dim, embed_dim) + self.drop = nn.Dropout(dropout) if dropout else None + + def forward(self, x): + x = x.permute(0, 2, 1) + x = self.front(x.unsqueeze(dim=1)) + x = self.pooling(x) + if self.drop: + x = self.drop(x) + x = self.bottleneck(x) + return x + + +class SimAM_ResNet100_ASP(nn.Module): + def __init__(self, in_planes=64, embed_dim=256, acoustic_dim=80, dropout=0): + super(SimAM_ResNet100_ASP, self).__init__() + self.front = SimAM_ResNet100(in_planes) + self.pooling = pooling_layers.ASP(in_planes, acoustic_dim) + self.bottleneck = nn.Linear(self.pooling.out_dim, embed_dim) + self.drop = nn.Dropout(dropout) if dropout else None + + def forward(self, x): + x = x.permute(0, 2, 1) + x = self.front(x.unsqueeze(dim=1)) + x = self.pooling(x) + if self.drop: + x = self.drop(x) + x = self.bottleneck(x) + return x + + +if __name__ == '__main__': + x = torch.zeros(1, 200, 80) + model = SimAM_ResNet34_ASP(embed_dim=256) + model.eval() + out = model(x) + print(out[-1].size()) + + num_params = sum(p.numel() for p in model.parameters()) + print("{} M".format(num_params / 1e6)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/speaker_model.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/speaker_model.py new file mode 100644 index 0000000000000000000000000000000000000000..989ff29f024290554bc6f80b100d71ff568bbaac --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/speaker_model.py @@ -0,0 +1,62 @@ +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# 2024 Shuai Wang (wsstriving@gmail.com) +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import wespeaker.models.tdnn as tdnn +import wespeaker.models.ecapa_tdnn as ecapa_tdnn +import wespeaker.models.resnet as resnet +import wespeaker.models.repvgg as repvgg +import wespeaker.models.campplus as campplus +import wespeaker.models.eres2net as eres2net +import wespeaker.models.gemini_dfresnet as gemini +import wespeaker.models.res2net as res2net +import wespeaker.models.whisper_PMFA as whisper_PMFA +import wespeaker.models.redimnet as redimnet +import wespeaker.models.redimnet2 as redimnet2 +import wespeaker.models.samresnet as samresnet +import wespeaker.models.xi_vector as xi_vector +import wespeaker.models.w2vbert_adapter_mfa as w2vbert_adapter_mfa + + +def get_speaker_model(model_name: str): + if model_name.startswith("XVEC"): + return getattr(tdnn, model_name) + elif model_name.startswith("ECAPA_TDNN"): + return getattr(ecapa_tdnn, model_name) + elif model_name.startswith("ResNet"): + return getattr(resnet, model_name) + elif model_name.startswith("REPVGG"): + return getattr(repvgg, model_name) + elif model_name.startswith("CAMPPlus"): + return getattr(campplus, model_name) + elif model_name.startswith("ERes2Net"): + return getattr(eres2net, model_name) + elif model_name.startswith("Res2Net"): + return getattr(res2net, model_name) + elif model_name.startswith("Gemini"): + return getattr(gemini, model_name) + elif model_name.startswith("whisper_PMFA"): + return getattr(whisper_PMFA, model_name) + elif model_name.startswith("ReDimNet2"): + return getattr(redimnet2, model_name) + elif model_name.startswith("ReDimNet"): + return getattr(redimnet, model_name) + elif model_name.startswith("SimAM_ResNet"): + return getattr(samresnet, model_name) + elif model_name.startswith("XI_VEC"): + return getattr(xi_vector, model_name) + elif model_name.startswith("W2VBert_Adapter_MFA"): + return getattr(w2vbert_adapter_mfa, model_name) + else: # model_name error !!! + print(model_name + " not found !!!") + exit(1) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/tdnn.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/tdnn.py new file mode 100644 index 0000000000000000000000000000000000000000..59724437e0a9da88491649f5fc9ab5e8278d4044 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/tdnn.py @@ -0,0 +1,131 @@ +# Copyright (c) 2021 Shuai Wang (wsstriving@gmail.com) +# 2024 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""TDNN model for x-vector learning""" + +import torch +import torch.nn as nn +import torch.nn.functional as F +import wespeaker.models.pooling_layers as pooling_layers + + +class TdnnLayer(nn.Module): + + def __init__(self, in_dim, out_dim, context_size, dilation=1, padding=0): + """Define the TDNN layer, essentially 1-D convolution + + Args: + in_dim (int): input dimension + out_dim (int): output channels + context_size (int): context size, essentially the filter size + dilation (int, optional): Defaults to 1. + padding (int, optional): Defaults to 0. + """ + super(TdnnLayer, self).__init__() + self.in_dim = in_dim + self.out_dim = out_dim + self.context_size = context_size + self.dilation = dilation + self.padding = padding + self.conv_1d = nn.Conv1d(self.in_dim, + self.out_dim, + self.context_size, + dilation=self.dilation, + padding=self.padding) + + # Set Affine=false to be compatible with the original kaldi version + self.bn = nn.BatchNorm1d(out_dim, affine=False) + + def forward(self, x): + out = self.conv_1d(x) + out = F.relu(out) + out = self.bn(out) + return out + + +class XVEC(nn.Module): + + def __init__(self, + feat_dim=40, + hid_dim=512, + stats_dim=1500, + embed_dim=512, + pooling_func='TSTP'): + """ + Implementation of Kaldi style xvec, as described in + X-VECTORS: ROBUST DNN EMBEDDINGS FOR SPEAKER RECOGNITION + """ + super(XVEC, self).__init__() + self.feat_dim = feat_dim + self.stats_dim = stats_dim + self.embed_dim = embed_dim + + self.frame_1 = TdnnLayer(feat_dim, hid_dim, context_size=5, dilation=1) + self.frame_2 = TdnnLayer(hid_dim, hid_dim, context_size=3, dilation=2) + self.frame_3 = TdnnLayer(hid_dim, hid_dim, context_size=3, dilation=3) + self.frame_4 = TdnnLayer(hid_dim, hid_dim, context_size=1, dilation=1) + self.frame_5 = TdnnLayer(hid_dim, + stats_dim, + context_size=1, + dilation=1) + + self.pool = getattr(pooling_layers, pooling_func)(in_dim=stats_dim) + self.pool_out_dim = self.pool.get_out_dim() + self.seg_1 = nn.Linear(self.pool_out_dim, embed_dim) + self.seg_bn_1 = nn.BatchNorm1d(embed_dim, affine=False) + self.seg_2 = nn.Linear(embed_dim, embed_dim) + + def _get_frame_level_feat(self, x): + # for inner class usage + x = x.permute(0, 2, 1) # (B,T,F) -> (B,F,T) + + out = self.frame_1(x) + out = self.frame_2(out) + out = self.frame_3(out) + out = self.frame_4(out) + out = self.frame_5(out) + + return out + + def get_frame_level_feat(self, x): + # for outer interface + out = self._get_frame_level_feat(x).permute(0, 2, 1) + + return out # (B, T, D) + + def forward(self, x): + out = self._get_frame_level_feat(x) + stats = self.pool(out) + embed_a = self.seg_1(stats) + out = F.relu(embed_a) + out = self.seg_bn_1(out) + embed_b = self.seg_2(out) + + return embed_a, embed_b + + +if __name__ == '__main__': + x = torch.rand(1, 200, 80) + model = XVEC(feat_dim=80, embed_dim=512, pooling_func='TSTP') + model.eval() + y = model(x) + print(y[-1].size()) + + num_params = sum(p.numel() for p in model.parameters()) + print("{} M".format(num_params / 1e6)) + + # from thop import profile + # x_np = torch.randn(1, 200, 80) + # flops, params = profile(model, inputs=(x_np, )) + # print("FLOPs: {} G, Params: {} M".format(flops / 1e9, params / 1e6)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/w2vbert_adapter_mfa.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/w2vbert_adapter_mfa.py new file mode 100644 index 0000000000000000000000000000000000000000..eb47bfdc4528462754d38d366f9770d8e3c2fbc1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/w2vbert_adapter_mfa.py @@ -0,0 +1,124 @@ +# Copyright (c) 2025 Qituan Shangguan (2369144677@qq.com) +# Based on original code from deeplab/3D-Speaker +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import torch +import torch.nn as nn +import wespeaker.models.pooling_layers as pooling_layers + + +class W2VBert_Adapter_MFA(nn.Module): + """ + Speaker Model using W2VBert Frontend and Adapter MFA backend. + """ + + def __init__( + self, + feat_dim: int, + embed_dim: int = 256, + pooling_func: str = "ASP", + n_mfa_layers: int = -1, + adapter_dim: int = 128, + dropout: float = 0.0, + num_frontend_hidden_layers: int = 24, + ): + """ + Args: + feat_dim (int): Hidden dim D of each input hidden state + (B, T, D). Should match frontend output_size(). + embed_dim (int): Final speaker embedding dim. + pooling_func (str): Pooling layer name. + n_mfa_layers (int): Number of last hidden states to use. + -1 means use all. + adapter_dim (int): Dim of adapter transform. + dropout (float): Dropout probability. + num_frontend_hidden_layers (int): Number of transformer + layers in frontend. + """ + super().__init__() + + actual_feat_dim = feat_dim + print( + "Adapter MFA initialized with input feature dimension: " + f"{actual_feat_dim}" + ) + + # Frontend returns N + 1 states (input embed + N layers) + num_available_states = num_frontend_hidden_layers + 1 + + if n_mfa_layers == -1: + self.n_mfa_layers = num_available_states + else: + self.n_mfa_layers = n_mfa_layers + + assert 1 <= self.n_mfa_layers <= num_available_states, ( + "Invalid n_mfa_layers " + f"({self.n_mfa_layers}). Available: {num_available_states}" + ) + + print( + f"Using {self.n_mfa_layers} last hidden states from frontend." + ) + + # Adapter layers for each selected hidden state + self.adapter_layers = nn.ModuleList( + [ + nn.Sequential( + nn.Linear(actual_feat_dim, adapter_dim), + nn.LayerNorm(adapter_dim), + nn.ReLU(True), + nn.Linear(adapter_dim, adapter_dim), + ) + for _ in range(self.n_mfa_layers) + ] + ) + + pooling_input_dim = adapter_dim * self.n_mfa_layers + self.pooling = getattr(pooling_layers, pooling_func)( + input_dim=pooling_input_dim, + hidden_dim=adapter_dim, + ) + + pool_out_dim = self.pooling.out_dim + self.bottleneck = nn.Linear(pool_out_dim, embed_dim) + self.drop = nn.Dropout(dropout) if dropout > 0 else nn.Identity() + + def forward(self, all_hidden_states: tuple): + # Select last N states + hidden_states_to_use = all_hidden_states[-self.n_mfa_layers :] + + # Adapter transform + adapter_outputs = [] + for i in range(self.n_mfa_layers): + x = self.adapter_layers[i](hidden_states_to_use[i]) + adapter_outputs.append(x) + + # Concatenate outputs + combined_features = torch.cat(adapter_outputs, dim=-1) + combined_features = combined_features.transpose(1, 2) + + # Pooling + pooled_embedding = self.pooling(combined_features) + + # Dropout + Projection + pooled_embedding = self.drop(pooled_embedding) + embedding = self.bottleneck(pooled_embedding) + + # Dummy loss for DDP compatibility during training + if self.training and hasattr(self, "projection"): + dummy_loss = 0.0 * self.projection.weight.sum() + embedding = embedding + dummy_loss + + # Return dummy tensor + embedding + return torch.tensor(0.0, device=embedding.device), embedding diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/whisper_PMFA.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/whisper_PMFA.py new file mode 100644 index 0000000000000000000000000000000000000000..c11483d8136a786541544cda05c5923192251e94 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/whisper_PMFA.py @@ -0,0 +1,139 @@ +# Copyright (c) 2024 Yiyang Zhao (zhaoyy22@mails.tsinghua.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +from torch import nn + +import wespeaker.models.pooling_layers as pooling_layers + + +class BatchNorm1d(nn.Module): + """Applies 1d batch normalization to the input tensor. + + Arguments + --------- + input_shape : tuple + The expected shape of the input. Alternatively, use ``input_size``. + input_size : int + The expected size of the input. Alternatively, use ``input_shape``. + eps : float + This value is added to std deviation estimation to improve the numerical + stability. + momentum : float + It is a value used for the running_mean and running_var computation. + affine : bool + When set to True, the affine parameters are learned. + track_running_stats : bool + When set to True, this module tracks the running mean and variance, + and when set to False, this module does not track such statistics. + combine_batch_time : bool + When true, it combines batch an time axis. + + + Example + ------- + >>> input = torch.randn(100, 10) + >>> norm = BatchNorm1d(input_shape=input.shape) + >>> output = norm(input) + >>> output.shape + torch.Size([100, 10]) + """ + + def __init__( + self, + input_shape=None, + input_size=None, + eps=1e-05, + momentum=0.1, + affine=True, + track_running_stats=True, + combine_batch_time=False, + skip_transpose=True, + ): + super().__init__() + self.combine_batch_time = combine_batch_time + self.skip_transpose = skip_transpose + + if input_size is None and skip_transpose: + input_size = input_shape[1] + elif input_size is None: + input_size = input_shape[-1] + + self.norm = nn.BatchNorm1d( + input_size, + eps=eps, + momentum=momentum, + affine=affine, + track_running_stats=track_running_stats, + ) + + def forward(self, x): + """Returns the normalized input tensor. + + Arguments + --------- + x : torch.Tensor (batch, time, [channels]) + input to normalize. 2d or 3d tensors are expected in input + 4d tensors can be used when combine_dims=True. + """ + shape_or = x.shape + if self.combine_batch_time: + if x.ndim == 3: + x = x.reshape(shape_or[0] * shape_or[1], shape_or[2]) + else: + x = x.reshape( + shape_or[0] * shape_or[1], shape_or[3], shape_or[2] + ) + + elif not self.skip_transpose: + x = x.transpose(-1, 1) + + x_n = self.norm(x) + + if self.combine_batch_time: + x_n = x_n.reshape(shape_or) + elif not self.skip_transpose: + x_n = x_n.transpose(1, -1) + + return x_n + + +class whisper_PMFA(torch.nn.Module): + def __init__( + self, + output_size=1280, + embedding_dim=192, + pooling_func='ASTP', + global_context_att=True): + super(whisper_PMFA, self).__init__() + self.pooling = getattr(pooling_layers, pooling_func)( + in_dim=output_size, global_context_att=global_context_att) + self.bn = BatchNorm1d(input_size=output_size * 2) + self.fc = torch.nn.Linear(output_size * 2, embedding_dim) + + def forward(self, x): + x = x.permute(0, 2, 1) + x = self.pooling(x) + x = x.unsqueeze(-1) + x = self.bn(x) + x = x.permute(0, 2, 1) + x = self.fc(x) + x = x.squeeze(1) + return x + + +def whisper_PMFA_large_v2(feat_dim, embed_dim): + return whisper_PMFA(output_size=feat_dim, + embedding_dim=embed_dim + ) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/xi_vector.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/xi_vector.py new file mode 100644 index 0000000000000000000000000000000000000000..a247c83f8ba41f88a8f013739f2958baa7a1a3d1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/models/xi_vector.py @@ -0,0 +1,65 @@ +# Copyright (c) 2025 Shuai Wang (wsstriving@gmail.com) +# 2025 Junjie LI (junjie98.li@connect.polyu.hk) +# 2025 Tianchi Liu (tianchi_liu@u.nus.edu) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''The implementation of Xi_vector. + +Reference: +[1] Lee, K. A., Wang, Q., & Koshinaka, T. (2021). Xi-vector embedding +for speaker recognition. IEEE Signal Processing Letters, 28, 1385-1389. +''' + + +import torch +import wespeaker.models.ecapa_tdnn as ecapa_tdnn +import wespeaker.models.tdnn as tdnn + + + + +def XI_VEC_ECAPA_TDNN_c1024(feat_dim, embed_dim, pooling_func='XI', emb_bn=False): + return ecapa_tdnn.ECAPA_TDNN(channels=1024, + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + emb_bn=emb_bn) + + +def XI_VEC_ECAPA_TDNN_c512(feat_dim, embed_dim, pooling_func='XI', emb_bn=False): + return ecapa_tdnn.ECAPA_TDNN(channels=512, + feat_dim=feat_dim, + embed_dim=embed_dim, + pooling_func=pooling_func, + emb_bn=emb_bn) + + + +def XI_VEC_XVEC(feat_dim, embed_dim, pooling_func='XI'): + return tdnn.XVEC(feat_dim=feat_dim, embed_dim=embed_dim, pooling_func=pooling_func) + + +if __name__ == '__main__': + x = torch.rand(1, 200, 80) + model = XI_VEC_XVEC(feat_dim=80, embed_dim=512, pooling_func='XI') + model.eval() + y = model(x) + print(y[-1].size()) + + num_params = sum(p.numel() for p in model.parameters()) + print("{} M".format(num_params / 1e6)) + + from thop import profile + x_np = torch.randn(1, 200, 80) + flops, params = profile(model, inputs=(x_np, )) + print("FLOPs: {} G, Params: {} M".format(flops / 1e9, params / 1e6)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/average_contrastive_model.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/average_contrastive_model.py new file mode 100644 index 0000000000000000000000000000000000000000..e45ea08e79a0a7487fad55d11d016001b7a6d660 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/average_contrastive_model.py @@ -0,0 +1,94 @@ +# Copyright (c) 2020 Mobvoi Inc (Di Wu) +# 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import glob +import re + +import torch + + +def get_args(): + parser = argparse.ArgumentParser(description='average model') + parser.add_argument('--dst_model', required=True, help='averaged model') + parser.add_argument('--src_path', + required=True, + help='src model path for average') + parser.add_argument('--num', + default=5, + type=int, + help='nums for averaged model') + parser.add_argument('--min_epoch', + default=0, + type=int, + help='min epoch used for averaging model') + parser.add_argument( + '--max_epoch', + default=65536, # Big enough + type=int, + help='max epoch used for averaging model') + args = parser.parse_args() + print(args) + return args + + +def get_model_encoder_state_dict(state_dict): + query_dict = {} + for key, val in state_dict.items(): + # for moco model + if key.startswith('encoder_q.'): + query_dict[key[10:]] = val + # for simclr model + elif key.startswith('encoder.'): + query_dict[key[8:]] = val + + return query_dict + + +def main(): + args = get_args() + + path_list = glob.glob('{}/[!avg][!final][!convert]*.pt'.format( + args.src_path)) + path_list = sorted( + path_list, + key=lambda p: int(re.findall(r"(?<=model_)\d*(?=.pt)", p)[0])) + path_list = path_list[-args.num:] + print(path_list) + avg = None + num = args.num + assert num == len(path_list) + for path in path_list: + print('Processing {}'.format(path)) + states = torch.load(path, map_location=torch.device('cpu')) + states = get_model_encoder_state_dict(states) + if avg is None: + avg = states + else: + for k in avg.keys(): + avg[k] += states[k] + # average + for k in avg.keys(): + if avg[k] is not None: + # pytorch 1.6 use true_divide instead of /= + avg[k] = torch.true_divide(avg[k], num) + print('Saving to {}'.format(args.dst_model)) + torch.save(avg, args.dst_model) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/average_dino_model.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/average_dino_model.py new file mode 100644 index 0000000000000000000000000000000000000000..c09290e5b49c356919cc01b69bd8ffbac91ee40e --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/average_dino_model.py @@ -0,0 +1,90 @@ +# Copyright (c) 2020 Mobvoi Inc (Di Wu) +# 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import glob +import re + +import torch + + +def get_args(): + parser = argparse.ArgumentParser(description='average model') + parser.add_argument('--dst_model', required=True, help='averaged model') + parser.add_argument('--src_path', + required=True, + help='src model path for average') + parser.add_argument('--num', + default=5, + type=int, + help='nums for averaged model') + parser.add_argument('--min_epoch', + default=0, + type=int, + help='min epoch used for averaging model') + parser.add_argument( + '--max_epoch', + default=65536, # Big enough + type=int, + help='max epoch used for averaging model') + args = parser.parse_args() + print(args) + return args + + +def get_t_model_state_dict(state_dict): + query_dict = {} + for key, val in state_dict.items(): + if key.startswith('t_model.'): + query_dict[key[8:]] = val + + return query_dict + + +def main(): + args = get_args() + + path_list = glob.glob('{}/[!avg][!final][!convert]*.pt'.format( + args.src_path)) + path_list = sorted( + path_list, + key=lambda p: int(re.findall(r"(?<=model_)\d*(?=.pt)", p)[0])) + path_list = path_list[-args.num:] + print(path_list) + avg = None + num = args.num + assert num == len(path_list) + for path in path_list: + print('Processing {}'.format(path)) + states = torch.load(path, map_location=torch.device('cpu')) + states = get_t_model_state_dict(states['model']) + if avg is None: + avg = states + else: + for k in avg.keys(): + avg[k] += states[k] + # average + for k in avg.keys(): + if avg[k] is not None: + # pytorch 1.6 use true_divide instead of /= + avg[k] = torch.true_divide(avg[k], num) + print('Saving to {}'.format(args.dst_model)) + torch.save(avg, args.dst_model) + + +if __name__ == '__main__': + main() diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/train_contrastive.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/train_contrastive.py new file mode 100644 index 0000000000000000000000000000000000000000..8f7cc661bbf4867370be4c2a727dbcc875f4008c --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/train_contrastive.py @@ -0,0 +1,227 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from pprint import pformat +import fire +import yaml +import tableprint as tp +import re + +import torch +import torch.distributed as dist +from torch.utils.data import DataLoader + +import wespeaker.utils.schedulers as schedulers +from wespeaker.utils.utils import get_logger, parse_config_or_kwargs, set_seed +from wespeaker.utils.checkpoint import load_checkpoint, save_checkpoint +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.ssl.models.moco_wrapper import MoCo +from wespeaker.ssl.models.simclr_wrapper import SimCLR +from wespeaker.ssl.utils.contrastive_executor import run_epoch +from wespeaker.ssl.dataset.dataset import SSLDataset, contrastive_collate_fn + + +def train(config='conf/config.yaml', **kwargs): + """Trains a model on the given features and spk labels. + + :config: A training configuration. Note that all parameters in the + config can also be manually adjusted with --ARG VALUE + :returns: None + """ + configs = parse_config_or_kwargs(config, **kwargs) + checkpoint = configs.get('checkpoint', None) + # dist configs + rank = int(os.environ['RANK']) + world_size = int(os.environ['WORLD_SIZE']) + gpu = int(configs['gpus'][rank]) + torch.cuda.set_device(gpu) + dist.init_process_group(backend='nccl') + + model_dir = os.path.join(configs['exp_dir'], "models") + if rank == 0: + try: + os.makedirs(model_dir) + except IOError: + print(model_dir + " already exists !!!") + if checkpoint is None: + exit(1) + dist.barrier(device_ids=[gpu]) # let the rank 0 mkdir first + + logger = get_logger(configs['exp_dir'], 'train.log') + if world_size > 1: + logger.info('training on multiple gpus, this gpu {}'.format(gpu)) + + if rank == 0: + logger.info("exp_dir is: {}".format(configs['exp_dir'])) + logger.info("<== Passed Arguments ==>") + # Print arguments into logs + for line in pformat(configs).split('\n'): + logger.info(line) + + # seed + set_seed(configs['seed'] + rank) + + # train data + with open(configs['wav_scp'], 'r') as f: + lines = f.readlines() + data_num = len(lines) + del lines + + if rank == 0: + logger.info("<== Data statistics ==>") + logger.info("train data num: {}".format(data_num)) + + # dataset and dataloader + train_dataset = SSLDataset(configs['data_type'], + configs['train_data'], + configs['dataset_args'], + None, + reverb_lmdb_file=configs.get( + 'reverb_data', None), + noise_lmdb_file=configs.get('noise_data', None)) + train_dataloader = DataLoader(train_dataset, + **configs['dataloader_args'], + collate_fn=contrastive_collate_fn) + batch_size = configs['dataloader_args']['batch_size'] + if configs['dataset_args'].get('sample_num_per_epoch', 0) > 0: + sample_num_per_epoch = configs['dataset_args']['sample_num_per_epoch'] + else: + sample_num_per_epoch = data_num + epoch_iter = sample_num_per_epoch // world_size // batch_size + if rank == 0: + logger.info("<== Dataloaders ==>") + logger.info("train dataloaders created") + logger.info('loader size: {}'.format(epoch_iter)) + + # model + logger.info("<== Model ==>") + model = get_speaker_model(configs['model'])(**configs['model_args']) + num_params = sum(param.numel() for param in model.parameters()) + if rank == 0: + logger.info('speaker_model size: {}'.format(num_params)) + if configs['model_init'] is not None: + logger.info('Load initial model from {}'.format(configs['model_init'])) + load_checkpoint(model, configs['model_init']) + elif checkpoint is None: + logger.info('Train model from scratch ...') + + if rank == 0: + # !!!IMPORTANT!!! + # Try to export the model by script, if fails, we should refine + # the code to satisfy the script export requirements + script_model = torch.jit.script(model) + script_model.save(os.path.join(model_dir, 'init.zip')) + + if configs['contrastive_type'] == "simclr": + configs['simclr_args']['embed_dim'] = configs['model_args'][ + 'embed_dim'] + model = SimCLR(model, **configs['simclr_args']) + else: + configs['moco_args']['embed_dim'] = configs['model_args']['embed_dim'] + model = MoCo(model, **configs['moco_args']) + + if rank == 0: + # print model + for line in pformat(model).split('\n'): + logger.info(line) + + # If specify checkpoint, load some info from checkpoint. + if checkpoint is not None: + load_checkpoint(model, checkpoint) + start_epoch = int(re.findall(r"(?<=model_)\d*(?=.pt)", + checkpoint)[0]) + 1 + logger.info('Load checkpoint: {}'.format(checkpoint)) + else: + start_epoch = 1 + logger.info('start_epoch: {}'.format(start_epoch)) + + # ddp_model + model.cuda() + ddp_model = torch.nn.parallel.DistributedDataParallel(model) + device = torch.device("cuda") + + criterion = torch.nn.CrossEntropyLoss() + configs['optimizer_args']['lr'] = configs['scheduler_args']['initial_lr'] + optimizer = getattr(torch.optim, + configs['optimizer'])(ddp_model.parameters(), + **configs['optimizer_args']) + if rank == 0: + logger.info("<== Optimizer ==>") + logger.info("optimizer is: " + configs['optimizer']) + + # scheduler + configs['scheduler_args']['num_epochs'] = configs['num_epochs'] + configs['scheduler_args']['epoch_iter'] = epoch_iter + # here, we consider the batch_size 64 as the base, the learning rate will be + # adjusted according to the batchsize and world_size used in different setup + configs['scheduler_args']['scale_ratio'] = 1.0 * world_size * configs[ + 'dataloader_args']['batch_size'] / 64 + scheduler = getattr(schedulers, + configs['scheduler'])(optimizer, + **configs['scheduler_args']) + if rank == 0: + logger.info("<== Scheduler ==>") + logger.info("scheduler is: " + configs['scheduler']) + + # save config.yaml + if rank == 0: + saved_config_path = os.path.join(configs['exp_dir'], 'config.yaml') + with open(saved_config_path, 'w') as fout: + data = yaml.dump(configs) + fout.write(data) + + # training + dist.barrier(device_ids=[gpu]) # synchronize here + if rank == 0: + logger.info("<========== Training process ==========>") + header = ['Epoch', 'Batch', 'Lr', 'Loss', "Acc"] + for line in tp.header(header, width=10, style='grid').split('\n'): + logger.info(line) + dist.barrier(device_ids=[gpu]) # synchronize here + + scaler = torch.cuda.amp.GradScaler(enabled=configs['enable_amp']) + for epoch in range(start_epoch, configs['num_epochs'] + 1): + train_dataset.set_epoch(epoch) + + run_epoch(train_dataloader, + epoch_iter, + ddp_model, + criterion, + optimizer, + scheduler, + epoch, + logger, + scaler, + enable_amp=configs['enable_amp'], + log_batch_interval=configs['log_batch_interval'], + device=device) + + if rank == 0: + if epoch % configs['save_epoch_interval'] == 0 or epoch >= configs[ + 'num_epochs'] - configs['num_avg']: + save_checkpoint( + model, os.path.join(model_dir, + 'model_{}.pt'.format(epoch))) + + if rank == 0: + os.symlink('model_{}.pt'.format(configs['num_epochs']), + os.path.join(model_dir, 'final_model.pt')) + logger.info(tp.bottom(len(header), width=10, style='grid')) + + +if __name__ == '__main__': + fire.Fire(train) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/train_dino.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/train_dino.py new file mode 100644 index 0000000000000000000000000000000000000000..0699b97e394b862b3ccf4b12d3e5f149ebf5e8a3 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/bin/train_dino.py @@ -0,0 +1,255 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from pprint import pformat +import fire +import yaml +import tableprint as tp +import re + +import torch +import torch.distributed as dist +from torch.utils.data import DataLoader + +from wespeaker.models.speaker_model import get_speaker_model +from wespeaker.utils.utils import get_logger, parse_config_or_kwargs, set_seed +from wespeaker.utils.checkpoint import load_checkpoint +from wespeaker.ssl.dataset.dataset import SSLDataset, dino_collate_fn +from wespeaker.ssl.models.dino_wrapper import DINO +from wespeaker.ssl.utils.dino_executor import run_epoch +from wespeaker.ssl.utils.dino_utils import ( + restart_from_checkpoint, + get_params_groups, + save_checkpoint, + cosine_scheduler, + LARS, +) + + +def train(config='conf/config.yaml', **kwargs): + """Trains a model on the given features and spk labels. + + :config: A training configuration. Note that all parameters in the + config can also be manually adjusted with --ARG VALUE + :returns: None + """ + configs = parse_config_or_kwargs(config, **kwargs) + checkpoint = configs.get('checkpoint', None) + # dist configs + rank = int(os.environ['RANK']) + world_size = int(os.environ['WORLD_SIZE']) + gpu = int(configs['gpus'][rank]) + torch.cuda.set_device(gpu) + dist.init_process_group(backend='nccl') + + model_dir = os.path.join(configs['exp_dir'], "models") + if rank == 0: + try: + os.makedirs(model_dir) + except IOError: + print(model_dir + " already exists !!!") + if checkpoint is None: + exit(1) + dist.barrier(device_ids=[gpu]) # let the rank 0 mkdir first + + logger = get_logger(configs['exp_dir'], 'train.log') + if world_size > 1: + logger.info('training on multiple gpus, this gpu {}'.format(gpu)) + + if rank == 0: + logger.info("exp_dir is: {}".format(configs['exp_dir'])) + logger.info("<== Passed Arguments ==>") + # Print arguments into logs + for line in pformat(configs).split('\n'): + logger.info(line) + + # seed + set_seed(configs['seed'] + rank) + + # train data + with open(configs['wav_scp'], 'r') as f: + lines = f.readlines() + data_num = len(lines) + del lines + + if rank == 0: + logger.info("<== Data statistics ==>") + logger.info("train data num: {}".format(data_num)) + + # dataset and dataloader + train_dataset = SSLDataset(configs['data_type'], + configs['train_data'], + configs['dataset_args'], + None, + reverb_lmdb_file=configs.get( + 'reverb_data', None), + noise_lmdb_file=configs.get('noise_data', None)) + train_dataloader = DataLoader(train_dataset, + **configs['dataloader_args'], + collate_fn=dino_collate_fn) + batch_size = configs['dataloader_args']['batch_size'] + if configs['dataset_args'].get('sample_num_per_epoch', 0) > 0: + sample_num_per_epoch = configs['dataset_args']['sample_num_per_epoch'] + else: + sample_num_per_epoch = data_num + epoch_iter = sample_num_per_epoch // world_size // batch_size + if rank == 0: + logger.info("<== Dataloaders ==>") + logger.info("train dataloaders created") + logger.info('loader size: {}'.format(epoch_iter)) + + # model + logger.info("<== Model ==>") + model = get_speaker_model(configs['model'])(**configs['model_args']) + num_params = sum(param.numel() for param in model.parameters()) + if rank == 0: + logger.info('speaker_model size: {}'.format(num_params)) + if configs['model_init'] is not None: + logger.info('Load initial model from {}'.format(configs['model_init'])) + load_checkpoint(model, configs['model_init']) + elif checkpoint is None: + logger.info('Train model from scratch ...') + + if rank == 0: + # !!!IMPORTANT!!! + # Try to export the model by script, if fails, we should refine + # the code to satisfy the script export requirements + script_model = torch.jit.script(model) + script_model.save(os.path.join(model_dir, 'init.zip')) + + # init student and teacher model for DINO training + configs['dino_head_args']['in_dim'] = configs['model_args']['embed_dim'] + chunk_info_args = configs['dataset_args']['chunk_info_args'] + configs['dino_loss_args']['out_dim'] = configs['dino_head_args']['out_dim'] + configs['dino_loss_args']['n_scrops'] = chunk_info_args[ + 'global_chunk_num'] + chunk_info_args['local_chunk_num'] + configs['dino_loss_args']['n_tcrops'] = chunk_info_args['global_chunk_num'] + configs['dino_loss_args']['nepochs'] = configs['num_epochs'] + model = DINO( + model, + dino_head_args=configs['dino_head_args'], + dino_loss_args=configs['dino_loss_args'], + sync_bn=configs.get('sync_bn', True), + ) + + if rank == 0: + # print model + for line in pformat(model).split('\n'): + logger.info(line) + + # ddp_model + model.cuda() + ddp_model = torch.nn.parallel.DistributedDataParallel( + model, broadcast_buffers=False) + device = torch.device("cuda") + + params_groups = get_params_groups(ddp_model) + if configs['optim_type'] == "adamw": + optimizer = torch.optim.AdamW(params_groups) + elif configs['optim_type'] == 'sgd': + optimizer = torch.optim.SGD(params_groups, lr=0, momentum=0.9) + elif configs['optim_type'] == 'lars': + optimizer = LARS(params_groups) + + # If specify checkpoint, load some info from checkpoint. + if checkpoint is not None: + restart_from_checkpoint( + checkpoint, + model=model, + optimizer=optimizer, + ) + start_epoch = int(re.findall(r"(?<=model_)\d*(?=.pt)", + checkpoint)[0]) + 1 + logger.info('Load checkpoint: {}'.format(checkpoint)) + else: + start_epoch = 1 + logger.info('start_epoch: {}'.format(start_epoch)) + + # init all the schedulers + scheduler_args = configs['scheduler_args'] + lr_scale_ratio = 1.0 * batch_size * world_size / 256. + lr_schedule = cosine_scheduler( + base_value=scheduler_args['lr'] * lr_scale_ratio, + final_value=scheduler_args['min_lr'] * lr_scale_ratio, + epochs=configs['num_epochs'], + niter_per_ep=epoch_iter, + warmup_epochs=scheduler_args['warmup_epochs'], + ) + wd_schedule = cosine_scheduler( + base_value=scheduler_args['weight_decay'], + final_value=scheduler_args['weight_decay_end'], + epochs=configs['num_epochs'], + niter_per_ep=epoch_iter, + ) + mt_schedule = cosine_scheduler( + base_value=scheduler_args['momentum_teacher'], + final_value=1, + epochs=configs['num_epochs'], + niter_per_ep=epoch_iter, + ) + + # save config.yaml + if rank == 0: + saved_config_path = os.path.join(configs['exp_dir'], 'config.yaml') + with open(saved_config_path, 'w') as fout: + data = yaml.dump(configs) + fout.write(data) + + # training + dist.barrier(device_ids=[gpu]) # synchronize here + if rank == 0: + logger.info("<========== Training process ==========>") + header = ['Epoch', 'Batch', 'Lr', 'Loss'] + for line in tp.header(header, width=10, style='grid').split('\n'): + logger.info(line) + dist.barrier(device_ids=[gpu]) # synchronize here + + scaler = torch.cuda.amp.GradScaler(enabled=configs['enable_amp']) + for epoch in range(start_epoch, configs['num_epochs'] + 1): + train_dataset.set_epoch(epoch) + + run_epoch(train_dataloader, + epoch_iter, + ddp_model, + optimizer, + lr_schedule, + wd_schedule, + mt_schedule, + epoch, + logger, + scaler, + clip_grad=configs['clip_grad'], + freeze_last_layer=configs['freeze_last_layer'], + enable_amp=configs['enable_amp'], + log_batch_interval=configs['log_batch_interval'], + device=device) + + if rank == 0: + if epoch % configs['save_epoch_interval'] == 0 or epoch >= configs[ + 'num_epochs'] - configs['num_avg']: + save_checkpoint( + model, optimizer, + os.path.join(model_dir, 'model_{}.pt'.format(epoch))) + + if rank == 0: + os.symlink('model_{}.pt'.format(configs['num_epochs']), + os.path.join(model_dir, 'final_model.pt')) + logger.info(tp.bottom(len(header), width=10, style='grid')) + + +if __name__ == '__main__': + fire.Fire(train) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/dataset/dataset.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/dataset/dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..36f895a88e49193d8e4322e53d85cb99faf54054 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/dataset/dataset.py @@ -0,0 +1,165 @@ +# Copyright (c) 2021 Mobvoi Inc. (authors: Binbin Zhang) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch + +from wespeaker.utils.file_utils import read_lists +from wespeaker.dataset.lmdb_data import LmdbData +import wespeaker.dataset.processor as processor +import wespeaker.ssl.dataset.processor as ssl_processor +from wespeaker.dataset.dataset import Processor, DataList + + +def dino_collate_fn(batch): + key_list, label_list = [], [] + local_chunks_list, global_chunks_list = [], [] + + for sample_dict in batch: + key_list.append(sample_dict['key']) + label_list.append(sample_dict['label']) + local_chunks_list.append( + torch.stack(sample_dict['feat']['local_chunks'])) + global_chunks_list.append( + torch.stack(sample_dict['feat']['global_chunks'])) + + return dict( + key=key_list, + label=label_list, + local_chunks=torch.stack(local_chunks_list), + global_chunks=torch.stack(global_chunks_list), + ) + + +def contrastive_collate_fn(batch): + local_chunks_list, global_chunks_list = [], [] + + for sample_dict in batch: + local_chunks_list.append( + torch.stack(sample_dict['feat']['local_chunks'])) + global_chunks_list.append( + torch.stack(sample_dict['feat']['global_chunks'])) + + return dict( + keys=torch.stack(local_chunks_list), + queries=torch.stack(global_chunks_list), + ) + + +def SSLDataset(data_type, + data_list_file, + configs, + spk2id_dict, + whole_utt=False, + reverb_lmdb_file=None, + noise_lmdb_file=None): + """ Construct dataset from arguments + + We have two shuffle stage in the Dataset. The first is global + shuffle at shards tar/raw/feat file level. The second is local shuffle + at training samples level. + + Args: + data_type(str): shard/raw/feat + data_list_file: data list file + configs: dataset configs + spk2id_dict: spk2id dict + reverb_lmdb_file: reverb data source lmdb file + noise_lmdb_file: noise data source lmdb file + whole_utt: use whole utt or random chunk + """ + assert data_type in ['shard', 'raw', 'feat'] + lists = read_lists(data_list_file) + shuffle = configs.get('shuffle', False) + # Global shuffle + dataset = DataList(lists, shuffle=shuffle) + if data_type == 'shard': + dataset = Processor(dataset, processor.url_opener) + dataset = Processor(dataset, processor.tar_file_and_group) + elif data_type == 'raw': + dataset = Processor(dataset, processor.parse_raw) + else: + dataset = Processor(dataset, processor.parse_feat) + + if configs.get('filter', True): + # Filter the data with unwanted length + filter_conf = configs.get('filter_args', {}) + dataset = Processor(dataset, + processor.filter, + frame_shift=configs['fbank_args'].get( + 'frame_shift', 10), + data_type=data_type, + **filter_conf) + + # Local shuffle + if shuffle: + dataset = Processor(dataset, processor.shuffle, + **configs['shuffle_args']) + + # spk2id + dataset = Processor(dataset, ssl_processor.spk_to_id, spk2id_dict) + + if data_type == 'feat': + if not whole_utt: + # random chunk + chunk_info_args = configs['chunk_info_args'] + chunk_info_args['data_type'] = 'feat' + dataset = Processor(dataset, ssl_processor.random_chunk_for_dino, + **chunk_info_args) + else: + # resample + resample_rate = configs.get('resample_rate', 16000) + dataset = Processor(dataset, processor.resample, resample_rate) + # speed perturb + speed_perturb_flag = configs.get('speed_perturb', True) + if speed_perturb_flag: + spk_num = len(spk2id_dict) if spk2id_dict is not None else 0 + dataset = Processor(dataset, processor.speed_perturb, spk_num) + if not whole_utt: + # random chunk + frame_shift = configs['fbank_args'].get('frame_shift', 10) + frame_length = configs['fbank_args'].get('frame_length', 25) + chunk_info_args = configs['chunk_info_args'] + for key in chunk_info_args: + if 'chunk_len' in key: + chunk_info_args[key] = ( + (chunk_info_args[key] - 1) * frame_shift + + frame_length) * resample_rate // 1000 + chunk_info_args['data_type'] = data_type + dataset = Processor(dataset, ssl_processor.random_chunk_for_dino, + **chunk_info_args) + # add reverb & noise + aug_prob = configs.get('aug_prob', 0.6) + if (reverb_lmdb_file and noise_lmdb_file) and (aug_prob > 0.0): + reverb_data = LmdbData(reverb_lmdb_file) + noise_data = LmdbData(noise_lmdb_file) + dataset = Processor(dataset, ssl_processor.add_reverb_noise, + reverb_data, noise_data, resample_rate, + aug_prob) + # compute fbank + dataset = Processor(dataset, ssl_processor.compute_fbank, + **configs['fbank_args']) + + # apply cmvn + dataset = Processor(dataset, ssl_processor.apply_cmvn) + + # spec augmentation + spec_aug_flag = configs.get('spec_aug', True) + if spec_aug_flag: + dataset = Processor(dataset, ssl_processor.spec_aug, + **configs['spec_aug_args']) + + return dataset diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/dataset/processor.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/dataset/processor.py new file mode 100644 index 0000000000000000000000000000000000000000..1fc416cf2098bdd0407ad7382713f46622b313d0 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/dataset/processor.py @@ -0,0 +1,355 @@ +# Copyright (c) 2021 Mobvoi Inc. (authors: Binbin Zhang) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import random + +import numpy as np +from scipy import signal +from scipy.io import wavfile +import torch +import torchaudio.compliance.kaldi as kaldi +from wespeaker.dataset.processor import ( + get_random_chunk, ) + + +def spk_to_id(data, spk2id): + """ Parse spk id + + Args: + data: Iterable[{key, wav/feat, spk}] + spk2id: Dict[str, int] + + Returns: + Iterable[{key, wav/feat, label}] + """ + for sample in data: + if spk2id and ('spk' in sample) and (sample['spk'] in spk2id): + label = spk2id[sample['spk']] + else: + label = -1 + sample['label'] = label + yield sample + + +def random_chunk_for_dino(data, + global_chunk_len, + global_chunk_num, + local_chunk_len, + local_chunk_num, + data_type='shard/raw/feat'): + """ + Following the strategy in https://arxiv.org/pdf/2104.14294.pdf, + and https://arxiv.org/abs/2210.15936, several global and local + chunks are sampled from each utterance for DINO training. + Args: + data: Iterable[{key, wav/feat, label}] + global_chunk_len: chunk length for global chunk + global_chunk_num: chunk number for global chunk + local_chunk_len: chunk length for local chunk + local_chunk_num: chunk number for local chunk + + Returns: + Iterable[{key, wav/feat, label}] + """ + for sample in data: + assert 'key' in sample + + if data_type == 'feat': + assert 'feat' in sample + feat = sample['feat'] + sample['feat'] = {'local_chunks': [], 'global_chunks': []} + for i in range(local_chunk_num): + sample['feat']['local_chunks'].append( + get_random_chunk(feat, local_chunk_len)) + for i in range(global_chunk_num): + sample['feat']['global_chunks'].append( + get_random_chunk(feat, global_chunk_len)) + else: + assert 'wav' in sample + wav = sample['wav'][0] + sample['wav'] = {'local_chunks': [], 'global_chunks': []} + for i in range(local_chunk_num): + sample['wav']['local_chunks'].append( + get_random_chunk(wav, local_chunk_len).unsqueeze(0)) + for i in range(global_chunk_num): + sample['wav']['global_chunks'].append( + get_random_chunk(wav, global_chunk_len).unsqueeze(0)) + yield sample + + +def add_reverb(audio, reverb_source, resample_rate=16000): + """ Add reverb + + Args: + audio: numpy.array (audio_len, ) + reverb_source: reverb LMDB data source + resample_rate: resample rate for reverb/noise data + Returns: + numpy.array (audio_len, ) + + """ + audio_len = audio.shape[0] + + _, rir_data = reverb_source.random_one() + rir_sr, rir_audio = wavfile.read(io.BytesIO(rir_data)) + rir_audio = rir_audio.astype(np.float32) + if rir_sr != resample_rate: + rir_audio = signal.resample( + rir_audio, int(len(rir_audio) / rir_sr * resample_rate)) + rir_audio = rir_audio / np.sqrt(np.sum(rir_audio**2)) + out_audio = signal.convolve(audio, rir_audio, mode='full')[:audio_len] + + return out_audio + + +def add_noise(audio, noise_source, resample_rate=16000): + """ Add reverb + + Args: + audio: numpy.array (audio_len, ) + noise_source: noise LMDB data source + resample_rate: resample rate for reverb/noise data + Returns: + numpy.array (audio_len, ) + + """ + audio_len = audio.shape[0] + audio_db = 10 * np.log10(np.mean(audio**2) + 1e-4) + + key, noise_data = noise_source.random_one() + if key.startswith('noise'): + snr_range = [0, 15] + elif key.startswith('speech'): + snr_range = [10, 30] + elif key.startswith('music'): + snr_range = [5, 15] + else: + snr_range = [0, 15] + noise_sr, noise_audio = wavfile.read(io.BytesIO(noise_data)) + noise_audio = noise_audio.astype(np.float32) / (1 << 15) + if noise_sr != resample_rate: + # Since the noise audio could be very long, it must be + # chunked first before resampled (to save time) + noise_audio = get_random_chunk( + noise_audio, int(audio_len / resample_rate * noise_sr)) + noise_audio = signal.resample(noise_audio, audio_len) + else: + noise_audio = get_random_chunk(noise_audio, audio_len) + noise_snr = random.uniform(snr_range[0], snr_range[1]) + noise_db = 10 * np.log10(np.mean(noise_audio**2) + 1e-4) + noise_audio = np.sqrt(10**( + (audio_db - noise_db - noise_snr) / 10)) * noise_audio + out_audio = audio + noise_audio + + return out_audio + + +def add_reverb_noise(data, + reverb_source, + noise_source, + resample_rate=16000, + aug_prob=0.6): + """ Add reverb & noise aug + + Args: + data: Iterable[{key, wav, label, sample_rate}] + reverb_source: reverb LMDB data source + noise_source: noise LMDB data source + resample_rate: resample rate for reverb/noise data + aug_prob: aug probability + + Returns: + Iterable[{key, wav, label, sample_rate}] + """ + + def aug_for_an_audio(audio): + """ Add reverb & noise aug for and audio + + Args: + audio: torch.Tensor (1, audio_len) + Returns: + torch.Tensor (1, audio_len) + """ + if aug_prob > random.random(): + audio = audio.numpy()[0] + aug_type = random.randint(1, 2) + if aug_type == 1: + # add reverberation + out_audio = add_reverb(audio, reverb_source, resample_rate) + else: + # add additive noise + out_audio = add_noise(audio, noise_source, resample_rate) + + # normalize into [-1, 1] + out_audio = out_audio / (np.max(np.abs(out_audio)) + 1e-4) + return torch.from_numpy(out_audio).unsqueeze(0) + else: + return audio + + for sample in data: + assert 'wav' in sample + assert 'key' in sample + if isinstance(sample['wav'], dict): + # for self supervised training, many chunks are sampled + # from each utterance. + # sample['wav'] = {'chunk_type':[chunk1, chunk2, ...], ...} + for key in sample['wav']: + for i, audio in enumerate(sample['wav'][key]): + sample['wav'][key][i] = aug_for_an_audio(audio) + else: + sample['wav'] = aug_for_an_audio(sample['wav']) + yield sample + + +def compute_fbank(data, + num_mel_bins=80, + frame_length=25, + frame_shift=10, + dither=1.0): + """ Extract fbank + + Args: + data: Iterable[{key, wav, label, sample_rate}] + + Returns: + Iterable[{key, feat, label, sample_rate}] + """ + + def compute_fbank_for_an_audio(waveform): + + waveform = waveform * (1 << 15) + # Only keep key, feat, label + mat = kaldi.fbank(waveform, + num_mel_bins=num_mel_bins, + frame_length=frame_length, + frame_shift=frame_shift, + dither=dither, + sample_frequency=sample_rate, + window_type='hamming', + use_energy=False) + return mat + + for sample in data: + assert 'sample_rate' in sample + assert 'wav' in sample + assert 'key' in sample + assert 'label' in sample + sample_rate = sample['sample_rate'] + if isinstance(sample['wav'], dict): + # for self supervised training, many chunks are sampled + # from each utterance. + # sample['wav'] = {'chunk_type':[chunk1, chunk2, ...], ...} + feat_dict = {} + for key in sample['wav']: + feat_dict[key] = [] + for waveform in sample['wav'][key]: + feat_dict[key].append(compute_fbank_for_an_audio(waveform)) + mat = feat_dict + else: + waveform = sample['wav'] + mat = compute_fbank_for_an_audio(waveform) + yield dict(key=sample['key'], label=sample['label'], feat=mat) + + +def apply_cmvn(data, norm_mean=True, norm_var=False): + """ Apply CMVN + + Args: + data: Iterable[{key, feat, label}] + + Returns: + Iterable[{key, feat, label}] + """ + + def apply_cmvn_for_a_feat(mat): + if norm_mean: + mat = mat - torch.mean(mat, dim=0) + if norm_var: + mat = mat / torch.sqrt(torch.var(mat, dim=0) + 1e-8) + return mat + + for sample in data: + assert 'key' in sample + assert 'feat' in sample + assert 'label' in sample + if isinstance(sample['feat'], dict): + # for self supervised training, many chunks are sampled + # from each utterance. + # sample['feat'] = {'chunk_type':[chunk1, chunk2, ...], ...} + for key in sample['feat']: + for i, mat in enumerate(sample['feat'][key]): + sample['feat'][key][i] = apply_cmvn_for_a_feat(mat) + mat = sample['feat'] + else: + mat = sample['feat'] + mat = apply_cmvn_for_a_feat(mat) + yield dict(key=sample['key'], label=sample['label'], feat=mat) + + +def spec_aug(data, num_t_mask=1, num_f_mask=1, max_t=10, max_f=8, prob=0.6): + """ Do spec augmentation + Inplace operation + + Args: + data: Iterable[{key, feat, label}] + num_t_mask: number of time mask to apply + num_f_mask: number of freq mask to apply + max_t: max width of time mask + max_f: max width of freq mask + prob: prob of spec_aug + + Returns + Iterable[{key, feat, label}] + """ + + def spec_aug_for_a_feat(x): + if random.random() < prob: + assert isinstance(x, torch.Tensor) + # y = x.clone().detach() + y = x.detach() # inplace operation + max_frames = y.size(0) + max_freq = y.size(1) + # time mask + for i in range(num_t_mask): + start = random.randint(0, max_frames - 1) + length = random.randint(1, max_t) + end = min(max_frames, start + length) + y[start:end, :] = 0 + # freq mask + for i in range(num_f_mask): + start = random.randint(0, max_freq - 1) + length = random.randint(1, max_f) + end = min(max_freq, start + length) + y[:, start:end] = 0 + return y + else: + return x + + for sample in data: + assert 'feat' in sample + if isinstance(sample['feat'], dict): + # for self supervised training, many chunks are sampled + # from each utterance. + # sample['feat'] = {'chunk_type':[chunk1, chunk2, ...], ...} + for key in sample['feat']: + for i, x in enumerate(sample['feat'][key]): + sample['feat'][key][i] = spec_aug_for_a_feat(x) + else: + x = sample['feat'] + sample['feat'] = spec_aug_for_a_feat(x) + yield sample diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/models/dino_wrapper.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/models/dino_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..82df45623581656513537ce3aac7c3925e71d3bb --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/models/dino_wrapper.py @@ -0,0 +1,301 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2023 Bing Han (hanbing97@sjtu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Misc functions. + +Mostly copy-paste from the public repo dino: +https://github.com/facebookresearch/dino/blob/main/utils.py +""" +import math +import copy + +import numpy as np +import torch +from torch import Tensor +from torch import nn +import torch.distributed as dist +import torch.nn.functional as F +import warnings + + +def _no_grad_trunc_normal_(tensor, mean, std, a, b): + # Cut & paste from PyTorch official master until it's in a few official + # releases - RW + # Method based on + # https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf + def norm_cdf(x): + # Computes standard normal cumulative distribution function + return (1. + math.erf(x / math.sqrt(2.))) / 2. + + if (mean < a - 2 * std) or (mean > b + 2 * std): + warnings.warn( + "mean is more than 2 std from [a, b] in nn.init.trunc_normal_. " + "The distribution of values may be incorrect.", + stacklevel=2) + + with torch.no_grad(): + # Values are generated by using a truncated uniform distribution and + # then using the inverse CDF for the normal distribution. + # Get upper and lower cdf values + l = norm_cdf((a - mean) / std) + u = norm_cdf((b - mean) / std) + + # Uniformly fill tensor with values from [l, u], then translate to + # [2l-1, 2u-1]. + tensor.uniform_(2 * l - 1, 2 * u - 1) + + # Use inverse cdf transform for normal distribution to get truncated + # standard normal + tensor.erfinv_() + + # Transform to proper mean, std + tensor.mul_(std * math.sqrt(2.)) + tensor.add_(mean) + + # Clamp to ensure it's in the proper range + tensor.clamp_(min=a, max=b) + return tensor + + +def trunc_normal_(tensor, mean=0., std=1., a=-2., b=2.): + # type: (Tensor, float, float, float, float) -> Tensor + return _no_grad_trunc_normal_(tensor, mean, std, a, b) + + +class DINOHead(nn.Module): + + def __init__(self, + in_dim, + out_dim, + use_bn=False, + norm_last_layer=True, + nlayers=3, + hidden_dim=2048, + bottleneck_dim=256, + normalize_input=False): + super().__init__() + self.normalize_input = normalize_input + + if nlayers == 0: + self.mlp = nn.Identity() + elif nlayers == 1: + self.mlp = nn.Linear(in_dim, bottleneck_dim) + else: + layers = [nn.Linear(in_dim, hidden_dim)] + if use_bn: + layers.append(nn.BatchNorm1d(hidden_dim)) + layers.append(nn.GELU()) + for _ in range(nlayers - 2): + layers.append(nn.Linear(hidden_dim, hidden_dim)) + if use_bn: + layers.append(nn.BatchNorm1d(hidden_dim)) + layers.append(nn.GELU()) + layers.append(nn.Linear(hidden_dim, bottleneck_dim)) + self.mlp = nn.Sequential(*layers) + self.apply(self._init_weights) + self.last_layer = nn.utils.weight_norm( + nn.Linear(bottleneck_dim, out_dim, bias=False)) + self.last_layer.weight_g.data.fill_(1) + if norm_last_layer: + self.last_layer.weight_g.requires_grad = False + + def _init_weights(self, m): + if isinstance(m, nn.Linear): + trunc_normal_(m.weight, std=.02) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, x, return_mlp=False): + if self.normalize_input: + x = nn.functional.normalize(x, dim=-1, p=2) + x = self.mlp(x) + if return_mlp: + return x + x = nn.functional.normalize(x, dim=-1, p=2) + x = self.last_layer(x) + return x + + +class DINOLoss(nn.Module): + + def __init__(self, + out_dim, + n_scrops, + n_tcrops, + warmup_teacher_temp, + teacher_temp, + nepochs, + warmup_teacher_temp_epochs_ratio=0.2, + student_temp=0.1, + center_momentum=0.9): + super().__init__() + self.student_temp = student_temp + self.center_momentum = center_momentum + self.n_scrops = n_scrops + self.n_tcrops = n_tcrops + self.register_buffer("center", torch.zeros(1, out_dim)) + # we apply a warm up for the teacher temperature because + # a too high temperature makes the training instable at the beginning + + warmup_teacher_temp_epochs = int(nepochs * + warmup_teacher_temp_epochs_ratio) + self.teacher_temp_schedule = np.concatenate( + (np.linspace(warmup_teacher_temp, teacher_temp, + warmup_teacher_temp_epochs), + np.ones(nepochs - warmup_teacher_temp_epochs) * teacher_temp)) + + self.student_entropy = 0.0 + self.teacher_entropy = 0.0 + + def forward(self, student_output, teacher_output, epoch, mode=0): + """ + Cross-entropy between softmax outputs of the + teacher and student networks. + """ + student_out = student_output / self.student_temp + student_tmp = student_out.detach() + student_out = student_out.chunk(self.n_scrops) + + # teacher centering and sharpening + temp = self.teacher_temp_schedule[epoch] + teacher_out = F.softmax((teacher_output - self.center) / temp, dim=-1) + teacher_tmp = (teacher_output.detach() - self.center) / temp + teacher_out = teacher_out.detach().chunk(self.n_tcrops) + + student_tmp = F.softmax(student_tmp, dim=1) + 1e-7 + teacher_tmp = F.softmax(teacher_tmp, dim=1) + 1e-7 + self.student_entropy = torch.mean( + torch.sum(-student_tmp * torch.log(student_tmp), dim=1)).item() + self.teacher_entropy = torch.mean( + torch.sum(-teacher_tmp * torch.log(teacher_tmp), dim=1)).item() + + total_loss = 0 + n_loss_terms = 0 + for iq, q in enumerate(teacher_out): + for v in range(len(student_out)): + if mode == 0: # skip the same + if v == iq: + continue + elif mode == 1: # only the channel invariant + if v != iq: + continue + elif mode == 2: # only the content invariant + if v < 2: + continue + loss = torch.sum(-q * F.log_softmax(student_out[v], dim=-1), + dim=-1) + total_loss += loss.mean() + n_loss_terms += 1 + total_loss /= n_loss_terms + self.update_center(teacher_output) + return total_loss + + @torch.no_grad() + def update_center(self, teacher_output): + """ + Update center used for teacher output. + """ + batch_center = torch.sum(teacher_output, dim=0, keepdim=True) + dist.all_reduce(batch_center) + batch_center = batch_center / (len(teacher_output) * get_world_size()) + # ema update + self.center = self.center * self.center_momentum + batch_center * ( + 1 - self.center_momentum) + + +def is_dist_avail_and_initialized(): + if not dist.is_available(): + return False + if not dist.is_initialized(): + return False + return True + + +def get_world_size(): + if not is_dist_avail_and_initialized(): + return 1 + return dist.get_world_size() + + +class DINO(nn.Module): + """ + https://arxiv.org/abs/2104.14294 + """ + + def __init__(self, + base_model, + dino_head_args, + dino_loss_args, + sync_bn=True): + """ + model: the student and teacher base model + """ + super(DINO, self).__init__() + + # get the student and teacher model + self.s_model = base_model + self.t_model = copy.deepcopy(base_model) + + self.s_model.add_module("projection_head", DINOHead(**dino_head_args)) + self.t_model.add_module("projection_head", DINOHead(**dino_head_args)) + self.t_model.projection_head.load_state_dict( + self.s_model.projection_head.state_dict()) + + if sync_bn: + self.s_model = nn.SyncBatchNorm.convert_sync_batchnorm( + self.s_model) + self.t_model = nn.SyncBatchNorm.convert_sync_batchnorm( + self.t_model) + + # the teacher model is not updated from back propagation + for p in self.t_model.parameters(): + p.requires_grad = False + + # init dino loss + self.dino_loss_calculator = DINOLoss(**dino_loss_args) + + @torch.no_grad() + def ema_update(self, m=0.0): + for param_q, param_k in zip(self.s_model.parameters(), + self.t_model.parameters()): + param_k.data.mul_(m).add_((1 - m) * param_q.detach().data) + + def forward(self, local_feats, global_feats, epoch=0): + """ + Input: + local_feats: (chunk_num * B, T, F) + global_feats: (chunk_num' * B, T, F) + Output: + loss: a scalar value + """ + # feed global and local features into student model + g_outputs = self.s_model(global_feats) + l_outputs = self.s_model(local_feats) + g_output = g_outputs[-1] if isinstance(g_outputs, tuple) else g_outputs + l_output = l_outputs[-1] if isinstance(l_outputs, tuple) else l_outputs + s_output = torch.cat([g_output, l_output]) + s_output = self.s_model.projection_head(s_output) + # feed global features into teacher model + with torch.no_grad(): + t_outputs = self.t_model(global_feats) + t_output = t_outputs[-1] if isinstance(t_outputs, + tuple) else t_outputs + t_output = self.t_model.projection_head(t_output) + + # compute CE loss + loss = self.dino_loss_calculator(s_output, t_output, epoch) + + return loss diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/models/moco_wrapper.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/models/moco_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..e9c316c4c35bfb6cf061d82ac6161c78079567a1 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/models/moco_wrapper.py @@ -0,0 +1,205 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. + +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +import torch +import torch.nn as nn +import copy +""" +Directly copied from + https://github.com/facebookresearch/moco/blob/main/moco/builder.py +""" + + +class MoCo(nn.Module): + """ + Build a MoCo model with: a query encoder, a key encoder, and a queue + https://arxiv.org/abs/1911.05722 + """ + + def __init__(self, + encoder, + embed_dim=256, + K=65536, + m=0.999, + T=0.07, + mlp=False): + """ + K: queue size; number of negative keys (default: 65536) + m: moco momentum of updating key encoder (default: 0.999) + T: softmax temperature (default: 0.07) + """ + super(MoCo, self).__init__() + + self.K = K + self.m = m + self.T = T + + # create the encoders + # num_classes is the output fc dimension + self.encoder_q = encoder + self.encoder_k = copy.deepcopy(encoder) + + if mlp: + self.encoder_q.add_module( + "mlp", + nn.Sequential(nn.Linear(embed_dim, embed_dim), nn.ReLU(), + nn.Linear(embed_dim, embed_dim))) + self.encoder_k.add_module( + "mlp", + nn.Sequential(nn.Linear(embed_dim, embed_dim), nn.ReLU(), + nn.Linear(embed_dim, embed_dim))) + else: + self.encoder_q.add_module("mlp", nn.Sequential()) + self.encoder_k.add_module("mlp", nn.Sequential()) + + for param_q, param_k in zip(self.encoder_q.parameters(), + self.encoder_k.parameters()): + param_k.data.copy_(param_q.data) # initialize + param_k.requires_grad = False # not update by gradient + + # create the queue + self.register_buffer("queue", torch.randn(embed_dim, K)) + self.queue = nn.functional.normalize(self.queue, dim=0) + + self.register_buffer("queue_ptr", torch.zeros(1, dtype=torch.long)) + + @torch.no_grad() + def _momentum_update_key_encoder(self): + """ + Momentum update of the key encoder + """ + for param_q, param_k in zip(self.encoder_q.parameters(), + self.encoder_k.parameters()): + param_k.data = param_k.data * self.m + param_q.data * (1.0 - + self.m) + + @torch.no_grad() + def _dequeue_and_enqueue(self, keys): + # gather keys before updating queue + keys = concat_all_gather(keys) + + batch_size = keys.shape[0] + + ptr = int(self.queue_ptr) + assert self.K % batch_size == 0 # for simplicity + + # replace the keys at ptr (dequeue and enqueue) + self.queue[:, ptr:ptr + batch_size] = keys.T + ptr = (ptr + batch_size) % self.K # move pointer + + self.queue_ptr[0] = ptr + + @torch.no_grad() + def _batch_shuffle_ddp(self, x): + """ + Batch shuffle, for making use of BatchNorm. + *** Only support DistributedDataParallel (DDP) model. *** + """ + # gather from all gpus + batch_size_this = x.shape[0] + x_gather = concat_all_gather(x) + batch_size_all = x_gather.shape[0] + + num_gpus = batch_size_all // batch_size_this + + # random shuffle index + idx_shuffle = torch.randperm(batch_size_all).to(x.device) + + # broadcast to all gpus + torch.distributed.broadcast(idx_shuffle, src=0) + + # index for restoring + idx_unshuffle = torch.argsort(idx_shuffle) + + # shuffled index for this gpu + gpu_idx = torch.distributed.get_rank() + idx_this = idx_shuffle.view(num_gpus, -1)[gpu_idx] + + return x_gather[idx_this], idx_unshuffle + + @torch.no_grad() + def _batch_unshuffle_ddp(self, x, idx_unshuffle): + """ + Undo batch shuffle. + *** Only support DistributedDataParallel (DDP) model. *** + """ + # gather from all gpus + batch_size_this = x.shape[0] + x_gather = concat_all_gather(x) + batch_size_all = x_gather.shape[0] + + num_gpus = batch_size_all // batch_size_this + + # restored index for this gpu + gpu_idx = torch.distributed.get_rank() + idx_this = idx_unshuffle.view(num_gpus, -1)[gpu_idx] + + return x_gather[idx_this] + + def forward(self, input_q, input_k): + """ + Input: + input_q: a batch of query inputs + input_k: a batch of key inputs + Output: + logits, targets + """ + + # compute query features + q = self.encoder_q(input_q) # queries: NxC + q = self.encoder_q.mlp(q) + q = nn.functional.normalize(q, dim=1) + + # compute key features + with torch.no_grad(): # no gradient to keys + self._momentum_update_key_encoder() # update the key encoder + + # shuffle for making use of BN + input_k, idx_unshuffle = self._batch_shuffle_ddp(input_k) + + k = self.encoder_k(input_k) # keys: NxC + k = self.encoder_k.mlp(k) + k = nn.functional.normalize(k, dim=1) + + # undo shuffle + k = self._batch_unshuffle_ddp(k, idx_unshuffle) + + # compute logits + # Einstein sum is more intuitive + # positive logits: Nx1 + l_pos = torch.einsum('nc,nc->n', [q, k]).unsqueeze(-1) + # negative logits: NxK + l_neg = torch.einsum('nc,ck->nk', [q, self.queue.clone().detach()]) + + # logits: Nx(1+K) + logits = torch.cat([l_pos, l_neg], dim=1) + + # apply temperature + logits /= self.T + + # labels: positive key indicators + labels = torch.zeros(logits.shape[0], + dtype=torch.long).to(input_q.device) + + # dequeue and enqueue + self._dequeue_and_enqueue(k) + + return logits, labels + + +# utils +@torch.no_grad() +def concat_all_gather(tensor): + """ + Performs all_gather operation on the provided tensors. + *** Warning ***: torch.distributed.all_gather has no gradient. + """ + tensors_gather = [ + torch.ones_like(tensor) + for _ in range(torch.distributed.get_world_size()) + ] + torch.distributed.all_gather(tensors_gather, tensor, async_op=False) + + output = torch.cat(tensors_gather, dim=0) + return output diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/models/simclr_wrapper.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/models/simclr_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..8f97cf544b6fc48c3ac6d6004232a8d65c07d55e --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/models/simclr_wrapper.py @@ -0,0 +1,102 @@ +# Copyright (c) 2023, Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class SimCLR(nn.Module): + """ + https://arxiv.org/pdf/2002.05709.pdf + """ + + def __init__(self, encoder, embed_dim=256, T=0.07, mlp=False, n_views=2): + """ + T: softmax temperature (default: 0.07) + n_views: number of views for each sample + """ + super(SimCLR, self).__init__() + + self.T = T + self.n_views = n_views + self.encoder = encoder + + if mlp: + self.encoder.add_module( + "mlp", + nn.Sequential(nn.Linear(embed_dim, embed_dim), nn.ReLU(), + nn.Linear(embed_dim, embed_dim))) + else: + self.encoder.add_module("mlp", nn.Sequential()) + + def prepare_for_info_nce_loss(self, features): + ''' + Input: + features: (self.n_views * bs, embed_dim) + Return: + logits: (self.n_views * bs, self.n_views * bs - 1) + labels (torch.long): (self.n_views * bs) + + ''' + + bs = features.shape[0] // self.n_views + # labels: (self.n_views * bs) + labels = torch.cat([torch.arange(bs) for _ in range(self.n_views)], + dim=0) + # labels: (self.n_views * bs, self.n_views * bs) + labels = (labels.unsqueeze(0) == labels.unsqueeze(1)).float() + labels = labels.to(features.device) + + features = F.normalize(features, dim=1) + + # similarity_matrix: (self.n_views * bs, self.n_views * bs) + similarity_matrix = torch.matmul(features, features.T) + + # discard the main diagonal from both: labels and similarities matrix + mask = torch.eye(labels.shape[0], dtype=torch.bool).to(features.device) + labels = labels[~mask].view(labels.shape[0], -1) + similarity_matrix = similarity_matrix[~mask].view( + similarity_matrix.shape[0], -1) + + # select and combine multiple positives + positives = similarity_matrix[labels.bool()].view(labels.shape[0], -1) + + # select only the negatives the negatives + negatives = similarity_matrix[~labels.bool()].view( + similarity_matrix.shape[0], -1) + + logits = torch.cat([positives, negatives], dim=1) + labels = torch.zeros(logits.shape[0], + dtype=torch.long).to(features.device) + + logits = logits / self.T + return logits, labels + + def forward(self, input_q, input_k): + """ + Input: + input_q: a batch of query inputs + input_k: a batch of key inputs + Output: + logits, targets + """ + + combine_input = torch.cat((input_q, input_k), dim=0) + features = self.encoder(combine_input) + features = self.encoder.mlp(features) + + logits, labels = self.prepare_for_info_nce_loss(features) + + return logits, labels diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/utils/contrastive_executor.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/utils/contrastive_executor.py new file mode 100644 index 0000000000000000000000000000000000000000..2f9fcd2610f72079a05332afcaf42824d7a3e765 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/utils/contrastive_executor.py @@ -0,0 +1,80 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import tableprint as tp + +import torch +import torchnet as tnt + + +def run_epoch(dataloader, + epoch_iter, + model, + criterion, + optimizer, + scheduler, + epoch, + logger, + scaler, + enable_amp, + log_batch_interval=100, + device=torch.device('cuda')): + model.train() + # By default use average pooling + loss_meter = tnt.meter.AverageValueMeter() + acc_meter = tnt.meter.ClassErrorMeter(accuracy=True) + + for i, batch in enumerate(dataloader): + + cur_iter = (epoch - 1) * epoch_iter + i + scheduler.step(cur_iter) + + # queries: (B, T, F) + queries = batch['queries'].squeeze(1).float().to(device) + # keys: (B, T, F) + keys = batch['keys'].squeeze(1).float().to(device) + + with torch.cuda.amp.autocast(enabled=enable_amp): + logits, labels = model(queries, keys) + loss = criterion(logits, labels) + + # loss, acc + loss_meter.add(loss.item()) + acc_meter.add(logits.cpu().detach().numpy(), labels.cpu().numpy()) + + # updata the model + optimizer.zero_grad() + # scaler does nothing here if enable_amp=False + scaler.scale(loss).backward() + scaler.step(optimizer) + scaler.update() + + # log + if (i + 1) % log_batch_interval == 0: + logger.info( + tp.row((epoch, i + 1, scheduler.get_lr()) + + (loss_meter.value()[0], acc_meter.value()[0]), + width=10, + style='grid')) + + if (i + 1) == epoch_iter: + break + + logger.info( + tp.row((epoch, i + 1, scheduler.get_lr()) + + (loss_meter.value()[0], acc_meter.value()[0]), + width=10, + style='grid')) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/utils/dino_executor.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/utils/dino_executor.py new file mode 100644 index 0000000000000000000000000000000000000000..b7f64a11f8e585cd4349f57d31ff35b3bc88a6b9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/utils/dino_executor.py @@ -0,0 +1,109 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import tableprint as tp + +import torch +import torchnet as tnt +from wespeaker.ssl.utils.dino_utils import ( + cancel_gradients_last_layer, + clip_gradients, +) + + +def run_epoch(dataloader, + epoch_iter, + model, + optimizer, + lr_schedule, + wd_schedule, + mt_schedule, + epoch, + logger, + scaler, + enable_amp, + clip_grad=3.0, + freeze_last_layer=1, + log_batch_interval=100, + device=torch.device('cuda')): + model.train() + + # By default use average pooling + loss_meter = tnt.meter.AverageValueMeter() + + for i, batch in enumerate(dataloader): + + cur_iter = (epoch - 1) * epoch_iter + i + # --------------- Update dynamic hyper-parameter --------------- + for k, param_group in enumerate(optimizer.param_groups): + param_group['lr'] = lr_schedule[cur_iter] + if k == 0: + param_group['weight_decay'] = wd_schedule[cur_iter] + # --------------- Update dynamic hyper-parameter --------------- + + # (B, chunk_num, T, F) + local_feats = batch['local_chunks'].float().to(device) + # (B, chunk_num', T, F) + global_feats = batch['global_chunks'].float().to(device) + + # (B, chunk_num, T, F) --> (chunk_num, B, T, F) + # --> (chunk_num * B, T, F) + local_T, local_F = local_feats.shape[-2:] + local_feats = local_feats.transpose(0, 1).contiguous().view( + -1, local_T, local_F) + global_T, global_F = global_feats.shape[-2:] + global_feats = global_feats.transpose(0, 1).contiguous().view( + -1, global_T, global_F) + + with torch.cuda.amp.autocast(enabled=enable_amp): + loss = model(local_feats, global_feats, epoch - 1) + + # loss, acc + loss_meter.add(loss.item()) + + # update the model + optimizer.zero_grad() + # scaler does nothing here if enable_amp=False + scaler.scale(loss).backward() + + # Unscales the gradients of optimizer's assigned params in-place + scaler.unscale_(optimizer) + clip_gradients(model, clip_grad) + cancel_gradients_last_layer(epoch - 1, model.module.s_model, + freeze_last_layer) + + scaler.step(optimizer) + scaler.update() + + # EMA update for teacher + m = mt_schedule[cur_iter] # momentum parameter + model.module.ema_update(m) + + # log + if (i + 1) % log_batch_interval == 0: + logger.info( + tp.row((epoch, i + 1, lr_schedule[cur_iter], + loss_meter.value()[0]), + width=10, + style='grid')) + + if (i + 1) == epoch_iter: + break + + logger.info( + tp.row((epoch, i + 1, lr_schedule[cur_iter], loss_meter.value()[0]), + width=10, + style='grid')) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/utils/dino_utils.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/utils/dino_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c306af06eaa8a52a1e34597d828b0df7cdda2518 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/ssl/utils/dino_utils.py @@ -0,0 +1,196 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# 2023 Zhengyang Chen (chenzhengyang117@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Misc functions. + +Mostly copy-paste from the public repo dino: +https://github.com/facebookresearch/dino/blob/main/utils.py +""" +import os +import numpy as np +import torch + + +def clip_gradients(model, clip): + norms = [] + for _, p in model.named_parameters(): + if p.grad is not None: + param_norm = p.grad.data.norm(2) + norms.append(param_norm.item()) + clip_coef = clip / (param_norm + 1e-6) + if clip_coef < 1: + p.grad.data.mul_(clip_coef) + return norms + + +def cancel_gradients_last_layer(epoch, model, freeze_last_layer): + if epoch >= freeze_last_layer: + return + for n, p in model.named_parameters(): + if "last_layer" in n: + p.grad = None + + +def save_checkpoint(model: torch.nn.Module, optimizer: torch.optim, path: str): + + def get_state_dict(model): + if isinstance(model, torch.nn.DataParallel): + state_dict = model.module.state_dict() + elif isinstance(model, torch.nn.parallel.DistributedDataParallel): + state_dict = model.module.state_dict() + else: + state_dict = model.state_dict() + return state_dict + + state_dict_gather = { + 'model': get_state_dict(model), + 'optimizer': get_state_dict(optimizer), + } + + torch.save(state_dict_gather, path) + + +def restart_from_checkpoint(ckp_path, run_variables=None, **kwargs): + """ + Re-start from checkpoint + """ + if not os.path.isfile(ckp_path): + return + print("Found checkpoint at {}".format(ckp_path)) + + # open checkpoint file + checkpoint = torch.load(ckp_path, map_location="cpu") + + # key is what to look for in the checkpoint file + # value is the object to load + # example: {'state_dict': model} + for key, value in kwargs.items(): + if key in checkpoint and value is not None: + try: + msg = value.load_state_dict(checkpoint[key], strict=False) + print("=> loaded '{}' from checkpoint '{}' with msg {}".format( + key, ckp_path, msg)) + except TypeError: + try: + msg = value.load_state_dict(checkpoint[key]) + print("=> loaded '{}' from checkpoint: '{}'".format( + key, ckp_path)) + except ValueError: + print( + "=> failed to load '{}' from checkpoint: '{}'".format( + key, ckp_path)) + else: + print("=> key '{}' not found in checkpoint: '{}'".format( + key, ckp_path)) + + # re load variable important for the run + if run_variables is not None: + for var_name in run_variables: + if var_name in checkpoint: + run_variables[var_name] = checkpoint[var_name] + + +def cosine_scheduler(base_value, + final_value, + epochs, + niter_per_ep, + warmup_epochs=0, + start_warmup_value=0): + warmup_schedule = np.array([]) + warmup_iters = warmup_epochs * niter_per_ep + if warmup_epochs > 0: + warmup_schedule = np.linspace(start_warmup_value, base_value, + warmup_iters) + + iters = np.arange(epochs * niter_per_ep - warmup_iters) + schedule = final_value + 0.5 * (base_value - final_value) * ( + 1 + np.cos(np.pi * iters / len(iters))) + + schedule = np.concatenate((warmup_schedule, schedule)) + assert len(schedule) == epochs * niter_per_ep + return schedule + + +class LARS(torch.optim.Optimizer): + """ + Almost copy-paste from + https://github.com/facebookresearch/barlowtwins/blob/main/main.py + """ + + def __init__(self, + params, + lr=0, + weight_decay=0, + momentum=0.9, + eta=0.001, + weight_decay_filter=None, + lars_adaptation_filter=None): + defaults = dict(lr=lr, + weight_decay=weight_decay, + momentum=momentum, + eta=eta, + weight_decay_filter=weight_decay_filter, + lars_adaptation_filter=lars_adaptation_filter) + super().__init__(params, defaults) + + @torch.no_grad() + def step(self): + for g in self.param_groups: + for p in g['params']: + dp = p.grad + + if dp is None: + continue + + if p.ndim != 1: + dp = dp.add(p, alpha=g['weight_decay']) + + if p.ndim != 1: + param_norm = torch.norm(p) + update_norm = torch.norm(dp) + one = torch.ones_like(param_norm) + q = torch.where( + param_norm > 0., + torch.where(update_norm > 0, + (g['eta'] * param_norm / update_norm), + one), one) + dp = dp.mul(q) + + param_state = self.state[p] + if 'mu' not in param_state: + param_state['mu'] = torch.zeros_like(p) + mu = param_state['mu'] + mu.mul_(g['momentum']).add_(dp) + + p.add_(mu, alpha=-g['lr']) + + +def get_params_groups(model): + regularized = [] + not_regularized = [] + for name, param in model.named_parameters(): + if not param.requires_grad: + continue + # we do not regularize biases nor Norm parameters + if name.endswith(".bias") or len(param.shape) == 1: + not_regularized.append(param) + else: + regularized.append(param) + return [{ + 'params': regularized + }, { + 'params': not_regularized, + 'weight_decay': 0. + }] diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/__init__.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/checkpoint.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/checkpoint.py new file mode 100644 index 0000000000000000000000000000000000000000..caa3fdcd45fc406364f028ca7cfd0783a8c10bde --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/checkpoint.py @@ -0,0 +1,95 @@ +# Copyright (c) 2020 Mobvoi Inc. (authors: Binbin Zhang) +# 2021 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import logging + + +def load_checkpoint(model: torch.nn.Module, path: str): + """ + Load a checkpoint and handle potential size mismatch in + the projection layer. + """ + checkpoint = torch.load(path, map_location="cpu", weights_only=False) + + if isinstance(checkpoint, dict) and "state_dict" in checkpoint: + checkpoint = checkpoint["state_dict"] + + current_state_dict = model.state_dict() + + proj_key = "projection.weight" + if proj_key in checkpoint and proj_key in current_state_dict: + ckpt_w = checkpoint[proj_key] + curr_w = current_state_dict[proj_key] + + # Check if shapes mismatch + if ckpt_w.shape != curr_w.shape: + logging.warning( + f"Size mismatch for {proj_key}: " + f"checkpoint has shape {ckpt_w.shape}, " + f"current model has shape {curr_w.shape}." + ) + + ckpt_len = ckpt_w.shape[0] + curr_len = curr_w.shape[0] + + # Case: checkpoint from speed-perturbed training + # (num_classes * 3) to LMFT training (original num_classes) + if ckpt_len > curr_len: + logging.info( + "Loading the first %d rows from checkpoint's " + "projection layer.", + curr_len, + ) + # Only use the first part of weights from checkpoint + checkpoint[proj_key] = ckpt_w[:curr_len, :] + + # Also handle bias if present + bias_key = "projection.bias" + if bias_key in checkpoint and bias_key in current_state_dict: + ckpt_b = checkpoint[bias_key] + if ckpt_b.shape[0] > curr_len: + checkpoint[bias_key] = ckpt_b[:curr_len] + + # Load with strict=False to tolerate missing / extra keys + missing_keys, unexpected_keys = model.load_state_dict( + checkpoint, + strict=False, + ) + + # Filter out projection keys we already handled explicitly so logs + # focus on truly unexpected tensors. + final_unexpected_keys = [ + k for k in unexpected_keys if "projection" not in k + ] + + for key in missing_keys: + # Missing projection keys are expected if the source model did + # not have projection; do not warn for those. + if "projection" not in key: + logging.warning("missing tensor: %s", key) + + for key in final_unexpected_keys: + logging.warning("unexpected tensor: %s", key) + + +def save_checkpoint(model: torch.nn.Module, path: str): + if isinstance(model, torch.nn.DataParallel): + state_dict = model.module.state_dict() + elif isinstance(model, torch.nn.parallel.DistributedDataParallel): + state_dict = model.module.state_dict() + else: + state_dict = model.state_dict() + torch.save(state_dict, path) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/embedding_processing.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/embedding_processing.py new file mode 100644 index 0000000000000000000000000000000000000000..7595ffac227bdbd8896f82108d0b9802357b7571 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/embedding_processing.py @@ -0,0 +1,271 @@ +# Copyright (c) 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import kaldiio +import pickle +import scipy.linalg as spl +import numpy as np +from wespeaker.utils.plda.plda_utils import get_data_for_plda + + +def chain_string_to_dict(chain_string=None): + # This function converts an input string into a list and dictionary + # structure suitable for use by the embedding processing classes below. + # For example, + # "mean-subtract --scp mean1_xvector.scp | length-norm " | + # "| lda --scp lda_xvector.scp --utt2spk utt2spk --dim $lda_dim " + # "| length-norm" + # (The above three lines is supposed to be one long string but style + # rules prevents it from be written that way here.) + # becomes + # [ + # ['mean-subtract', {'scp': 'mean1_xvector.scp'}], + # ['length-norm', {}], + # ['lda', {'scp': 'lda_xvector.scp', + # 'utt2spk': 'utt2spk', + # 'dim': '100'}], + # ['length-norm', {}] + # ] + + if chain_string is not None: + links = chain_string.split('|') + else: + links = [] + + a = [] + for l in links: + + x = l.split('--') + method = x.pop(0) + method = method.lstrip(' ') + method = method.rstrip(' ') + + args_and_values = {} + for xx in x: + xx = re.sub("=", " ", xx) + xx = re.sub(" +", " ", xx) + xx = xx.lstrip(' ') + xx = xx.rstrip(' ') + xx = xx.split(' ') + assert len(xx) == 2 + args_and_values[xx[0]] = xx[1] + + a.append([method, args_and_values]) + + return a + + +class Lda: + + def compute_mean_and_lda_scatter_matrices(self, + scp_file, + utt2spk_file, + equal_speaker_weight=False, + current_chain=None): + # equal_speaker_weight: If True, each speaker is considered equally + # important in the calculation of the mean and scatter matrices. If + # False, speakers are weighted by their number of utterances. + if current_chain is None: + current_chain = [] + _, embeddings_dict = get_data_for_plda(scp_file, utt2spk_file) + speakers = embeddings_dict.keys() + speaker_counts = [] + speaker_means = [] + speaker_covs = [] + n_used = 0 + n_skipped = 0 + for s in speakers: + embd_s = current_chain(np.vstack(embeddings_dict[s])) + count_s = embd_s.shape[0] + # With bias=False we need at least 2 speakers, with bias=True we + # need at least 1. But this would result in covariance matrix = 0 + # for all its elements. (This is not necessarily wrong). + if count_s > 1: + mean_s = np.mean(embd_s, axis=0) + cov_s = np.cov(embd_s, rowvar=False, bias=True) + n_used += 1 + speaker_counts.append(count_s) + speaker_means.append(mean_s) + speaker_covs.append(cov_s) + + else: + n_skipped += 1 + + speaker_counts = np.array(speaker_counts) + speaker_means = np.vstack(speaker_means) + speaker_covs = np.array(speaker_covs) + print( + " #speakers: {}, #used {}, #skipped {} (only having one utterances)" + .format(len(speakers), n_used, n_skipped)) + + if equal_speaker_weight: + mean = np.mean(speaker_means, axis=0) + between_class_covariance = np.cov(speaker_means, + rowvar=False, + bias=True) + within_class_covariance = np.sum(speaker_covs, + axis=0) / len(speakers) + else: + mean = np.sum(speaker_counts[:, np.newaxis] * speaker_means, + axis=0) / np.sum(speaker_counts) + between_class_covariance = np.cov(speaker_means, + rowvar=False, + bias=True, + fweights=speaker_counts) + within_class_covariance = np.sum( + speaker_counts[:, np.newaxis, np.newaxis] * speaker_covs, + axis=0) / np.sum(speaker_counts) + + return mean, between_class_covariance, within_class_covariance + + def __init__(self, args, current_chain=None): + if current_chain is None: + current_chain = [] + + print(" LDA") + scp_file = args['scp'] + utt2spk_file = args['utt2spk'] + dim = int(args['dim']) + eps = float(args['eps']) if 'eps' in args else 1e-6 + + self.m, BC, WC = self.compute_mean_and_lda_scatter_matrices( + scp_file, utt2spk_file, current_chain=current_chain) + + E, M = spl.eigh(WC) + # Floor the within-class covariance eigenvalues. We noticed that this + # was done in Kaldi. + E_floor = np.max(E) * eps + E[E < E_floor] = E_floor + """ + # The new within-class covariance. + WC = M.dot(np.diag(E).dot(M.T)) + D, lda = spl.eigh( BC, WC ) # The output of eigh is sorted in + self.lda = lda[:,-dim:] # ascending order so we so we kee + self.T1 = np.eye(self.m.shape[0]) # the "dim" last eigenvectors. + """ + # Since we have already found the eigen decomposition of WC, we could + # whiten it by T1 = 1 / sqrt(E), I = T1 WC T1'. So instead of solving + # spl.eigh( BC, WC ) we can apply T1 on BC and solve + # spl.eigh( T1 BC T1', T1 WC T1' ) + # = spl.eigh( T1 BC T1', I ) + # = spl.eigh( T1 BC T1') + # as follows. However, T1 then needs to be inlcluded when transforming + # the data. In either case, the result is that after LDA transform, the + # data will have white WC and diagonal BC + T1 = np.dot(np.diag(1 / np.sqrt(E)), M.T) + BC = np.dot(np.dot(T1, BC), T1.T) + D, lda = spl.eigh(BC) + self.lda = np.dot(T1.T, lda[:, -dim:]) + + print(" Input dimension: {}, output dimension: {}," + " sum of all eigenvalues {:.2f}, sum of kept eigenvalues {:.2f}". + format(len(D), dim, np.sum(D), np.sum(D[-dim:]))) + print(" All eigenvalues: {}".format(D)) + + def __call__(self, embd): + return (embd - self.m).dot(self.lda) + + +class Length_norm: + + def __init__(self, args=None, current_chain=None): + pass + + def __call__(self, embd): + embd_proc = embd.copy() + embd_proc /= np.sqrt((embd_proc**2).sum( + axis=1)[:, np.newaxis]) # This would make the lengths equal to one + """ + Todo: For Kaldi compatibility we may want to add this as option as + well as Kaldi style normalization. + embd_proc *= np.sqrt(embd_normed.shape[1]) + """ + return (embd_proc) + + +class Whitening: + + def __init__(self, args, current_chain): + pass + + +class MeanSubtraction(): + + def __init__(self, args, current_chain=None): + if current_chain is None: + current_chain = [] + + e = [] + for key, vec in kaldiio.load_scp_sequential(args['scp']): + e.append(vec) + self.mean = np.mean(current_chain(np.vstack(e)), axis=0) + + def __call__(self, embd): + return embd - self.mean + + +class EmbeddingProcessingChain: + + # This is used to map the processing steps, coming from the input + # argument as strings, into the corresponding clases. + string2class = { + 'lda': Lda, + 'length-norm': Length_norm, + 'whitening': Whitening, + 'mean-subtract': MeanSubtraction + } + + def __init__(self, chain=None): + c = chain_string_to_dict(chain) + self.chain_of_classes = [] # This is not a great name... + for m, a in c: + print("Method: {}".format(m)) + print("Argument: {}".format(a)) + self.chain_of_classes.append(self.string2class[m](a, self)) + + def __call__(self, embd): + for c in self.chain_of_classes: + embd = c(embd) + return embd + + def save(self, path, data_format='pickle'): + print("Saving embedding processing chain to {}".format(path)) + with open(path, 'wb') as f: + pickle.dump(self.chain_of_classes, f) + + def load(self, path, data_format='pickle'): + print("Loading embedding processing chain from {}".format(path)) + with open(path, 'rb') as f: + self.chain_of_classes = pickle.load(f) + + def update_link(self, link_no_to_replace, new_link): + nl = chain_string_to_dict(new_link) + + # For now, it is only supported to update one link. This + # should be generalized in the future. + assert len(nl) == 1, "Length of new chain must be one." + + m, a = nl[0] + old_chain_of_classes = self.chain_of_classes + self.chain_of_classes = [] + + for i, ol in enumerate(old_chain_of_classes): + if (i != link_no_to_replace): + self.chain_of_classes.append(ol) + else: + print("Replacing link number {} ({}) with".format(i, ol)) + print("Method: {}".format(m)) + print("Argument: {}".format(a)) + self.chain_of_classes.append(self.string2class[m](a, self)) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/executor.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/executor.py new file mode 100644 index 0000000000000000000000000000000000000000..f32796cfb4002a728d7f89f674e3ee0d683ec7b7 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/executor.py @@ -0,0 +1,96 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# 2022 Chengdong Liang (liangchengdong@mail.nwpu.edu.cn) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import tableprint as tp + +import torch +import torchnet as tnt +from wespeaker.dataset.dataset_utils import apply_cmvn, spec_aug + + +def run_epoch(dataloader, epoch_iter, model, criterion, optimizer, scheduler, + margin_scheduler, epoch, logger, scaler, device, configs): + model.train() + # By default use average pooling + loss_meter = tnt.meter.AverageValueMeter() + acc_meter = tnt.meter.ClassErrorMeter(accuracy=True) + + frontend_type = configs['dataset_args'].get('frontend', 'fbank') + for i, batch in enumerate(dataloader): + cur_iter = (epoch - 1) * epoch_iter + i + scheduler.step(cur_iter) + margin_scheduler.step(cur_iter) + + utts = batch['key'] + targets = batch['label'] + targets = targets.long().to(device) # (B) + if frontend_type == 'fbank': + features = batch['feat'] # (B,T,F) + features = features.float().to(device) + else: # 's3prl', 'tfmel' + wavs = batch['wav'] # (B,1,W) + wavs = wavs.squeeze(1).float().to(device) # (B,W) + wavs_len = torch.LongTensor([wavs.shape[1]]).repeat( + wavs.shape[0]).to(device) # (B) + with torch.cuda.amp.autocast(enabled=configs['enable_amp']): + features, _ = model.module.frontend(wavs, wavs_len) + + with torch.cuda.amp.autocast(enabled=configs['enable_amp']): + # apply cmvn + if configs['dataset_args'].get('cmvn', True): + features = apply_cmvn( + features, **configs['dataset_args'].get('cmvn_args', {})) + # spec augmentation + if configs['dataset_args'].get('spec_aug', False): + features = spec_aug(features, + **configs['dataset_args']['spec_aug_args']) + + outputs = model(features) # (embed_a,embed_b) in most cases + embeds = outputs[-1] if isinstance(outputs, tuple) else outputs + outputs = model.module.projection(embeds, targets) + if isinstance(outputs, tuple): + outputs, loss = outputs + else: + loss = criterion(outputs, targets) + + # loss, acc + loss_meter.add(loss.item()) + acc_meter.add(outputs.cpu().detach().numpy(), targets.cpu().numpy()) + + # updata the model + optimizer.zero_grad() + # scaler does nothing here if enable_amp=False + scaler.scale(loss).backward() + scaler.step(optimizer) + scaler.update() + + # log + if (i + 1) % configs['log_batch_interval'] == 0: + logger.info( + tp.row((epoch, i + 1, scheduler.get_lr(), + margin_scheduler.get_margin()) + + (loss_meter.value()[0], acc_meter.value()[0]), + width=10, + style='grid')) + + if (i + 1) == epoch_iter: + break + + logger.info( + tp.row( + (epoch, i + 1, scheduler.get_lr(), margin_scheduler.get_margin()) + + (loss_meter.value()[0], acc_meter.value()[0]), + width=10, + style='grid')) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/executor_deprecated.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/executor_deprecated.py new file mode 100644 index 0000000000000000000000000000000000000000..417701604135c30c48b9f2c316bfe798afb265a5 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/executor_deprecated.py @@ -0,0 +1,83 @@ +# Copyright (c) 2021 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from contextlib import nullcontext +import tableprint as tp + +import torch +import torchnet as tnt + + +def run_epoch(dataloader, + model, + criterion, + optimizer, + scheduler, + margin_scheduler, + epoch, + logger, + log_batch_interval=100, + device=torch.device('cuda')): + model.train() + # By default use average pooling + loss_meter = tnt.meter.AverageValueMeter() + acc_meter = tnt.meter.ClassErrorMeter(accuracy=True) + + # https://github.com/wenet-e2e/wenet/blob/main/wenet/utils/executor.py#L40 + if isinstance(model, torch.nn.parallel.DistributedDataParallel): + model_context = model.join + else: + model_context = nullcontext + + with torch.set_grad_enabled(True), model_context(): + for i, (utts, features, targets) in enumerate(dataloader): + + cur_iter = (epoch - 1) * len(dataloader) + i + scheduler.step(cur_iter) + margin_scheduler.step(cur_iter) + + features = features.float().to(device) # (B,T,F) + targets = targets.long().to(device) + outputs = model(features) # (embed_a,embed_b) in most cases + embeds = outputs[-1] if isinstance(outputs, tuple) else outputs + outputs = model.module.projection(embeds, targets) + if isinstance(outputs, tuple): + outputs, loss = outputs + else: + loss = criterion(outputs, targets) + # loss, acc + loss_meter.add(loss.item()) + acc_meter.add(outputs.cpu().detach().numpy(), + targets.cpu().numpy()) + + # updata the model + optimizer.zero_grad() + loss.backward() + optimizer.step() + + # log + if (i + 1) % log_batch_interval == 0: + logger.info( + tp.row((epoch, i + 1, scheduler.get_lr(), + margin_scheduler.get_margin()) + + (loss_meter.value()[0], acc_meter.value()[0]), + width=10, + style='grid')) + + logger.info( + tp.row((epoch, len(dataloader), scheduler.get_lr(), + margin_scheduler.get_margin()) + + (loss_meter.value()[0], acc_meter.value()[0]), + width=10, + style='grid')) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/file_utils.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/file_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..76491326021bfbf4697c86f60dbbc5c8e8ec9ac9 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/file_utils.py @@ -0,0 +1,65 @@ +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def read_scp(scp_file): + """read scp file (also support PIPE format) + + Args: + scp_file (str): path to the scp file + + Returns: + list: key_value_list + """ + key_value_list = [] + with open(scp_file, "r", encoding='utf8') as fin: + for line in fin: + tokens = line.strip().split() + key = tokens[0] + value = " ".join(tokens[1:]) + key_value_list.append((key, value)) + return key_value_list + + +def read_lists(list_file): + """read list file with only 1 column + + Args: + list_file (str): path to the list file + + Returns: + list: lists + """ + lists = [] + with open(list_file, 'r', encoding='utf8') as fin: + for line in fin: + lists.append(line.strip()) + return lists + + +def read_table(table_file): + """read table file with any columns + + Args: + table_file (str): path to the table file + + Returns: + list: table_list + """ + table_list = [] + with open(table_file, 'r', encoding='utf8') as fin: + for line in fin: + tokens = line.strip().split() + table_list.append(tokens) + return table_list diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/plda/kaldi_utils.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/plda/kaldi_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..98bdcab077e0fa0e4eda218f2d6b0c8dddbf5cf4 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/plda/kaldi_utils.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python + +# Copyright 2019 Lukas Burget (burget@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import struct + +import numpy as np +from kaldi_io import open_or_fd, BadSampleSize, UnknownMatrixHeader +from kaldi_io.kaldi_io import _read_compressed_mat, _read_mat_ascii + + +def read_plda(file_or_fd): + """ Loads PLDA from a file in kaldi format (binary or text). + Input: + file_or_fd - file name or file handle with kaldi PLDA model. + Output: + Tuple (mu, tr, psi) define a PLDA model using the kaldi parametrization + mu : mean vector + tr : transform whitening within- and diagonalizing across-class + covariance matrix + psi - diagonal of the across-class covariance in the transformed space + """ + fd = open_or_fd(file_or_fd) + try: + binary = fd.read(2) + if binary == b'\x00B': + assert (fd.read(7) == b' ') + plda_mean = _read_vec_binary(fd) + plda_trans = _read_mat_binary(fd) + plda_psi = _read_vec_binary(fd) + else: + assert (binary + fd.read(5) == b' ') + plda_mean = np.array(fd.readline().strip(' \n[]').split(), + dtype=float) + assert (fd.read(2) == b' [') + plda_trans = _read_mat_ascii(fd) + plda_psi = np.array(fd.readline().strip(' \n[]').split(), + dtype=float) + assert (fd.read(8) == b' ') + finally: + if fd is not file_or_fd: + fd.close() + return plda_mean, plda_trans, plda_psi + + +def _read_vec_binary(fd): + # Data type, + type = fd.read(3) + if type == b'FV ': + sample_size = 4 # floats + elif type == b'DV ': + sample_size = 8 # doubles + else: + raise BadSampleSize + assert (sample_size > 0) + # Dimension, + assert fd.read(1) == b'\4' # int-size + vec_size = struct.unpack(' 0) + # Dimensions + s1, rows, s2, cols = \ + np.frombuffer(fd.read(10), dtype='int8,int32,int8,int32', count=1)[0] + # Read whole matrix + buf = fd.read(rows * cols * sample_size) + if sample_size == 4: + vec = np.frombuffer(buf, dtype='float32') + elif sample_size == 8: + vec = np.frombuffer(buf, dtype='float64') + else: + raise BadSampleSize + mat = np.reshape(vec, (rows, cols)) + return mat + + +def _read_sparse_mat(fd, format): + """ Read a sparse matrix, + """ + from scipy.sparse import csr_matrix + assert (format == 'SM ') + + # Mapping for matrix elements, + def read_sparse_vector(fd): + _format = fd.read(3).decode() + assert (_format == 'SV ') + _, dim = np.frombuffer(fd.read(5), dtype='int8,int32', count=1)[0] + _, num_elems = np.frombuffer(fd.read(5), dtype='int8,int32', + count=1)[0] + col = [] + data = [] + for j in range(num_elems): + size = np.frombuffer(fd.read(1), dtype='int8', count=1)[0] + dtype = 'int32' if size == 4 else 'int64' + c = np.frombuffer(fd.read(size), dtype=dtype, count=1)[0] + size = np.frombuffer(fd.read(1), dtype='int8', count=1)[0] + dtype = 'float32' if size == 4 else 'float64' + d = np.frombuffer(fd.read(size), dtype=dtype, count=1)[0] + col.append(c) + data.append(d) + return col, data, dim + + _, num_rows = np.frombuffer(fd.read(5), dtype='int8,int32', count=1)[0] + + rows = [] + cols = [] + all_data = [] + max_dim = 0 + for i in range(num_rows): + col, data, dim = read_sparse_vector(fd) + rows += [i] * len(col) + cols += col + all_data += data + max_dim = max(dim, max_dim) + sparse_mat = csr_matrix((all_data, (rows, cols)), + shape=(num_rows, max_dim)) + return sparse_mat diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/plda/plda_utils.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/plda/plda_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..dab0923b2a77a2a2b0f72fd2fd75acf5fd15fe58 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/plda/plda_utils.py @@ -0,0 +1,100 @@ +# Copyright (c) 2023 Shuai Wang (wsstriving@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math + +import kaldiio +import numpy as np + + +def read_vec_scp_file(scp_file): + """ + Read the pre-extracted kaldi-format speaker embeddings. + :param scp_file: path to xvector.scp + :return: dict {wav_name: embedding} + """ + samples_dict = {} + for key, vec in kaldiio.load_scp_sequential(scp_file): + samples_dict[key] = vec + return samples_dict + + +def read_label_file(label_file): + """ + Read the utt2spk file + :param label_file: the path to utt2spk + :return: dict {wav_name: spk_id} + """ + labels_dict = {} + with open(label_file, 'r') as fin: + for line in fin: + tokens = line.strip().split() + labels_dict[tokens[0]] = tokens[1] + return labels_dict + + +def norm_embeddings(embeddings, kaldi_style=True): + """ + Norm embeddings to unit length + :param embeddings: input embeddings + :param kaldi_style: if true, the norm should be embedding dimension + :return: + """ + scale = math.sqrt(embeddings.shape[-1]) if kaldi_style else 1. + if len(embeddings.shape) == 2: + return (scale * embeddings.transpose() / + np.linalg.norm(embeddings, axis=1)).transpose() + elif len(embeddings.shape) == 1: + return scale * embeddings / np.linalg.norm(embeddings) + + +def get_data_for_plda(scp_file, utt2spk_file): + samples_dict = read_vec_scp_file(scp_file) + labels_dict = read_label_file(utt2spk_file) + samples = [] + model_dict = {} + for key, vec in samples_dict.items(): + samples.append(vec) + if key in labels_dict: + label = labels_dict[key] + if label in model_dict.keys(): + model_dict[label].append(vec) + else: + model_dict[label] = [vec] + else: + print("WARNING: {} not in utt2spk ({}), skipping it.".format( + key, utt2spk_file)) + + return np.vstack(samples), model_dict + + +def compute_normalizing_transform(covar): + try: + c = np.linalg.cholesky(covar) + except np.linalg.LinAlgError: + c = np.linalg.cholesky(covar + np.eye(covar.shape[0]) * 1e-6) + c = np.linalg.inv(c) + return c + + +def sort_svd(s, d): + """ + :param s: + :param d: + :return: + """ + idx = np.argsort(-s) + s1 = s[idx] + d1 = d.T + d1 = d1[idx].T + return s1, d1 diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/plda/two_cov_plda.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/plda/two_cov_plda.py new file mode 100644 index 0000000000000000000000000000000000000000..ded9ef2ba2bf00a15701fcb88a8d00d1390a7b9d --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/plda/two_cov_plda.py @@ -0,0 +1,363 @@ +# Copyright (c) 2022 Shuai Wang (wsstriving@gmail.com) +# 2023 Shuai Wang, Houjun Huang +# 2024 Johan Rohdin (rohdin@fit.vutbr.cz) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import math +import h5py +import numpy as np +import scipy.linalg as spl +from numpy.linalg import inv +from tqdm import tqdm +from wespeaker.utils.plda.kaldi_utils import read_plda + +from wespeaker.utils.plda.plda_utils import compute_normalizing_transform +from wespeaker.utils.plda.plda_utils import get_data_for_plda +from wespeaker.utils.plda.plda_utils import norm_embeddings +from wespeaker.utils.plda.plda_utils import read_vec_scp_file +from wespeaker.utils.plda.plda_utils import sort_svd + +M_LOG_2PI = 1.8378770664093454835606594728112 + +ClassInfo = collections.namedtuple('ClassInfo', + ['weight', 'num_example', 'mu']) + + +class PldaStats(object): + + def __init__(self, dim): + self.dim = dim + self.num_example, self.num_classes = 0, 0 + self.class_weight, self.example_weight = 0, 0, + self.sum_, self.offset_scatter = np.zeros(dim), np.zeros((dim, dim)) + self.classinfo = [] + + def add_samples(self, weight, spk_embeddings): + """ + Add samples of a certain speaker to the PLDA stats + :param weight: class_weight, default set to 1. + :param spk_embeddings: All embedding samples from a certain speaker + :return: + """ + n = spk_embeddings.shape[0] + mean = np.mean(spk_embeddings, axis=0) + tmp = spk_embeddings - mean + self.offset_scatter += weight * np.matmul(tmp.T, tmp) + self.classinfo.append(ClassInfo(weight, n, mean)) + self.num_example += n + self.num_classes += 1 + self.class_weight += weight + self.example_weight += weight * n + self.sum_ += weight * mean + + +class TwoCovPLDA: + + def __init__(self, + scp_file=None, + utt2spk_file=None, + embed_dim=256, + subtract_train_set_mean=False, + normalize_length=False): + self.subtract_train_set_mean = subtract_train_set_mean + self.normalize_length = normalize_length + self.dim = embed_dim + self.mu = np.zeros(self.dim) + # The transform which whitens the within- and + # diagonalizes the across-class covariance matrix + self.transform = np.zeros((self.dim, self.dim)) + # The diagonal of the across-class covariance in the transformed space + self.psi = np.zeros(self.dim) + self.offset = np.zeros(self.dim) + self.stats = PldaStats(self.dim) + self.B = np.eye(self.dim) + self.B_stats = np.zeros((self.dim, self.dim)) + self.B_count = 0 + self.W = np.eye(self.dim) + self.W_stats = np.zeros((self.dim, self.dim)) + self.W_count = 0 + if scp_file is not None: + samples, self.embeddings_dict = get_data_for_plda( + scp_file, utt2spk_file) + if subtract_train_set_mean: + train_mean_vec = samples.mean(0) + else: + train_mean_vec = np.zeros(embed_dim) + for key, mat in self.embeddings_dict.items(): + mat = np.vstack(mat) + mat = mat - train_mean_vec + if self.normalize_length: + mat = norm_embeddings(mat) + self.stats.add_samples(1.0, mat) + self.mu = self.stats.sum_ / self.stats.class_weight + + def train(self, num_em_iters): + for i in range(num_em_iters): + print("Plda estimation %d of %d" % (i, num_em_iters)) + self.em_one_iter() + self.get_output() + + def em_one_iter(self): + self.B_stats, self.B_count = np.zeros( + (self.stats.dim, self.stats.dim)), 0 + self.W_stats, self.W_count = np.zeros( + (self.stats.dim, self.stats.dim)), 0 + self.W_stats += self.stats.offset_scatter + self.W_count += self.stats.example_weight - self.stats.class_weight + B_inv = inv(self.B) + W_inv = inv(self.W) + for i in range(self.stats.num_classes): + info = self.stats.classinfo[i] + m = info.mu - self.stats.sum_ / self.stats.class_weight + weight = info.weight + n = info.num_example + mix_var = inv(B_inv + n * W_inv) + w = np.matmul(mix_var, n * np.matmul(W_inv, m)) + m_w = m - w + self.B_stats += weight * (mix_var + np.outer(w, w)) + self.B_count += weight + self.W_stats += weight * n * (mix_var + np.outer(m_w, m_w)) + self.W_count += weight + + self.W = self.W_stats / self.W_count + self.B = self.B_stats / self.B_count + self.W = 0.5 * (self.W + self.W.T) + self.B = 0.5 * (self.B + self.B.T) + + print("W_count:", self.W_count, "Trace of W:", np.trace(self.W)) + print("B_count:", self.B_count, "Trace of B:", np.trace(self.B)) + + def get_output(self): + self.mu = self.stats.sum_ / self.stats.class_weight + transform1 = compute_normalizing_transform(self.W) + B_proj = np.matmul(transform1, self.B) + B_proj = np.matmul(B_proj, transform1.T) + s, U = np.linalg.eigh(B_proj) + s = np.where(s > 0.0, s, 0.0) + s, U = sort_svd(s, U) + + self.transform = np.matmul(U.T, transform1) + self.psi = s + self.offset = np.zeros(self.dim) + self.offset = -1.0 * np.matmul(self.transform, self.mu) + + def transform_embedding(self, embedding): + transformed_embedding = np.matmul(self.transform, embedding) + transformed_embedding += self.offset + normalization_factor = math.sqrt( + self.dim) / np.linalg.norm(transformed_embedding) + if self.normalize_length: + transformed_embedding = normalization_factor * transformed_embedding + return transformed_embedding + + def log_likelihood_ratio(self, transformed_train_embedding, + transformed_test_embedding, n): + mean = n * self.psi / (n * self.psi + + 1.0) * transformed_train_embedding + variance = 1.0 + self.psi / (n * self.psi + 1.0) + logdet = np.sum(np.log(variance)) + sqdiff = transformed_test_embedding - mean + sqdiff = np.power(sqdiff, 2.0) + variance = 1.0 / variance + loglike_given_class = -0.5 * (logdet + M_LOG_2PI * self.dim + + np.dot(sqdiff, variance)) + sqdiff = transformed_test_embedding + sqdiff = np.power(sqdiff, 2.0) + variance = self.psi + 1.0 + logdet = np.sum(np.log(variance)) + variance = 1.0 / variance + loglike_without_class = -0.5 * (logdet + M_LOG_2PI * self.dim + + np.dot(sqdiff, variance)) + loglike_ratio = loglike_given_class - loglike_without_class + return loglike_ratio + + def eval_sv(self, + enroll_scp, + enroll_utt2spk, + test_scp, + trials, + score_file, + multisession_avg=True, + indomain_scp=None): + """ + Caculate the plda score + :param enroll_scp: + :param enroll_utt2spk: + :param test_scp: + :param trials: + :param score_file: + :param indomain_scp: + :return: + """ + _, enroll_embeddings_dict = get_data_for_plda(enroll_scp, + enroll_utt2spk) + test_embeddings_dict = read_vec_scp_file(test_scp) + + if indomain_scp is not None: + indomain_embeddings_dict = read_vec_scp_file(indomain_scp) + mean_vec = np.vstack(list( + indomain_embeddings_dict.values())).mean(0) + else: + mean_vec = np.zeros(self.dim) + + enrollspks = {} + testspks = {} + enrollcounts = {} + for key, value in enroll_embeddings_dict.items(): + if multisession_avg: + enrollcounts[key] = 1 + else: + enrollcounts[key] = len(value) + value = np.vstack(value) + value = value - mean_vec # Shuai + + # Normalize length + # It is questionable whether this should be applied + # after speaker mean in case of multisession scoring. + if self.normalize_length: + tmp = norm_embeddings(np.mean(value, 0)) + + else: + tmp = np.mean(value, 0) + tmp = self.transform_embedding(tmp) + enrollspks[key] = tmp + + for key, value in test_embeddings_dict.items(): + value = value - mean_vec # Shuai + if self.normalize_length: + tmp = norm_embeddings(value) + else: + tmp = value + tmp = self.transform_embedding(tmp) + testspks[key] = tmp + + with open(score_file, 'w') as write_score: + with open(trials, 'r') as read_trials: + for line in tqdm(read_trials): + tokens = line.strip().split() + score = self.log_likelihood_ratio(enrollspks[tokens[0]], + testspks[tokens[1]], + enrollcounts[tokens[0]]) + segs = line.strip().split() + output_line = ('{} {} {:.5f} {}\n'.format( + segs[0], segs[1], score, segs[2])) + write_score.write(output_line) + + def adapt(self, adapt_scp, ac_scale=0.5, wc_scale=0.5): + # Implemented by the BUT speech group + # plda = load_model(model_path, from_kaldi=from_kaldi) + adp_data = np.array(list(read_vec_scp_file(adapt_scp).values())) + mean_vec = adp_data.mean(0) + adp_data = adp_data - mean_vec + if self.normalize_length: + adp_data = norm_embeddings(adp_data) + + plda_mean, plda_trans, plda_psi = self.mu, self.transform, self.psi + W = inv(plda_trans.T.dot(plda_trans)) + W = (W + W.T) / 2 + B = np.linalg.inv((plda_trans.T / plda_psi).dot(plda_trans)) + B = (B + B.T) / 2 + T = B + W + # adp_data = np.vstack(self.xvect) + # Covariance of the adaptation data. + data_cov = np.cov(adp_data.T) + [v, e] = spl.eigh(data_cov, (T + T.T) / 2) + iet = np.linalg.inv(e.T) + excess = iet[:, v > 1].dot(np.diag(np.sqrt(v[v > 1] - 1))) + V_adp = excess * np.sqrt(ac_scale) + B_adp = B + V_adp.dot(V_adp.T) + U_adp = excess * np.sqrt(wc_scale) + W_adp = W + U_adp.dot(U_adp.T) + mu_adp = np.mean(adp_data, axis=0) + mu, A, B = mu_adp, (B_adp + B_adp.T) / 2.0, (W_adp + W_adp.T) / 2.0 + eps = 1e-9 + [D, V] = np.linalg.eigh(B) + D = np.diag(1.0 / np.sqrt(D + eps)) + # First transform + T1 = np.dot(D, V.T) + # This should equal the identity matrix + B1 = np.dot(np.dot(T1, B), T1.T) + A1 = np.dot(np.dot(T1, A), T1.T) + # Second transform is given by T2.T * (.) * T2 + [D, T2] = np.linalg.eigh(A1) + # Joint transform + T = np.dot(T2.T, T1) + # Transform the matrices + A2 = np.dot(np.dot(T, A), T.T) + B2 = np.dot(np.dot(T, B), T.T) + plda_trans, plda_psi, X = T, np.diag(A2), B2 + + adapt_plda = TwoCovPLDA() + adapt_plda.mu = mu + adapt_plda.transform = plda_trans + adapt_plda.psi = plda_psi + adapt_plda.offset = -1.0 * np.matmul(adapt_plda.transform, + adapt_plda.mu) + + return adapt_plda + + def save_model(self, output_file_name): + print("saving the trained plda to {}".format(output_file_name)) + with h5py.File(output_file_name, "w") as f: + f.create_dataset("mu", + data=self.mu, + maxshape=(None), + compression="gzip", + fletcher32=True) + f.create_dataset("transform", + data=self.transform, + maxshape=(None, None), + compression="gzip", + fletcher32=True) + f.create_dataset("psi", + data=self.psi, + maxshape=(None), + compression="gzip", + fletcher32=True) + f.create_dataset("offset", + data=self.offset, + maxshape=(None), + compression="gzip", + fletcher32=True) + f.create_dataset("normalize_length", + data=int(self.normalize_length), + maxshape=(None)) + f.create_dataset("subtract_train_set_mean", + data=int(self.subtract_train_set_mean), + maxshape=(None)) + + @staticmethod + def load_model(model_name, from_kaldi=False): + plda = TwoCovPLDA() + if from_kaldi: + plda.mu, plda.transform, plda.psi = read_plda(model_name) + plda.offset = np.zeros(plda.mu.shape[0]) + plda.offset = -1.0 * np.matmul(plda.transform, plda.mu) + else: + with h5py.File(model_name, "r") as f: + plda.mu = f.get("mu")[()] + plda.transform = f.get("transform")[()] + plda.psi = f.get("psi")[()] + plda.offset = f.get("offset")[()] + plda.normalize_length = bool(f.get("normalize_length")[()]) + plda.subtract_train_set_mean = bool( + f.get("subtract_train_set_mean")[()]) + print("PLDA normalize length is {}.".format( + plda.normalize_length)) + print("PLDA subtract_train_set_mean is {}.".format( + plda.subtract_train_set_mean)) + + plda.dim = plda.mu.shape[0] + return plda diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/schedulers.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/schedulers.py new file mode 100644 index 0000000000000000000000000000000000000000..393579caea8059abcb00e83a2a801285bca9ea23 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/schedulers.py @@ -0,0 +1,421 @@ +# Copyright (c) 2021 Shuai Wang (wsstriving@gmail.com) +# 2021 Zhengyang Chen (chenzhengyang117@gmail.com) +# 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + + +class MarginScheduler: + + def __init__(self, + model, + epoch_iter, + increase_start_epoch, + fix_start_epoch, + initial_margin, + final_margin, + update_margin, + increase_type='exp'): + ''' + The margin is fixed as initial_margin before increase_start_epoch, + between increase_start_epoch and fix_start_epoch, the margin is + exponentially increasing from initial_margin to final_margin + after fix_start_epoch, the margin is fixed as final_margin. + ''' + self.model = model + self.increase_start_iter = (increase_start_epoch - 1) * epoch_iter + self.fix_start_iter = (fix_start_epoch - 1) * epoch_iter + self.initial_margin = initial_margin + self.final_margin = final_margin + self.increase_type = increase_type + + self.fix_already = False + self.current_iter = 0 + self.update_margin = update_margin and hasattr(self.model.projection, + 'update') + self.increase_iter = self.fix_start_iter - self.increase_start_iter + + self.init_margin() + + def init_margin(self): + if hasattr(self.model.projection, 'update'): + self.model.projection.update(margin=self.initial_margin) + + def get_increase_margin(self): + initial_val = 1.0 + final_val = 1e-3 + + current_iter = self.current_iter - self.increase_start_iter + + if self.increase_type == 'exp': # exponentially increase the margin + ratio = 1.0 - math.exp( + (current_iter / self.increase_iter) * + math.log(final_val / (initial_val + 1e-6))) * initial_val + else: # linearly increase the margin + ratio = 1.0 * current_iter / self.increase_iter + return self.initial_margin + (self.final_margin - + self.initial_margin) * ratio + + def step(self, current_iter=None): + if not self.update_margin or self.fix_already: + return + + if current_iter is not None: + self.current_iter = current_iter + + if self.current_iter >= self.fix_start_iter: + self.fix_already = True + if hasattr(self.model.projection, 'update'): + self.model.projection.update(margin=self.final_margin) + elif self.current_iter >= self.increase_start_iter: + if hasattr(self.model.projection, 'update'): + self.model.projection.update(margin=self.get_increase_margin()) + + self.current_iter += 1 + + def get_margin(self): + try: + margin = self.model.projection.margin + except Exception: + margin = 0.0 + + return margin + +class WarmupLR_withStepDecay: + """ + A Wespeaker-compatible class version of WarmupLR_withStepDecay from + the W2V codebase. It does not inherit from a base scheduler class + and follows a pattern similar to MarginScheduler. + """ + + def __init__( + self, + optimizer, + num_epochs, + epoch_iter, + scale_ratio, + warmup_step: int, + decay_step: int, + gamma: float = 0.1, + ): + self.optimizer = optimizer + self.warmup_step = warmup_step * epoch_iter + self.num_epochs = num_epochs + self.gamma = gamma + self.epoch_iter = epoch_iter # number of batches in each epoch + self.scale_ratio = scale_ratio + + # Convert decay_step from epochs to batches. + # If decay_step <= 0, disable decay and avoid division by zero. + self.decay_step_in_batches = ( + decay_step * epoch_iter if decay_step > 0 else -1 + ) + + # Save base learning rates + self.initial_lr_groups = [ + param_group["lr"] + for param_group in self.optimizer.param_groups + ] + self.current_iter = 0 + + def get_lr_factor(self, cur_step): + """Learning rate lambda logic from W2V original implementation.""" + if cur_step < self.warmup_step: + return (cur_step + 1) / (self.warmup_step + 1) + else: + if self.decay_step_in_batches > 0: + return self.gamma ** ( + (cur_step - self.warmup_step) + // self.decay_step_in_batches + ) + else: + return 1.0 + + def set_lr_factor(self, factor): + """Apply LR factor to all param groups.""" + for lr_target, param_group in zip( + self.initial_lr_groups, self.optimizer.param_groups + ): + param_group["lr"] = lr_target * factor + + def get_lr(self): + """Return the current learning rate.""" + return self.optimizer.param_groups[0]["lr"] + + def step(self, current_iter=None): + """ + Called at every training iteration. + If current_iter is provided by caller (train.py), use it. + """ + if current_iter is not None: + self.current_iter = current_iter + + factor = self.get_lr_factor(self.current_iter) + self.set_lr_factor(factor) + + if current_iter is None: + self.current_iter += 1 + +class WarmupCosineScheduler: + """ + A Wespeaker-compatible class version of WarmupCosineScheduler from + the W2V codebase. It does not inherit a base class and follows a + pattern similar to MarginScheduler. + """ + + def __init__( + self, + optimizer, + num_epochs, + epoch_iter, # corresponds to step_per_epoch in W2V code + min_lr, + max_lr, + warmup_epoch, + fix_epoch, + scale_ratio, + ): + self.optimizer = optimizer + self.num_epochs = num_epochs + assert min_lr <= max_lr + self.min_lr = min_lr + self.max_lr = max_lr + self.step_per_epoch = epoch_iter + self.scale_ratio = scale_ratio + + self.warmup_step = warmup_epoch * self.step_per_epoch + self.fix_step = int(fix_epoch * self.step_per_epoch) + self.current_step = 0.0 # matches behavior in W2V code + + def set_lr(self, new_lr): + for param_group in self.optimizer.param_groups: + param_group["lr"] = new_lr + return new_lr + + def step(self, current_iter=None): + """ + Called every iteration by train.py. + Accepts external current_iter for global-step control. + """ + if current_iter is not None: + self.current_step = current_iter + + new_lr = self.clr(self.current_step) + self.set_lr(new_lr) + + if current_iter is None: + self.current_step += 1 + return new_lr + + def clr(self, step): + """Cosine LR schedule with warmup and fixed LR stages.""" + if step < self.warmup_step: + if self.warmup_step == 0: + return self.max_lr + return self.max_lr * (step / self.warmup_step) + elif self.warmup_step <= step < self.fix_step: + return ( + self.min_lr + + 0.5 * (self.max_lr - self.min_lr) + * ( + 1 + + math.cos( + math.pi + * (step - self.warmup_step) + / (self.fix_step - self.warmup_step) + ) + ) + ) + else: + return self.min_lr + + def get_lr(self): + """Return current LR (Wespeaker compatibility).""" + return self.optimizer.param_groups[0]["lr"] + +class BaseClass: + ''' + Base Class for learning rate scheduler + ''' + + def __init__(self, + optimizer, + num_epochs, + epoch_iter, + initial_lr, + final_lr, + warm_up_epoch=6, + scale_ratio=1.0, + warm_from_zero=False): + ''' + warm_up_epoch: the first warm_up_epoch is the multiprocess warm-up stage + scale_ratio: multiplied to the current lr in the multiprocess training + process + ''' + self.optimizer = optimizer + self.max_iter = num_epochs * epoch_iter + self.initial_lr = initial_lr + self.final_lr = final_lr + self.scale_ratio = scale_ratio + self.current_iter = 0 + self.warm_up_iter = warm_up_epoch * epoch_iter + self.warm_from_zero = warm_from_zero + + def get_multi_process_coeff(self): + lr_coeff = 1.0 * self.scale_ratio + if self.current_iter < self.warm_up_iter: + if self.warm_from_zero: + lr_coeff = self.scale_ratio * self.current_iter / self.warm_up_iter + elif self.scale_ratio > 1: + lr_coeff = (self.scale_ratio - + 1) * self.current_iter / self.warm_up_iter + 1.0 + + return lr_coeff + + def get_current_lr(self): + ''' + This function should be implemented in the child class + ''' + return 0.0 + + def get_lr(self): + return self.optimizer.param_groups[0]['lr'] + + def set_lr(self): + current_lr = self.get_current_lr() + for param_group in self.optimizer.param_groups: + param_group['lr'] = current_lr + + def step(self, current_iter=None): + if current_iter is not None: + self.current_iter = current_iter + + self.set_lr() + self.current_iter += 1 + + def step_return_lr(self, current_iter=None): + if current_iter is not None: + self.current_iter = current_iter + + current_lr = self.get_current_lr() + self.current_iter += 1 + + return current_lr + + +class ExponentialDecrease(BaseClass): + + def __init__(self, + optimizer, + num_epochs, + epoch_iter, + initial_lr, + final_lr, + warm_up_epoch=6, + scale_ratio=1.0, + warm_from_zero=False): + super().__init__(optimizer, num_epochs, epoch_iter, initial_lr, + final_lr, warm_up_epoch, scale_ratio, warm_from_zero) + + def get_current_lr(self): + lr_coeff = self.get_multi_process_coeff() + current_lr = lr_coeff * self.initial_lr * math.exp( + (self.current_iter / self.max_iter) * + math.log(self.final_lr / self.initial_lr)) + return current_lr + + +class TriAngular2(BaseClass): + ''' + The implementation of https://arxiv.org/pdf/1506.01186.pdf + ''' + + def __init__(self, + optimizer, + num_epochs, + epoch_iter, + initial_lr, + final_lr, + warm_up_epoch=6, + scale_ratio=1.0, + cycle_step=2, + reduce_lr_diff_ratio=0.5): + super().__init__(optimizer, num_epochs, epoch_iter, initial_lr, + final_lr, warm_up_epoch, scale_ratio) + + self.reduce_lr_diff_ratio = reduce_lr_diff_ratio + self.cycle_iter = cycle_step * epoch_iter + self.step_size = self.cycle_iter // 2 + + self.max_lr = initial_lr + self.min_lr = final_lr + self.gap = self.max_lr - self.min_lr + + def get_current_lr(self): + lr_coeff = self.get_multi_process_coeff() + point = self.current_iter % self.cycle_iter + cycle_index = self.current_iter // self.cycle_iter + + self.max_lr = self.min_lr + self.gap * self.reduce_lr_diff_ratio**cycle_index + + if point <= self.step_size: + current_lr = self.min_lr + (self.max_lr - + self.min_lr) * point / self.step_size + else: + current_lr = self.max_lr - (self.max_lr - self.min_lr) * ( + point - self.step_size) / self.step_size + + current_lr = lr_coeff * current_lr + + return current_lr + + +def show_lr_curve(scheduler): + import matplotlib.pyplot as plt + + lr_list = [] + for current_lr in range(0, scheduler.max_iter): + lr_list.append(scheduler.step_return_lr(current_lr)) + data_index = list(range(1, len(lr_list) + 1)) + + plt.plot(data_index, lr_list, '-o', markersize=1) + plt.legend(loc='best') + plt.xlabel("Iteration") + plt.ylabel("LR") + + plt.show() + + +if __name__ == '__main__': + optimizer = None + num_epochs = 6 + epoch_iter = 500 + initial_lr = 0.6 + final_lr = 0.1 + warm_up_epoch = 2 + scale_ratio = 4 + scheduler = ExponentialDecrease(optimizer, num_epochs, epoch_iter, + initial_lr, final_lr, warm_up_epoch, + scale_ratio) + # scheduler = TriAngular2(optimizer, + # num_epochs, + # epoch_iter, + # initial_lr, + # final_lr, + # warm_up_epoch, + # scale_ratio, + # cycle_step=2, + # reduce_lr_diff_ratio=0.5) + + show_lr_curve(scheduler) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/score_metrics.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/score_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..57f366db303f618e9de1ecb3a894bf4a1c68311f --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/score_metrics.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +""" +This script computes the official performance metrics for the NIST 2016 SRE. +The metrics include EER and DCFs (min/act). +""" + +__author__ = "Omid Sadjadi" +__email__ = "omid.sadjadi@nist.gov" +__version__ = "4.1" + +import numpy as np +from scipy.stats import norm +import matplotlib.pyplot as plt +import sys + + +def compute_norm_counts(scores, edges, wghts=None): + """ computes normalized (and optionally weighted) score counts for the + bin edges. + """ + + if scores.size > 0: + score_counts = np.histogram(scores, bins=edges, + weights=wghts)[0].astype('f') + norm_counts = np.cumsum(score_counts) / score_counts.sum() + else: + norm_counts = None + return norm_counts + + +def compute_pmiss_pfa(scores, labels, weights=None): + """ computes false positive rate (FPR) and false negative rate (FNR) + given trial socres and their labels. A weights option is also provided to + equalize the counts over score partitions (if there is such partitioning). + """ + + tgt_scores = scores[labels == 1] # target trial scores + imp_scores = scores[labels == 0] # impostor trial scores + + resol = max( + [np.count_nonzero(labels == 0), + np.count_nonzero(labels == 1), 1.e6]) + edges = np.linspace(np.min(scores), np.max(scores), resol) + + if weights is not None: + tgt_weights = weights[labels == 1] + imp_weights = weights[labels == 0] + else: + tgt_weights = None + imp_weights = None + + fnr = compute_norm_counts(tgt_scores, edges, tgt_weights) + fpr = 1 - compute_norm_counts(imp_scores, edges, imp_weights) + + return fnr, fpr + + +def compute_pmiss_pfa_rbst(scores, labels, weights=None): + """ computes false positive rate (FPR) and false negative rate (FNR) + given trial socres and their labels. A weights option is also provided to + equalize the counts over score partitions (if there is such partitioning). + """ + + sorted_ndx = np.argsort(scores) + labels = labels[sorted_ndx] + if weights is not None: + weights = weights[sorted_ndx] + else: + weights = np.ones((labels.shape), dtype='f8') + + tgt_wghts = weights * (labels == 1).astype('f8') + imp_wghts = weights * (labels == 0).astype('f8') + + fnr = np.cumsum(tgt_wghts) / np.sum(tgt_wghts) + fpr = 1 - np.cumsum(imp_wghts) / np.sum(imp_wghts) + return fnr, fpr + + +def compute_eer(fnr, fpr, scores=None): + """ computes the equal error rate (EER) given FNR and FPR values calculated + for a range of operating points on the DET curve + """ + + diff_pm_fa = fnr - fpr + x1 = np.flatnonzero(diff_pm_fa >= 0)[0] + x2 = np.flatnonzero(diff_pm_fa < 0)[-1] + a = (fnr[x1] - fpr[x1]) / (fpr[x2] - fpr[x1] - (fnr[x2] - fnr[x1])) + + if scores is not None: + score_sort = np.sort(scores) + return fnr[x1] + a * (fnr[x2] - fnr[x1]), score_sort[x1] + + return fnr[x1] + a * (fnr[x2] - fnr[x1]) + + +def compute_c_norm(fnr, fpr, p_target, c_miss=1, c_fa=1): + """ computes normalized minimum detection cost function (DCF) given + the costs for false accepts and false rejects as well as a priori + probability for target speakers + """ + + c_det = min(c_miss * fnr * p_target + c_fa * fpr * (1 - p_target)) + c_def = min(c_miss * p_target, c_fa * (1 - p_target)) + + return c_det / c_def + + +def compute_c_dcf(fnr, fpr, p_target, c_miss=1, c_fa=1): + """ computes normalized minimum detection cost function (DCF) given + the costs for false accepts and false rejects as well as a priori + probability for target speakers + """ + + c_det = min(c_miss * fnr * p_target + c_fa * fpr * (1 - p_target)) + + return c_det + + +def plot_det_curve(fnr, fpr, save_path=None): + """ plots the detection error trade-off (DET) curve + """ + + p_miss = norm.ppf(fnr) + p_fa = norm.ppf(fpr) + + xytick = [ + 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, + 0.2, 0.4 + ] + xytick_labels = map(str, [x * 100 for x in xytick]) + + plt.plot(p_fa, p_miss, 'r') + plt.xticks(norm.ppf(xytick), xytick_labels) + plt.yticks(norm.ppf(xytick), xytick_labels) + plt.xlim(norm.ppf([0.00051, 0.5])) + plt.ylim(norm.ppf([0.00051, 0.5])) + plt.xlabel("false-alarm rate [%]", fontsize=12) + plt.ylabel("false-reject rate [%]", fontsize=12) + eer = compute_eer(fnr, fpr) + plt.plot(norm.ppf(eer), norm.ppf(eer), 'o') + plt.annotate( + "EER = %.2f%%" % (eer * 100), + xy=(norm.ppf(eer), norm.ppf(eer)), + xycoords='data', + xytext=(norm.ppf(eer + 0.05), norm.ppf(eer + 0.05)), + textcoords='data', + arrowprops=dict(arrowstyle="-|>", + connectionstyle="arc3, rad=+0.2", + fc="w"), + size=12, + va='center', + ha='center', + bbox=dict(boxstyle="round4", fc="w"), + ) + plt.grid() + if save_path is not None: + plt.savefig(save_path) + plt.clf() + else: + plt.show() + + +def compute_equalized_scores(max_tar_imp_counts, sc, labs, masks): + + count_weights = [] + scores = [] + labels = [] + for ix in range(len(masks)): + amask = masks[ix] + alabs = labs[amask] + num_targets = np.count_nonzero(alabs == 1) + num_non_targets = alabs.size - num_targets + labels.append(alabs) + scores.append(sc[amask]) + tar_weight = max_tar_imp_counts[ + 0] / num_targets if num_targets > 0 else 0 + imp_weight = max_tar_imp_counts[ + 1] / num_non_targets if num_non_targets > 0 else 0 + + # print('''condition: {}, #targets: {}, weight: {}, #non-targets: {}, + # weight: {}'''.format(ix + 1, num_targets, tar_weight, + # num_non_targets, imp_weight)) + + acount_weights = np.empty(alabs.shape, dtype='f') + acount_weights[alabs == 1] = np.array([tar_weight] * num_targets) + acount_weights[alabs == 0] = np.array([imp_weight] * num_non_targets) + count_weights.append(acount_weights) + + scores = np.hstack(scores) + labels = np.hstack(labels) + count_weights = np.hstack(count_weights) + + return scores, labels, count_weights + + +if __name__ == '__main__': + scores = [] + labels = [] + scores_file = sys.argv[1] + + with open(scores_file) as readlines: + for line in readlines: + tokens = line.strip().split() + scores.append(float(tokens[-1])) + labels.append(tokens[0][:7] == tokens[1][:7]) + + scores = np.hstack(scores) + labels = np.hstack(labels) + + p_tgt_1, p_tgt_2 = 0.01, 0.005 + + fnr, fpr = compute_pmiss_pfa_rbst(scores, labels) + eer, thres = compute_eer(fnr, fpr, scores) + # print("EER:{:.2f}% Thres:{:.4f}".format(eer*100, thres)) + + min_dcf_1 = compute_c_norm(fnr, fpr, p_tgt_1) + min_dcf_2 = compute_c_norm(fnr, fpr, p_tgt_2) + min_c_primary = (min_dcf_1 + min_dcf_2) / 2 + min_dcf_08 = compute_c_norm(fnr, fpr, p_target=0.01, c_miss=1, c_fa=1) + min_dcf_10 = compute_c_norm(fnr, fpr, p_target=0.001, c_miss=1, c_fa=1) + print("---- Unequalized -----") + print("EER = {0:.2f}".format(100 * eer)) + print("minDCF08 = {0:.4f}, minDCF10 = {1:.4f}, minCprimary = {2:.4f}\n\n". + format(min_dcf_08, min_dcf_10, min_c_primary)) + # print("minDCF1 = {0:.4f}, minDCF2 = {1:.4f}, minCprimary = {2:.4f}\n\n". + # format(min_dcf_1, min_dcf_2, min_c_primary)) + + plot_det_curve(fnr, fpr) diff --git a/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/utils.py b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..23bb3d04c4c2275bc2f92135240b3f22c63dda52 --- /dev/null +++ b/MOSS_Speaker-RoPE/wespeaker/wespeaker/utils/utils.py @@ -0,0 +1,83 @@ +# Copyright (c) 2022 Hongji Wang (jijijiang77@gmail.com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +import random + +import numpy as np +import torch +import yaml + + +def get_logger(outdir, fname): + formatter = logging.Formatter( + "[ %(levelname)s : %(asctime)s ] - %(message)s") + logging.basicConfig(level=logging.DEBUG, + format="[ %(levelname)s : %(asctime)s ] - %(message)s") + logger = logging.getLogger("Pyobj, f") + # Dump log to file + fh = logging.FileHandler(os.path.join(outdir, fname)) + fh.setFormatter(formatter) + logger.addHandler(fh) + return logger + + +def parse_config_or_kwargs(config_file, **kwargs): + """parse_config_or_kwargs + + :param config_file: Config file that has parameters, yaml format + :param **kwargs: Other alternative parameters or overwrites for config + """ + with open(config_file) as con_read: + yaml_config = yaml.load(con_read, Loader=yaml.FullLoader) + # values from config file are all possible params + help_str = "Valid Parameters are:\n" + help_str += "\n".join(list(yaml_config.keys())) + # passed kwargs will override yaml config + # for key in kwargs.keys(): + # assert key in yaml_config, "Parameter {} invalid!\n".format(key) + return dict(yaml_config, **kwargs) + + +def validate_path(dir_name): + """ Create the directory if it doesn't exist + :param dir_name + :return: None + """ + dir_name = os.path.dirname(dir_name) # get the path + if not os.path.exists(dir_name) and (dir_name != ''): + os.makedirs(dir_name) + + +def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + + torch.manual_seed(seed) + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + # torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = True + + +def spk2id(utt_spk_list): + _, spk_list = zip(*utt_spk_list) + spk_list = sorted(set(spk_list)) # remove overlap and sort + + spk2id_dict = {} + for i, spk in enumerate(spk_list): + spk2id_dict[spk] = i + return spk2id_dict diff --git a/speaker_rope.md b/speaker_rope.md new file mode 100644 index 0000000000000000000000000000000000000000..e3186527db6ade9312604bad3bb9d8e4ca1b2c60 --- /dev/null +++ b/speaker_rope.md @@ -0,0 +1,953 @@ + +# Multi-axis Relative Rotary Encoding with Layer-wise Random Speaker Projection for Speaker-aware Attention + +--- + +# 1. 动机(Motivation) + +## 1.1 Transformer 中相对关系建模的重要性 + +Self-attention 的核心计算为: + +[ +A_{ij}=q_i^Tk_j +] + +该计算仅依赖 query 与 key 的内容匹配,而缺少显式结构关系建模。 + +因此 Transformer 通常引入位置编码,使模型能够感知 token 间的相对关系。 + +RoPE(Rotary Position Embedding)的核心思想是不直接将 position embedding 加入 token 表示,而是通过旋转变换将绝对位置映射到 query/key 空间: + +[ +q_i'=R(p_i)q_i +] + +[ +k_j'=R(p_j)k_j +] + +此时 attention 得分: + +[ +(q_i')^Tk_j' +] + +展开: + +# [ + +q_i^TR(p_i)^TR(p_j)k_j +] + +由于旋转矩阵满足: + +[ +R(a)^TR(b)=R(b-a) +] + +因此: + +[ +\boxed{ +A_{ij} +====== + +q_i^TR(p_j-p_i)k_j +} +] + +即 attention 不再依赖绝对位置: + +[ +p_i,p_j +] + +而只依赖: + +[ +p_j-p_i +] + +因此 RoPE 将: + +[ +\text{absolute position} +] + +转化为: + +[ +\boxed{\text{relative position}} +] + +--- + +# 1.2 Speaker embedding 的特殊性 + +在 speaker diarization、speaker-attributed ASR 以及 conversation modeling 中,通常利用 speaker embedding: + +[ +e_s\in\mathbb R^{d_e} +] + +表示说话人身份。 + +然而,与时间位置不同,speaker embedding 不具有绝对坐标语义。 + +对于任意保持空间结构的旋转: + +[ +e'_s=Ue_s +] + +其中: + +[ +U^TU=I +] + +有: + +[ +\cos(e_i',e_j') +=============== + +\cos(e_i,e_j) +] + +说明 speaker embedding 的语义主要存在于: + +[ +\boxed{ +\text{relative geometry} +} +] + +而非: + +[ +\text{absolute coordinate} +] + +因此,一个理想的 speaker encoding 应满足: + +1. 不依赖 speaker embedding 的绝对方向; +2. 保留 speaker embedding 空间中的相对关系; +3. 相同 speaker token 保持较强 interaction; +4. 不同 speaker token 自动产生 attention decorrelation。 + +--- + +# 1.3 从二维 RoPE 到 Speaker-aware Rotary Encoding + +二维 RoPE 将二维位置: + +[ +(x,y) +] + +映射到两个独立 rotary 子空间: + +[ +R(x,y) +====== + +R_x(x)\oplus R_y(y) +] + +使不同相对轴分别建模。 + +类似地,在 speaker-aware attention 中,可以认为: + +[ +(position,speaker) +] + +构成两个不同的相对结构: + +* position axis: + 时间相对距离; + +* speaker axis: + speaker embedding 几何关系。 + +因此设计: + +[ +\boxed{ +R_{total} +========= + +R_{pos} +\oplus +R_{spk} +} +] + +其中: + +* position rotary 保留原始 RoPE; +* speaker rotary 建模相对 speaker relationship。 + +--- + +# 2. 编码设计(Encoding Design) + +--- + +# 2.1 Block-wise Rotary Dimension Split + +设 Transformer attention head 维度: + +[ +d_h +] + +划分为: + +[ +d_h=d_p+d_s +] + +其中: + +* (d_p):position rotary 子空间; +* (d_s):speaker rotary 子空间。 + +例如: + +[ +d_h=64 +] + +采用: + +[ +d_p=32 +] + +[ +d_s=32 +] + +则: + +[ +q_i= +[q_i^{pos};q_i^{spk}] +] + +[ +k_i= +[k_i^{pos};k_i^{spk}] +] + +即: + +[ +\boxed{ +\text{前半部分编码时间,后半部分编码 speaker} +} +] + +--- + +# 2.2 Position Rotary Branch + +position 分支完全保持原始 RoPE。 + +对于第 (l) 层: + +[ +q_{i,l}^{pos\prime} +=================== + +R(p_i)q_{i,l}^{pos} +] + +[ +k_{i,l}^{pos\prime} +=================== + +R(p_i)k_{i,l}^{pos} +] + +其中: + +[ +p_i=i +] + +--- + +# 2.3 Layer-wise Random Speaker Projection + +## Speaker embedding normalization + +首先对 speaker embedding 归一化: + +[ +\hat e_i +======== + +\frac{e_i}{||e_i||} +] + +后续记: + +[ +e_i=\hat e_i +] + +因此: + +[ +||e_i||=1 +] + +--- + +## Layer-wise projection matrix + +不同于共享单一 projection,本方法为每个 Transformer layer 分配独立随机矩阵: + +[ +\boxed{ +W_l\in +\mathbb R^{d_s/2\times d_e} +} +] + +其中: + +[ +\boxed{ +(W_l)_{mn}\sim N(0,1) +} +] + +并满足: + +* 不同 layer: + +[ +W_l\neq W_{l'} +] + +* 同一 layer、同一 utterance: + +所有 token 使用同一个: + +[ +W_l +] + +因此: + +[ +\theta_{i,l} +============ + +W_le_i +] + +--- + +## 为什么采用 layer-wise projection? + +不同 layer 学习不同抽象层级的 speaker relationship。 + +因此: + +layer (l): + +[ +\theta_{i,l}=W_le_i +] + +相当于从不同随机子空间观察 speaker manifold。 + +同时: + +由于同一 layer 内: + +[ +W_l +] + +保持一致: + +[ +\theta_{j,l}-\theta_{i,l} +========================= + +W_l(e_j-e_i) +] + +保证 relative speaker property。 + +--- + +# 2.4 Speaker Rotary Transformation + +第 (l) 层: + +[ +q_{i,l}^{spk\prime} +=================== + +R(\theta_{i,l})q_{i,l}^{spk} +] + +[ +k_{i,l}^{spk\prime} +=================== + +R(\theta_{i,l})k_{i,l}^{spk} +] + +--- + +# 2.5 Combined Attention + +最终: + +[ +q_i' +==== + +[ +q_i^{pos\prime}; +q_i^{spk\prime} +] +] + +[ +k_j' +==== + +[ +k_j^{pos\prime}; +k_j^{spk\prime} +] +] + +attention: + +[ +A_{ij} +====== + +(q_i')^Tk_j' +] + +由于两个子空间正交: + +[ +\boxed{ +A_{ij} +====== + +A_{pos} ++ +A_{spk} +} +] + +其中: + +[ +A_{pos} +======= + +(q_i^{pos\prime})^Tk_j^{pos\prime} +] + +[ +A_{spk} +======= + +(q_i^{spk\prime})^Tk_j^{spk\prime} +] + +--- + +# 3. 理论证明(Theoretical Analysis) + +--- + +# 3.1 原始 RoPE 相对位置性质保持 + +考虑 position branch: + +[ +q_i^{pos\prime} +=============== + +R(p_i)q_i^{pos} +] + +[ +k_j^{pos\prime} +=============== + +R(p_j)k_j^{pos} +] + +attention: + +[ +A_{pos} +======= + +(q_i^{pos\prime})^Tk_j^{pos\prime} +] + +展开: + +# [ + +(q_i^{pos})^TR(p_i)^TR(p_j)k_j^{pos} +] + +利用旋转性质: + +[ +R(p_i)^TR(p_j) +============== + +R(p_j-p_i) +] + +得到: + +[ +\boxed{ +A_{pos} +======= + +(q_i^{pos})^TR(p_j-p_i)k_j^{pos} +} +] + +因此: + +加入 speaker rotary 后: + +[ +\boxed{ +\text{original RoPE relative position property is preserved} +} +] + +原因: + +position rotary 与 speaker rotary 位于独立子空间。 + +--- + +# 3.2 Layer-wise Speaker Rotary Relative Property + +对于第 (l) 层: + +[ +q_{i,l}^{spk\prime} +=================== + +R(\theta_{i,l})q_{i,l}^{spk} +] + +[ +k_{j,l}^{spk\prime} +=================== + +R(\theta_{j,l})k_{j,l}^{spk} +] + +因此: + +[ +A_{spk}^{(l)} +============= + +(q_i^{spk})^T +R(\theta_{i,l})^TR(\theta_{j,l}) +k_j^{spk} +] + +得到: + +# [ + +(q_i^{spk})^T +R(\theta_{j,l}-\theta_{i,l}) +k_j^{spk} +] + +由于: + +[ +\theta_{i,l}=W_le_i +] + +所以: + +[ +\theta_{j,l}-\theta_{i,l} +========================= + +W_l(e_j-e_i) +] + +因此: + +[ +\boxed{ +A_{spk}^{(l)} +============= + +(q_i^{spk})^T +R(W_l(e_j-e_i)) +k_j^{spk} +} +] + +说明: + +第 (l) 层 speaker rotary 只依赖: + +[ +\boxed{ +e_j-e_i +} +] + +而不依赖: + +[ +e_i,e_j +] + +的绝对坐标。 + +--- + +# 3.3 Overall Multi-axis Relative Property + +第 (l) 层: + +[ +A_{ij}^{(l)} +============ + +A_{pos}^{(l)} ++ +A_{spk}^{(l)} +] + +即: + +# [ + +q_p^TR(p_j-p_i)k_p ++ +q_s^TR(W_l(e_j-e_i))k_s +] + +因此: + +[ +\boxed{ +A_{ij}^{(l)} +============ + +f( +p_j-p_i, +e_j-e_i +) +} +] + +同时满足: + +* relative position encoding; +* relative speaker encoding。 + +--- + +# 3.4 Speaker Rotary Phase Decorrelation + +考虑: + +[ +\Delta e=e_j-e_i +] + +speaker rotary 中: + +[ +R(W_l\Delta e) +] + +对应复数形式: + +[ +e^{iw_r^T\Delta e} +] + +其中: + +[ +w_r\sim N(0,I) +] + +由于: + +[ +w_r^T\Delta e +\sim +N(0,||\Delta e||^2) +] + +根据 Gaussian characteristic function: + +[ +E[e^{ix}] +========= + +e^{-\frac12Var(x)} +] + +得到: + +[ +\boxed{ +E[ +e^{iw_r^T\Delta e} +] += + +e^{-\frac12||\Delta e||^2} +} +] + +因此: + +[ +\boxed{ +E[S_m(\Delta e)] +================ + +m e^{-\frac12||\Delta e||^2} +} +] + +随着: + +[ +||e_j-e_i|| +] + +增加: + +phase cancellation 增强。 + +因此: + +[ +\boxed{ +speaker\ distance +\uparrow +\Rightarrow +attention\ correlation +\downarrow +} +] + +--- + +# 3.5 与 Cosine Speaker Similarity 的关系 + +由于: + +[ +||e_i||=||e_j||=1 +] + +有: + +[ +||e_i-e_j||^2 +============= + +2-2cos(e_i,e_j) +] + +因此: + +[ +E[S] +==== + +e^{-(1-cos(e_i,e_j))} +] + +即: + +[ +\boxed{ +cos(e_i,e_j) +\uparrow +\Rightarrow +speaker\ rotary\ correlation +\uparrow +} +] + +与 speaker embedding 几何空间一致。 + +--- + +# 4. 设计评价(Design Summary) + +## 4.1 方法优势 + +### (1) 完全保留原 RoPE 能力 + +采用: + +[ +R_{total} +========= + +R_{pos}\oplus R_{spk} +] + +position branch 与原始 RoPE 完全一致。 + +因此: + +* 不破坏已有位置建模能力; +* 不影响长上下文建模。 + +--- + +### (2) 从绝对 speaker encoding 转向相对 speaker encoding + +传统: + +[ +x+e_s +] + +依赖 speaker embedding 绝对坐标。 + +本文: + +[ +R(W_l(e_j-e_i)) +] + +直接编码: + +[ +speaker\ relationship +] + +--- + +### (3) Layer-wise random projection 提升表示能力 + +不同 Transformer layer: + +[ +W_1,W_2,...,W_L +] + +提供不同随机观察空间。 + +同时: + +同层内共享: + +[ +W_l +] + +保证: + +[ +\theta_j-\theta_i=W_l(e_j-e_i) +] + +严格满足 relative property。 + +--- + +### (4) 理论性质统一 + +最终 attention: + +[ +\boxed{ +A_{ij}^{(l)} +============ + +f( +\Delta p, +\Delta e +) +} +] + +同时具有: + +* RoPE relative position property; +* speaker relative geometry property; +* speaker distance induced decorrelation。 + +--- + +# 4.2 总结 + +本文提出: + +[ +\boxed{ +\textbf{Multi-axis Relative Rotary Encoding with Layer-wise Random Speaker Projection} +} +] + +将 rotary embedding 从单一时间轴扩展到: + +[ +(position,speaker) +] + +通过: + +[ +\boxed{ +R_{total} +========= + +R_{pos} +\oplus +R(W_le) +} +] + +实现: + +* 时间相对关系建模; +* speaker 相对关系建模。 + +理论证明: + +1. 原始 RoPE 的相对位置性质完全保持; +2. speaker rotary 满足 relative speaker encoding; +3. layer-wise random projection 保留 speaker embedding 几何不变性; +4. speaker embedding 距离增加导致 rotary phase cancellation,从而产生 attention decorrelation。 + +该方法提供了一种无需显式 speaker token、无需全局 speaker index 的 speaker-aware Transformer encoding 机制。